Compare commits

..

49 Commits

Author SHA1 Message Date
Tom
aa89578297 Fixed TTS using StreamElements. Fixed several issues. 2026-01-03 05:19:33 +00:00
Tom
fb04f4003f Changed various locking mechanisms. 2025-03-29 20:28:36 +00:00
Tom
eddd9e6403 Added support for group permission messages. 2025-03-29 20:27:55 +00:00
Tom
622b359b12 Added proper slave mode - additional clients after the first connection. Fixed a few issues. Updated to version 4.8.2. 2025-03-06 16:05:15 +00:00
Tom
cbdca1c008 Twitch connection now relies on events to connect. Added logging for when TTS filter is not a regex. Minor code clean up. 2025-01-19 01:23:29 +00:00
Tom
86fc6bc24d Version update to v4.7 2025-01-18 22:59:12 +00:00
Tom
c4e651ff7f Removed useless logging line. 2025-01-18 22:51:26 +00:00
Tom
6e6f20b097 Fixed explicit TTS voices in messages when there is text before the first voice name. 2025-01-18 22:47:15 +00:00
Tom
03d24b0905 Fixed more group stuffs. 2025-01-18 22:45:36 +00:00
Tom
48ac5c4fa0 Updated NuGet packages. 2025-01-18 21:52:33 +00:00
Tom
d13cd71ac0 Minor logging changes. 2025-01-18 21:52:15 +00:00
Tom
5067ffe119 Added chat message to redemptions. Added Subscription End to Twitch. Added more variables to certain redemptions. 2025-01-18 21:51:50 +00:00
Tom
9a17ad16b3 Fixed groups & their websocket support. 2025-01-18 21:41:00 +00:00
Tom
c21890b55d Removing groups from chatters when deleting groups. 2025-01-18 17:56:15 +00:00
Tom
3b24208acc Added connection backoff for OBS. 2025-01-18 17:43:29 +00:00
Tom
c373af5281 Fixed some minor things. 2025-01-18 17:34:02 +00:00
Tom
9f884f71ae Added group chatters support to websocket. 2025-01-18 17:33:15 +00:00
Tom
a49e52a6bb Added group support for websockets. 2025-01-18 17:31:51 +00:00
Tom
aed0421843 Fixed getting scene item id from OBS. 2025-01-18 16:37:04 +00:00
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
Tom
b35183249b Added Veadotube integration 2024-12-02 20:51:04 +00:00
Tom
48dd6858a1 Fixed certain redemptions 2024-11-15 02:29:23 +00:00
Tom
0932c1c38e Fixed redemptions not loading properly 2024-11-08 16:11:24 +00:00
Tom
66f2bf7ec6 Cleaned up request acks. Added internal service bus for internal messaging. 2024-11-08 15:32:42 +00:00
Tom
fe2eb86a08 Removed messages from TTSPublisher 2024-11-08 15:26:10 +00:00
Tom
f47685a17d Update to version 4.4 2024-10-22 07:55:37 +00:00
Tom
69de352318 Fixed ad break message 2024-10-22 07:55:16 +00:00
Tom
07b035039d Added policies. Added action for channel ad break ending. 2024-10-22 07:54:59 +00:00
Tom
f1f345970f Fixed 7tv api due to changes on their end 2024-10-22 07:50:17 +00:00
Tom
77465598c1 Fixed tts add & delete commands 2024-10-22 07:49:09 +00:00
Tom
ed318ca6e8 Cursor from Twitch Event responses is now optional 2024-08-19 04:45:29 +00:00
Tom
dadbe13352 Fixed raid spam prevention for joined channels. 2024-08-14 20:35:35 +00:00
146 changed files with 4436 additions and 1172 deletions

View File

@@ -0,0 +1,10 @@
using HermesSocketLibrary.Requests.Messages;
namespace TwitchChatTTS.Bus.Data
{
public class RedemptionInitiation
{
public required IEnumerable<Redemption> Redemptions { get; set; }
public required IDictionary<string, RedeemableAction> Actions { get; set; }
}
}

83
Bus/ServiceBusCentral.cs Normal file
View File

@@ -0,0 +1,83 @@
using System.Collections.Immutable;
using Serilog;
namespace TwitchChatTTS.Bus
{
public class ServiceBusCentral
{
private readonly IDictionary<string, ServiceBusObservable> _topics;
private readonly IDictionary<string, ISet<IObserver<ServiceBusData>>> _receivers;
private readonly ILogger _logger;
private readonly object _lock;
public ServiceBusCentral(ILogger logger)
{
_topics = new Dictionary<string, ServiceBusObservable>();
_receivers = new Dictionary<string, ISet<IObserver<ServiceBusData>>>();
_logger = logger;
_lock = new object();
}
public void Add(string topic, IObserver<ServiceBusData> observer)
{
lock (_lock)
{
if (!_receivers.TryGetValue(topic, out var observers))
{
observers = new HashSet<IObserver<ServiceBusData>>();
_receivers.Add(topic, observers);
}
observers.Add(observer);
}
}
public ServiceBusObservable GetTopic(string topic)
{
lock (_lock)
{
if (!_topics.TryGetValue(topic, out var bus))
{
bus = new ServiceBusObservable(topic, this, _logger);
_topics.Add(topic, bus);
}
return bus;
}
}
public IEnumerable<IObserver<ServiceBusData>> GetObservers(string topic)
{
lock (_lock)
{
if (_receivers.TryGetValue(topic, out var observers))
return observers.ToImmutableArray();
}
return [];
}
public bool RemoveObserver(string topic, IObserver<ServiceBusData> observer)
{
lock (_lock)
{
if (_receivers.TryGetValue(topic, out var observers))
return observers.Remove(observer);
}
return false;
}
public void Send(object sender, string topic, object value)
{
var observers = GetObservers(topic);
foreach (var consumer in observers)
{
try
{
consumer.OnNext(new ServiceBusData(sender, topic, value));
}
catch (Exception ex)
{
_logger.Error(ex, "Failed to execute observer on send.");
}
}
}
}
}

18
Bus/ServiceBusData.cs Normal file
View File

@@ -0,0 +1,18 @@
namespace TwitchChatTTS.Bus
{
public class ServiceBusData
{
public string Topic { get; }
public object? Sender { get; }
public object? Value { get; }
public DateTime Timestamp { get; }
public ServiceBusData(object sender, string topic, object value)
{
Topic = topic;
Sender = sender;
Value = value;
Timestamp = DateTime.UtcNow;
}
}
}

View File

@@ -0,0 +1,48 @@
using System.Reactive;
using Serilog;
namespace TwitchChatTTS.Bus
{
public class ServiceBusObservable : ObservableBase<ServiceBusData>
{
private readonly string _topic;
private readonly ServiceBusCentral _central;
private readonly ILogger _logger;
public ServiceBusObservable(string topic, ServiceBusCentral central, ILogger logger)
{
_topic = topic;
_central = central;
_logger = logger;
}
protected override IDisposable SubscribeCore(IObserver<ServiceBusData> observer)
{
_central.Add(_topic, 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 readonly string _topic;
private readonly ServiceBusCentral _central;
private readonly IObserver<ServiceBusData> _receiver;
public ServiceBusUnsubscriber(string topic, ServiceBusCentral central, IObserver<ServiceBusData> receiver)
{
_topic = topic;
_central = central;
_receiver = receiver;
}
public void Dispose()
{
_central.RemoveObserver(_topic, _receiver);
}
}
}
}

31
Bus/ServiceBusObserver.cs Normal file
View File

@@ -0,0 +1,31 @@
using System.Reactive;
using Serilog;
namespace TwitchChatTTS.Bus
{
public class ServiceBusObserver : ObserverBase<ServiceBusData>
{
private readonly Action<ServiceBusData> _action;
private readonly ILogger _logger;
public ServiceBusObserver(Action<ServiceBusData> action, ILogger logger)
{
_action = action;
_logger = logger;
}
protected override void OnCompletedCore()
{
}
protected override void OnErrorCore(Exception error)
{
_logger.Error(error, "Error occurred.");
}
protected override void OnNextCore(ServiceBusData value)
{
_action.Invoke(value);
}
}
}

View File

@@ -9,5 +9,6 @@ namespace TwitchChatTTS.Chat.Commands
Syntax = 4, Syntax = 4,
Fail = 5, Fail = 5,
OtherRoom = 6, OtherRoom = 6,
RateLimited = 7
} }
} }

View File

@@ -3,6 +3,7 @@ using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common; using CommonSocketLibrary.Common;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Serilog; using Serilog;
using TwitchChatTTS.Chat.Commands.Limits;
using TwitchChatTTS.Chat.Groups.Permissions; using TwitchChatTTS.Chat.Groups.Permissions;
using TwitchChatTTS.Hermes.Socket; using TwitchChatTTS.Hermes.Socket;
using TwitchChatTTS.Twitch.Socket.Messages; using TwitchChatTTS.Twitch.Socket.Messages;
@@ -13,10 +14,10 @@ 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 ILogger _logger; private readonly ILogger _logger;
private string CommandStartSign { get; } = "!"; private string CommandStartSign { get; } = "!";
@@ -24,22 +25,22 @@ 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,
ILogger logger ILogger logger
) )
{ {
_user = user; _user = user;
_hermes = (hermes as HermesSocketClient)!; _hermes = (hermes as HermesSocketClient)!;
//_twitch = (twitch as TwitchWebsocketClient)!;
_permissionManager = permissionManager; _permissionManager = permissionManager;
_permissionPolicy = limitManager;
_logger = logger; _logger = logger;
} }
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();
@@ -69,9 +70,10 @@ namespace TwitchChatTTS.Chat.Commands
// Check if command can be executed by this chatter. // Check if command can be executed by this chatter.
var command = selectorResult.Command; var command = selectorResult.Command;
long chatterId = long.Parse(message.ChatterUserId); long chatterId = long.Parse(message.ChatterUserId);
var path = $"tts.commands.{com}";
if (chatterId != _user.OwnerId) if (chatterId != _user.OwnerId)
{ {
bool executable = command.AcceptCustomPermission ? CanExecute(chatterId, groups, $"tts.commands.{com}", selectorResult.Permissions) : false; bool executable = command.AcceptCustomPermission ? CanExecute(chatterId, groups, path, selectorResult.Permissions) : false;
if (!executable) if (!executable)
{ {
_logger.Warning($"Denied permission to use command [chatter id: {chatterId}][args: {arg}][command type: {command.GetType().Name}]"); _logger.Warning($"Denied permission to use command [chatter id: {chatterId}][args: {arg}][command type: {command.GetType().Name}]");
@@ -79,6 +81,12 @@ namespace TwitchChatTTS.Chat.Commands
} }
} }
if (!_permissionPolicy.TryUse(chatterId, groups, path))
{
_logger.Warning($"Chatter reached usage limit on command [command type: {command.GetType().Name}][chatter id: {chatterId}][path: {path}][groups: {string.Join("|", groups)}]");
return ChatCommandResult.RateLimited;
}
// Check if the arguments are valid. // Check if the arguments are valid.
var arguments = _commandSelector.GetNonStaticArguments(args, selectorResult.Path); var arguments = _commandSelector.GetNonStaticArguments(args, selectorResult.Path);
foreach (var entry in arguments) foreach (var entry in arguments)
@@ -88,7 +96,7 @@ namespace TwitchChatTTS.Chat.Commands
// Optional parameters were validated while fetching this command. // Optional parameters were validated while fetching this command.
if (!parameter.Optional && !parameter.Validate(argument, message.Message.Fragments)) if (!parameter.Optional && !parameter.Validate(argument, message.Message.Fragments))
{ {
_logger.Warning($"Command failed due to an argument being invalid [argument name: {parameter.Name}][argument value: {argument}][arguments: {arg}][command type: {command.GetType().Name}][chatter: {message.ChatterUserLogin}][chatter id: {message.ChatterUserId}]"); _logger.Warning($"Command failed due to an argument being invalid [argument name: {parameter.Name}][argument value: {argument}][parameter type: {parameter.GetType().Name}][arguments: {arg}][command type: {command.GetType().Name}][chatter: {message.ChatterUserLogin}][chatter id: {message.ChatterUserId}]");
return ChatCommandResult.Syntax; return ChatCommandResult.Syntax;
} }
} }

View File

@@ -1,88 +0,0 @@
namespace TwitchChatTTS.Chat.Commands.Limits
{
public interface ICommandLimitManager
{
bool HasReachedLimit(long chatterId, string name, string group);
void RemoveUsageLimit(string name, string group);
void SetUsageLimit(int count, TimeSpan span, string name, string group);
bool TryUse(long chatterId, string name, string group);
}
public class CommandLimitManager : ICommandLimitManager
{
// group + name -> chatter id -> usage
private readonly IDictionary<string, IDictionary<long, Usage>> _usages;
// group + name -> limit
private readonly IDictionary<string, Limit> _limits;
public CommandLimitManager()
{
_usages = new Dictionary<string, IDictionary<long, Usage>>();
_limits = new Dictionary<string, Limit>();
}
public bool HasReachedLimit(long chatterId, string name, string group)
{
throw new NotImplementedException();
}
public void RemoveUsageLimit(string name, string group)
{
throw new NotImplementedException();
}
public void SetUsageLimit(int count, TimeSpan span, string name, string group)
{
throw new NotImplementedException();
}
public bool TryUse(long chatterId, string name, string group)
{
var path = $"{group}.{name}";
if (!_limits.TryGetValue(path, out var limit))
return true;
if (!_usages.TryGetValue(path, out var groupUsage))
{
groupUsage = new Dictionary<long, Usage>();
_usages.Add(path, groupUsage);
}
if (!groupUsage.TryGetValue(chatterId, out var usage))
{
usage = new Usage()
{
Usages = new long[limit.Count],
Index = 0
};
groupUsage.Add(chatterId, usage);
}
int first = (usage.Index + 1) % limit.Count;
long timestamp = DateTime.UtcNow.Ticks / TimeSpan.TicksPerMillisecond;
if (timestamp - usage.Usages[first] < limit.Span)
{
return false;
}
usage.Usages[usage.Index] = timestamp;
usage.Index = first;
return true;
}
private class Usage
{
public long[] Usages { get; set; }
public int Index { get; set; }
}
private struct Limit
{
public int Count { get; set; }
public int Span { get; set; }
}
}
}

View File

@@ -0,0 +1,10 @@
namespace TwitchChatTTS.Chat.Commands.Limits
{
public interface IUsagePolicy<K>
{
void Remove(string group, string policy);
void Set(string group, string policy, int count, TimeSpan span);
bool TryUse(K key, string group, string policy);
public bool TryUse(K key, IEnumerable<string> groups, string policy);
}
}

View File

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

@@ -36,13 +36,13 @@ namespace TwitchChatTTS.Chat.Commands
{ {
b.CreateStaticInputParameter("add", b => b.CreateStaticInputParameter("add", b =>
{ {
b.CreateVoiceNameParameter("voiceName", false) b.CreateUnvalidatedParameter("voiceName")
.CreateCommand(new AddTTSVoiceCommand(_user, _logger)); .CreateCommand(new AddTTSVoiceCommand(_user, _logger));
}) })
.AddAlias("insert", "add") .AddAlias("insert", "add")
.CreateStaticInputParameter("delete", b => .CreateStaticInputParameter("delete", b =>
{ {
b.CreateVoiceNameParameter("voiceName", true) b.CreateVoiceNameParameter("voiceName", false)
.CreateCommand(new DeleteTTSVoiceCommand(_user, _logger)); .CreateCommand(new DeleteTTSVoiceCommand(_user, _logger));
}) })
.AddAlias("del", "delete") .AddAlias("del", "delete")

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

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

View File

@@ -12,6 +12,8 @@ namespace TwitchChatTTS.Chat.Groups
IEnumerable<string> GetGroupNamesFor(long chatter); 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

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

View File

@@ -12,7 +12,7 @@ using TwitchChatTTS.Twitch.Socket.Messages;
namespace TwitchChatTTS.Chat.Messaging namespace TwitchChatTTS.Chat.Messaging
{ {
public class ChatMessageReader public class ChatMessageReader : IChatMessageReader
{ {
private readonly User _user; private readonly User _user;
private readonly TTSPlayer _player; private readonly TTSPlayer _player;
@@ -62,15 +62,15 @@ namespace TwitchChatTTS.Chat.Messaging
var emoteUsage = GetEmoteUsage(fragments); var emoteUsage = GetEmoteUsage(fragments);
var tasks = new List<Task>(); var tasks = new List<Task>();
if (_obs.Streaming) if ((!_obs.Connected || _obs.Streaming) && !_user.Slave)
{ {
if (emoteUsage.NewEmotes.Any()) if (emoteUsage.NewEmotes.Any())
tasks.Add(_hermes.SendEmoteDetails(emoteUsage.NewEmotes)); tasks.Add(_hermes.SendEmoteDetails(emoteUsage.NewEmotes));
if (emoteUsage.EmotesUsed.Any() && messageId != null && chatterId != null) if (emoteUsage.EmotesUsed.Any() && messageId != null && chatterId != null)
tasks.Add(_hermes.SendEmoteUsage(messageId, chatterId.Value, emoteUsage.EmotesUsed)); tasks.Add(_hermes.SendEmoteUsage(messageId, chatterId.Value, emoteUsage.EmotesUsed));
if (!string.IsNullOrEmpty(chatterLogin) && chatterId != null && !_user.Chatters.Contains(chatterId.Value)) if (chatterId.HasValue && !_user.Chatters.Contains(chatterId.Value))
{ {
tasks.Add(_hermes.SendChatterDetails(chatterId.Value, chatterLogin)); tasks.Add(_hermes.SendChatterDetails(chatterId.Value, chatterLogin!));
_user.Chatters.Add(chatterId.Value); _user.Chatters.Add(chatterId.Value);
} }
} }
@@ -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);
@@ -94,7 +95,6 @@ namespace TwitchChatTTS.Chat.Messaging
private IEnumerable<TTSMessage> HandlePartialMessage(string voice, string message) private IEnumerable<TTSMessage> HandlePartialMessage(string voice, string message)
{ {
var parts = _sfxRegex.Split(message); var parts = _sfxRegex.Split(message);
if (parts.Length == 1) if (parts.Length == 1)
{ {
return [new TTSMessage() return [new TTSMessage()
@@ -224,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))
@@ -234,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

@@ -0,0 +1,10 @@
using TwitchChatTTS.Twitch.Socket;
using TwitchChatTTS.Twitch.Socket.Messages;
namespace TwitchChatTTS.Chat.Messaging
{
public interface IChatMessageReader
{
Task Read(TwitchWebsocketClient sender, long broadcasterId, long? chatterId, string? chatterLogin, string? messageId, TwitchReplyInfo? reply, TwitchChatFragment[] fragments, int priority);
}
}

View File

@@ -5,24 +5,17 @@ namespace TwitchChatTTS.Chat.Observers
public class TTSPublisher : IObservable<TTSGroupedMessage> public class TTSPublisher : IObservable<TTSGroupedMessage>
{ {
private readonly HashSet<IObserver<TTSGroupedMessage>> _observers; private readonly HashSet<IObserver<TTSGroupedMessage>> _observers;
private readonly HashSet<TTSGroupedMessage> _messages;
public TTSPublisher() public TTSPublisher()
{ {
_observers = new(); _observers = new();
_messages = new();
} }
public IDisposable Subscribe(IObserver<TTSGroupedMessage> observer) public IDisposable Subscribe(IObserver<TTSGroupedMessage> observer)
{ {
if (_observers.Add(observer)) _observers.Add(observer);
{
foreach (var item in _messages)
observer.OnNext(item);
}
return new Unsubscriber<TTSGroupedMessage>(_observers, observer); return new Unsubscriber<TTSGroupedMessage>(_observers, observer);
} }
} }

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;
} }
@@ -29,8 +33,7 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
if (message.AnotherClient) if (message.AnotherClient)
{ {
if (client.LoggedIn) _logger.Warning($"Another client has connected to the same account via {(message.WebLogin ? "web login" : "application")}.");
_logger.Warning($"Another client has connected to the same account via {(message.WebLogin ? "web login" : "application")}.");
return; return;
} }
if (client.LoggedIn) if (client.LoggedIn)
@@ -39,24 +42,38 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
return; return;
} }
_user.HermesUserId = message.UserId; _user.Slave = message.Slave;
_user.OwnerId = message.OwnerId; _logger.Information(_user.Slave ? "This client is not responsible for reacting to chat messages." : "This client is responsible for reacting to chat messages.");
_user.DefaultTTSVoice = message.DefaultTTSVoice;
_user.VoicesAvailable = message.TTSVoicesAvailable;
_user.VoicesEnabled = new HashSet<string>(message.EnabledTTSVoices);
_user.TwitchConnection = message.Connections.FirstOrDefault(c => c.Default && c.Type == "twitch");
_user.NightbotConnection = message.Connections.FirstOrDefault(c => c.Default && c.Type == "nightbot");
var filters = message.WordFilters.Where(f => f.Search != null && f.Replace != null).ToArray(); _user.HermesUserId = message.UserId;
_user.HermesUsername = message.UserName;
_user.TwitchUsername = message.UserName;
_user.TwitchUserId = long.Parse(message.ProviderAccountId);
_user.OwnerId = message.OwnerId;
_user.StreamElementsOverlayKey = message.StreamElementsOverlayKey;
_user.DefaultTTSVoice = message.DefaultTTSVoice;
_user.VoicesAvailable = new ConcurrentDictionary<string, string>(message.TTSVoicesAvailable);
_user.VoicesEnabled = new HashSet<string>(message.EnabledTTSVoices);
_user.TwitchConnection = message.Connections.FirstOrDefault(c => c.Default && c.Type == "twitch") ?? message.Connections.FirstOrDefault(c => c.Type == "twitch");
_user.NightbotConnection = message.Connections.FirstOrDefault(c => c.Default && c.Type == "nightbot") ?? message.Connections.FirstOrDefault(c => c.Type == "nightbot");
if (_user.TwitchConnection != null)
{
_logger.Debug("Twitch connection: " + _user.TwitchConnection.Name + " / " + _user.TwitchConnection.AccessToken);
}
var filters = message.WordFilters.Where(f => f.Search != null && f.Replace != null).ToList();
foreach (var filter in filters) 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;
@@ -68,6 +85,7 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
await client.FetchEmotes(); await client.FetchEmotes();
await client.FetchRedemptions(); await client.FetchRedemptions();
await client.FetchPermissions(); await client.FetchPermissions();
await client.FetchPolicies();
if (_user.NightbotConnection != null) if (_user.NightbotConnection != null)
{ {
@@ -84,6 +102,8 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
_logger.Information("TTS is now ready."); _logger.Information("TTS is now ready.");
client.Ready = true; client.Ready = true;
_bus.Send(this, "tts_connected", _user);
} }
} }
} }

View File

@@ -1,374 +1,52 @@
using System.Collections.Concurrent;
using System.Text.Json;
using System.Text.RegularExpressions;
using CommonSocketLibrary.Abstract; using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common; using CommonSocketLibrary.Common;
using HermesSocketLibrary.Requests.Callbacks;
using HermesSocketLibrary.Requests.Messages;
using HermesSocketLibrary.Socket.Data; using HermesSocketLibrary.Socket.Data;
using Microsoft.Extensions.DependencyInjection;
using Serilog; using Serilog;
using TwitchChatTTS.Chat.Emotes; using TwitchChatTTS.Hermes.Socket.Requests;
using TwitchChatTTS.Chat.Groups;
using TwitchChatTTS.Chat.Groups.Permissions;
using TwitchChatTTS.Twitch.Redemptions;
namespace TwitchChatTTS.Hermes.Socket.Handlers namespace TwitchChatTTS.Hermes.Socket.Handlers
{ {
public class RequestAckHandler : IWebSocketHandler public class RequestAckHandler : IWebSocketHandler
{ {
private User _user; private readonly RequestAckManager _manager;
private readonly ICallbackManager<HermesRequestData> _callbackManager;
private readonly TwitchApiClient _twitch;
private readonly NightbotApiClient _nightbot;
private readonly IServiceProvider _serviceProvider;
private readonly JsonSerializerOptions _options;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly object _voicesAvailableLock = new object();
public int OperationCode { get; } = 4; public int OperationCode { get; } = 4;
public RequestAckHandler( public RequestAckHandler(
ICallbackManager<HermesRequestData> callbackManager, RequestAckManager manager,
TwitchApiClient twitch,
NightbotApiClient nightbot,
IServiceProvider serviceProvider,
User user,
JsonSerializerOptions options,
ILogger logger ILogger logger
) )
{ {
_callbackManager = callbackManager; _manager = manager;
_twitch = twitch;
_nightbot = nightbot;
_serviceProvider = serviceProvider;
_user = user;
_options = options;
_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;
}
HermesRequestData? hermesRequestData = null;
if (!string.IsNullOrEmpty(message.Request.RequestId))
{
hermesRequestData = _callbackManager.Take(message.Request.RequestId);
if (hermesRequestData == null)
_logger.Warning($"Could not find callback for request [request id: {message.Request.RequestId}][type: {message.Request.Type}]");
else if (hermesRequestData.Data == null)
hermesRequestData.Data = new Dictionary<string, object>();
} }
_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>())}]");
if (message.Request.Type == "get_tts_voices") var json = message.Data?.ToString();
if (message.Request.Type == null)
{ {
var voices = JsonSerializer.Deserialize<IEnumerable<VoiceDetails>>(message.Data.ToString(), _options); _logger.Warning("Request type is null. Unknown what acknowlegement this is for.");
if (voices == null) return Task.CompletedTask;
return;
lock (_voicesAvailableLock)
{
_user.VoicesAvailable = voices.ToDictionary(e => e.Id, e => e.Name);
}
_logger.Information("Updated all available voices for TTS.");
} }
else if (message.Request.Type == "create_tts_user") if (!string.IsNullOrWhiteSpace(message.Error))
{ {
if (!long.TryParse(message.Request.Data["chatter"].ToString(), out long chatterId)) _logger.Warning("An error occured while processing the request on the server: " + message.Error);
{ return Task.CompletedTask;
_logger.Warning($"Failed to parse chatter id [chatter id: {message.Request.Data["chatter"]}]");
return;
}
string userId = message.Request.Data["user"].ToString();
string voiceId = message.Request.Data["voice"].ToString();
_user.VoicesSelected.Add(chatterId, voiceId);
_logger.Information($"Added new TTS voice [voice: {voiceId}] for user [user id: {userId}]");
}
else if (message.Request.Type == "update_tts_user")
{
if (!long.TryParse(message.Request.Data["chatter"].ToString(), out long chatterId))
{
_logger.Warning($"Failed to parse chatter id [chatter id: {message.Request.Data["chatter"]}]");
return;
}
string userId = message.Request.Data["user"].ToString();
string voiceId = message.Request.Data["voice"].ToString();
_user.VoicesSelected[chatterId] = voiceId;
_logger.Information($"Updated TTS voice [voice: {voiceId}] for user [user id: {userId}]");
}
else if (message.Request.Type == "create_tts_voice")
{
string? voice = message.Request.Data["voice"].ToString();
string? voiceId = message.Data.ToString();
if (voice == null || voiceId == null)
return;
lock (_voicesAvailableLock)
{
var list = _user.VoicesAvailable.ToDictionary(k => k.Key, v => v.Value);
list.Add(voiceId, voice);
_user.VoicesAvailable = list;
}
_logger.Information($"Created new tts voice [voice: {voice}][id: {voiceId}].");
}
else if (message.Request.Type == "delete_tts_voice")
{
var voice = message.Request.Data["voice"].ToString();
if (!_user.VoicesAvailable.TryGetValue(voice, out string? voiceName) || voiceName == null)
return;
lock (_voicesAvailableLock)
{
var dict = _user.VoicesAvailable.ToDictionary(k => k.Key, v => v.Value);
dict.Remove(voice);
_user.VoicesAvailable.Remove(voice);
}
_logger.Information($"Deleted a voice [voice: {voiceName}]");
}
else if (message.Request.Type == "update_tts_voice")
{
string voiceId = message.Request.Data["idd"].ToString();
string voice = message.Request.Data["voice"].ToString();
if (!_user.VoicesAvailable.TryGetValue(voiceId, out string? voiceName) || voiceName == null)
return;
_user.VoicesAvailable[voiceId] = voice;
_logger.Information($"Updated TTS voice [voice: {voice}][id: {voiceId}]");
}
else if (message.Request.Type == "get_connections")
{
var connections = JsonSerializer.Deserialize<IEnumerable<Connection>>(message.Data?.ToString(), _options);
if (connections == null)
{
_logger.Error("Null value was given when attempting to fetch connections.");
_logger.Debug(message.Data?.ToString());
return;
}
_user.TwitchConnection = connections.FirstOrDefault(c => c.Type == "twitch" && c.Default);
_user.NightbotConnection = connections.FirstOrDefault(c => c.Type == "nightbot" && c.Default);
if (_user.TwitchConnection != null)
_twitch.Initialize(_user.TwitchConnection.ClientId, _user.TwitchConnection.AccessToken);
if (_user.NightbotConnection != null)
_nightbot.Initialize(_user.NightbotConnection.ClientId, _user.NightbotConnection.AccessToken);
_logger.Information($"Fetched connections from TTS account [count: {connections.Count()}][twitch: {_user.TwitchConnection != null}][nightbot: {_user.NightbotConnection != null}]");
}
else if (message.Request.Type == "get_tts_users")
{
var users = JsonSerializer.Deserialize<IDictionary<long, string>>(message.Data.ToString(), _options);
if (users == null)
return;
var temp = new ConcurrentDictionary<long, string>();
foreach (var entry in users)
temp.TryAdd(entry.Key, entry.Value);
_user.VoicesSelected = temp;
_logger.Information($"Updated {temp.Count()} chatters' selected voice.");
}
else if (message.Request.Type == "get_chatter_ids")
{
var chatters = JsonSerializer.Deserialize<IEnumerable<long>>(message.Data.ToString(), _options);
if (chatters == null)
return;
_user.Chatters = [.. chatters];
_logger.Information($"Fetched {chatters.Count()} chatters' id.");
}
else if (message.Request.Type == "get_emotes")
{
var emotes = JsonSerializer.Deserialize<IEnumerable<EmoteInfo>>(message.Data.ToString(), _options);
if (emotes == null)
return;
var emoteDb = _serviceProvider.GetRequiredService<IEmoteDatabase>();
var count = 0;
var duplicateNames = 0;
foreach (var emote in emotes)
{
if (emoteDb.Get(emote.Name) == null)
{
emoteDb.Add(emote.Name, emote.Id);
count++;
}
else
duplicateNames++;
}
_logger.Information($"Fetched {count} emotes from various sources.");
if (duplicateNames > 0)
_logger.Warning($"Found {duplicateNames} emotes with duplicate names.");
}
else if (message.Request.Type == "get_enabled_tts_voices")
{
var enabledTTSVoices = JsonSerializer.Deserialize<IEnumerable<string>>(message.Data.ToString(), _options);
if (enabledTTSVoices == null)
{
_logger.Error("Failed to load enabled tts voices.");
return;
}
if (_user.VoicesEnabled == null)
_user.VoicesEnabled = enabledTTSVoices.ToHashSet();
else
_user.VoicesEnabled.Clear();
foreach (var voice in enabledTTSVoices)
_user.VoicesEnabled.Add(voice);
_logger.Information($"TTS voices [count: {_user.VoicesEnabled.Count}] have been enabled.");
}
else if (message.Request.Type == "get_permissions")
{
var groupInfo = JsonSerializer.Deserialize<GroupInfo>(message.Data.ToString(), _options);
if (groupInfo == null)
{
_logger.Error("Failed to load groups & permissions.");
return;
}
var chatterGroupManager = _serviceProvider.GetRequiredService<IChatterGroupManager>();
var permissionManager = _serviceProvider.GetRequiredService<IGroupPermissionManager>();
permissionManager.Clear();
chatterGroupManager.Clear();
var groupsById = groupInfo.Groups.ToDictionary(g => g.Id, g => g);
foreach (var group in groupInfo.Groups)
chatterGroupManager.Add(group);
foreach (var permission in groupInfo.GroupPermissions)
{
_logger.Debug($"Adding group permission [permission id: {permission.Id}][group id: {permission.GroupId}][path: {permission.Path}][allow: {permission.Allow?.ToString() ?? "null"}]");
if (!groupsById.TryGetValue(permission.GroupId, out var group))
{
_logger.Warning($"Failed to find group by id [permission id: {permission.Id}][group id: {permission.GroupId}][path: {permission.Path}]");
continue;
}
var path = $"{group.Name}.{permission.Path}";
permissionManager.Set(path, permission.Allow);
_logger.Debug($"Added group permission [id: {permission.Id}][group id: {permission.GroupId}][path: {permission.Path}]");
}
_logger.Information($"Groups [count: {groupInfo.Groups.Count()}] & Permissions [count: {groupInfo.GroupPermissions.Count()}] have been loaded.");
foreach (var chatter in groupInfo.GroupChatters)
if (groupsById.TryGetValue(chatter.GroupId, out var group))
chatterGroupManager.Add(chatter.ChatterId, group.Name);
_logger.Information($"Users in each group [count: {groupInfo.GroupChatters.Count()}] have been loaded.");
}
else if (message.Request.Type == "get_tts_word_filters")
{
var wordFilters = JsonSerializer.Deserialize<IEnumerable<TTSWordFilter>>(message.Data.ToString(), _options);
if (wordFilters == null)
{
_logger.Error("Failed to load word filters.");
return;
}
var filters = wordFilters.Where(f => f.Search != null && f.Replace != null).ToArray();
foreach (var filter in filters)
{
try
{
var re = new Regex(filter.Search!, RegexOptions.Compiled);
re.Match(string.Empty);
filter.Regex = re;
}
catch (Exception) { }
}
_user.RegexFilters = filters;
_logger.Information($"TTS word filters [count: {_user.RegexFilters.Count()}] have been refreshed.");
}
else if (message.Request.Type == "update_tts_voice_state")
{
string voiceId = message.Request.Data?["voice"].ToString()!;
bool state = message.Request.Data?["state"].ToString()!.ToLower() == "true";
if (!_user.VoicesAvailable.TryGetValue(voiceId, out string? voiceName) || voiceName == null)
{
_logger.Warning($"Failed to find voice by id [id: {voiceId}]");
return;
}
if (state)
_user.VoicesEnabled.Add(voiceId);
else
_user.VoicesEnabled.Remove(voiceId);
_logger.Information($"Updated voice state [voice: {voiceName}][new state: {(state ? "enabled" : "disabled")}]");
}
else if (message.Request.Type == "get_redemptions")
{
IEnumerable<Redemption>? redemptions = JsonSerializer.Deserialize<IEnumerable<Redemption>>(message.Data!.ToString()!, _options);
if (redemptions != null)
{
_logger.Information($"Redemptions [count: {redemptions.Count()}] loaded.");
if (hermesRequestData != null)
hermesRequestData.Data!.Add("redemptions", redemptions);
}
else
_logger.Information(message.Data.GetType().ToString());
}
else if (message.Request.Type == "get_redeemable_actions")
{
IEnumerable<RedeemableAction>? actions = JsonSerializer.Deserialize<IEnumerable<RedeemableAction>>(message.Data!.ToString()!, _options);
if (actions == null)
{
_logger.Warning("Failed to read the redeemable actions for redemptions.");
return;
}
if (hermesRequestData?.Data == null || hermesRequestData.Data["redemptions"] is not IEnumerable<Redemption> redemptions)
{
_logger.Warning("Failed to read the redemptions while updating redemption actions.");
return;
}
_logger.Information($"Redeemable actions [count: {actions.Count()}] loaded.");
var redemptionManager = _serviceProvider.GetRequiredService<IRedemptionManager>();
redemptionManager.Initialize(redemptions, actions.ToDictionary(a => a.Name, a => a));
}
else if (message.Request.Type == "get_default_tts_voice")
{
string? defaultVoice = message.Data?.ToString();
if (defaultVoice != null)
{
_user.DefaultTTSVoice = defaultVoice;
_logger.Information($"Default TTS voice was changed to '{defaultVoice}'.");
}
}
else if (message.Request.Type == "update_default_tts_voice")
{
if (message.Request.Data?.TryGetValue("voice", out object? voice) == true && voice is string v)
{
_user.DefaultTTSVoice = v;
_logger.Information($"Default TTS voice was changed to '{v}'.");
}
else
_logger.Warning("Failed to update default TTS voice via request.");
}
else
{
_logger.Warning($"Found unknown request type when acknowledging [type: {message.Request.Type}]");
}
if (hermesRequestData != null)
{
_logger.Debug($"Callback was found for request [request id: {message.Request.RequestId}][type: {message.Request.Type}]");
hermesRequestData.Callback?.Invoke(hermesRequestData.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,29 +62,54 @@ 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.EnterReadLock();
try
{ {
if (Connected) if (Connected)
return; return;
} }
finally
{
_rwls.ExitReadLock();
}
_logger.Debug($"Attempting to connect to {URL}"); _logger.Debug($"Attempting to connect to {URL}");
await ConnectAsync(URL); await ConnectAsync(URL);
} }
private async Task Disconnect() private async Task Disconnect()
{ {
lock (_lock) _rwls.EnterReadLock();
try
{ {
if (!Connected) if (!Connected)
return; return;
} }
finally
{
_rwls.ExitReadLock();
}
await DisconnectAsync(new SocketDisconnectionEventArgs("Normal disconnection", "Disconnection was executed")); await DisconnectAsync(new SocketDisconnectionEventArgs("Normal disconnection", "Disconnection was executed"));
} }
@@ -193,16 +222,10 @@ namespace TwitchChatTTS.Hermes.Socket
private async Task FetchRedeemableActions(IEnumerable<Redemption> redemptions) private async Task FetchRedeemableActions(IEnumerable<Redemption> redemptions)
{ {
var requestId = _callbackManager.GenerateKeyForCallback(new HermesRequestData()
{
Data = new Dictionary<string, object>() { { "redemptions", redemptions } }
});
await Send(3, new RequestMessage() await Send(3, new RequestMessage()
{ {
RequestId = requestId,
Type = "get_redeemable_actions", Type = "get_redeemable_actions",
Data = null Data = new Dictionary<string, object>() { { "redemptions", redemptions } }
}); });
} }
@@ -215,6 +238,15 @@ namespace TwitchChatTTS.Hermes.Socket
}); });
} }
public async Task FetchPolicies()
{
await Send(3, new RequestMessage()
{
Type = "get_policies",
Data = null
});
}
public async Task FetchConnections() public async Task FetchConnections()
{ {
await Send(3, new RequestMessage() await Send(3, new RequestMessage()
@@ -226,17 +258,15 @@ namespace TwitchChatTTS.Hermes.Socket
public void Initialize() public void Initialize()
{ {
_logger.Information("Initializing Hermes websocket client."); _logger.Information("Initializing Tom to Speech websocket client.");
OnConnected += async (sender, e) => OnConnected += async (sender, e) =>
{ {
lock (_lock) if (Connected)
{ return;
if (Connected) Connected = true;
return;
Connected = true; _logger.Information("Tom to Speech websocket client connected.");
}
_logger.Information("Hermes websocket client connected.");
_heartbeatTimer.Enabled = true; _heartbeatTimer.Enabled = true;
LastHeartbeatReceived = DateTime.UtcNow; LastHeartbeatReceived = DateTime.UtcNow;
@@ -246,21 +276,21 @@ namespace TwitchChatTTS.Hermes.Socket
ApiKey = _configuration.Hermes!.Token!, ApiKey = _configuration.Hermes!.Token!,
MajorVersion = TTS.MAJOR_VERSION, MajorVersion = TTS.MAJOR_VERSION,
MinorVersion = TTS.MINOR_VERSION, MinorVersion = TTS.MINOR_VERSION,
PatchVersion = TTS.PATCH_VERSION,
}); });
}; };
OnDisconnected += async (sender, e) => OnDisconnected += async (sender, e) =>
{ {
lock (_lock) if (!Connected)
{ return;
if (!Connected)
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);
@@ -375,7 +405,7 @@ namespace TwitchChatTTS.Hermes.Socket
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.Error(ex, "Failed to send a heartbeat back to the Hermes websocket server."); _logger.Error(ex, "Failed to send a heartbeat back to the Tom to Speech websocket server.");
} }
} }
else if (signalTime - LastHeartbeatReceived > TimeSpan.FromSeconds(120)) else if (signalTime - LastHeartbeatReceived > TimeSpan.FromSeconds(120))
@@ -386,7 +416,7 @@ namespace TwitchChatTTS.Hermes.Socket
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.Error(ex, "Failed to disconnect from Hermes websocket server."); _logger.Error(ex, "Failed to disconnect from Tom to Speech websocket server.");
Ready = false; Ready = false;
LoggedIn = false; LoggedIn = false;
Connected = false; Connected = false;
@@ -400,10 +430,18 @@ namespace TwitchChatTTS.Hermes.Socket
public new async Task Send<T>(int opcode, T message) public new async Task Send<T>(int opcode, T message)
{ {
if (!Connected) _rwls.EnterReadLock();
try
{ {
_logger.Warning("Hermes websocket client is not connected. Not sending a message."); if (!Connected)
return; {
_logger.Warning("Tom to Speech websocket client is not connected. Not sending a message.");
return;
}
}
finally
{
_rwls.ExitReadLock();
} }
await base.Send(opcode, message); await base.Send(opcode, message);

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,51 @@
using System.Text.Json;
using HermesSocketServer.Messages;
using Serilog;
using TwitchChatTTS.Chat.Commands.Limits;
using TwitchChatTTS.Chat.Groups;
namespace TwitchChatTTS.Hermes.Socket.Requests
{
public class CreatePolicyAck : IRequestAck
{
public string Name => "create_policy";
private readonly IChatterGroupManager _groups;
private readonly IUsagePolicy<long> _policies;
private readonly JsonSerializerOptions _options;
private readonly ILogger _logger;
public CreatePolicyAck(IChatterGroupManager groups, IUsagePolicy<long> policies, JsonSerializerOptions options, ILogger logger)
{
_groups = groups;
_policies = policies;
_options = options;
_logger = logger;
}
public void Acknowledge(string requestId, string? json, IDictionary<string, object>? requestData)
{
if (string.IsNullOrWhiteSpace(json)) {
_logger.Warning($"Policy JSON data is null.");
return;
}
var policy = JsonSerializer.Deserialize<Policy>(json, _options);
if (policy == null)
{
_logger.Warning($"Policy data is null.");
return;
}
var group = _groups.Get(policy.GroupId.ToString());
if (group == null)
{
_logger.Warning($"Policy's group id not found [group id: {policy.GroupId}][policy id: {policy.Id}]");
return;
}
_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));
_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

@@ -0,0 +1,52 @@
using Serilog;
namespace TwitchChatTTS.Hermes.Socket.Requests
{
public class CreateTTSUserAck : IRequestAck
{
public string Name => "create_tts_user";
private readonly User _user;
private readonly ILogger _logger;
public CreateTTSUserAck(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;
}
if (!long.TryParse(requestData["chatter"].ToString(), out long chatterId))
{
_logger.Warning($"Failed to parse chatter id [chatter id: {requestData["chatter"]}]");
return;
}
if (chatterId <= 0)
{
_logger.Warning($"Chatter Id is invalid [chatter id: {chatterId}]");
return;
}
var voiceId = requestData["voice"].ToString();
if (string.IsNullOrEmpty(voiceId))
{
_logger.Warning("Voice Id is invalid.");
return;
}
if (!_user.VoicesAvailable.TryGetValue(voiceId, out var voiceName))
{
_logger.Warning($"Voice Id does not exist [voice id: {voiceId}]");
return;
}
_user.VoicesSelected.Add(chatterId, voiceId);
_logger.Information($"Created a new TTS user [chatter id: {_user.TwitchUserId}][chatter id: {chatterId}][voice id: {voiceId}][voice name: {voiceName}].");
}
}
}

View File

@@ -0,0 +1,47 @@
using Serilog;
namespace TwitchChatTTS.Hermes.Socket.Requests
{
public class CreateTTSVoiceAck : IRequestAck
{
public string Name => "create_tts_voice";
private readonly User _user;
private readonly ILogger _logger;
public CreateTTSVoiceAck(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 voice = requestData["voice"].ToString()!;
var voiceId = json;
if (string.IsNullOrEmpty(voice))
{
_logger.Warning("Voice name is invalid.");
return;
}
if (string.IsNullOrEmpty(voiceId))
{
_logger.Warning("Voice Id is invalid.");
return;
}
if (_user.VoicesAvailable.TryGetValue(voiceId, out var voiceName))
{
_logger.Warning($"Voice Id already exists [voice id: {voiceId}][voice name: {voiceName}]");
return;
}
_user.VoicesAvailable.Add(voiceId, voice);
_logger.Information($"Created a new tts voice [voice: {voice}][id: {voiceId}]");
}
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,49 @@
using Serilog;
using TwitchChatTTS.Chat.Commands.Limits;
using TwitchChatTTS.Chat.Groups;
namespace TwitchChatTTS.Hermes.Socket.Requests
{
public class DeletePolicyAck : IRequestAck
{
public string Name => "delete_policy";
private readonly IChatterGroupManager _groups;
private readonly IUsagePolicy<long> _policies;
private readonly ILogger _logger;
public DeletePolicyAck(IChatterGroupManager groups, IUsagePolicy<long> policies, ILogger logger)
{
_groups = groups;
_policies = policies;
_logger = logger;
}
public void Acknowledge(string requestId, string? json, IDictionary<string, object>? requestData)
{
if (json == null)
{
_logger.Warning("Policy JSON data is null.");
return;
}
var data = json.Split('/');
if (data.Length != 2)
{
_logger.Error("Deleting a policy failed: data received is invalid.");
return;
}
var groupId = data[0];
var path = data[1];
var group = _groups.Get(groupId);
if (group == null)
{
_logger.Warning($"Deleting a policy failed: group id does not exist [group id: {groupId}][path: {path}]");
return;
}
_policies.Remove(group.Name, path);
_logger.Information($"Policy has been deleted [group id: {groupId}][path: {path}]");
}
}
}

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

@@ -0,0 +1,41 @@
using Serilog;
namespace TwitchChatTTS.Hermes.Socket.Requests
{
public class DeleteTTSVoiceAck : IRequestAck
{
public string Name => "delete_tts_voice";
private readonly User _user;
private readonly ILogger _logger;
public DeleteTTSVoiceAck(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 voice = requestData["voice"].ToString();
if (string.IsNullOrEmpty(voice))
{
_logger.Warning($"Voice Id is invalid [voice id: {voice}]");
return;
}
if (!_user.VoicesAvailable.TryGetValue(voice, out string? voiceName))
{
_logger.Warning($"Voice Id does not exist [voice id: {voice}]");
return;
}
_user.VoicesAvailable.Remove(voice);
_logger.Information($"Deleted a voice [voice id: {voice}][voice name: {voiceName}]");
}
}
}

View File

@@ -0,0 +1,44 @@
using System.Text.Json;
using Serilog;
namespace TwitchChatTTS.Hermes.Socket.Requests
{
public class GetChatterIdsAck : IRequestAck
{
public string Name => "get_chatter_ids";
private readonly User _user;
private readonly JsonSerializerOptions _options;
private readonly ILogger _logger;
public GetChatterIdsAck(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("Chatters JSON is null.");
return;
}
var chatters = JsonSerializer.Deserialize<IEnumerable<long>>(json, _options);
if (chatters == null)
{
_logger.Warning("Chatters is null.");
return;
}
if (!chatters.Any())
{
_logger.Warning("Chatters is empty.");
return;
}
_user.Chatters = [.. chatters];
_logger.Information($"Fetched chatters [count: {chatters.Count()}]");
}
}
}

View File

@@ -0,0 +1,58 @@
using System.Text.Json;
using HermesSocketLibrary.Socket.Data;
using Serilog;
namespace TwitchChatTTS.Hermes.Socket.Requests
{
public class GetConnectionsAck : IRequestAck
{
public string Name => "get_connections";
private readonly TwitchApiClient _twitch;
private readonly NightbotApiClient _nightbot;
private readonly User _user;
private readonly JsonSerializerOptions _options;
private readonly ILogger _logger;
public GetConnectionsAck
(
TwitchApiClient twitch,
NightbotApiClient nightbot,
User user,
JsonSerializerOptions options,
ILogger logger
)
{
_twitch = twitch;
_nightbot = nightbot;
_user = user;
_options = options;
_logger = logger;
}
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);
if (connections == null)
{
_logger.Error("Null value was given when attempting to fetch connections.");
return;
}
_user.TwitchConnection = connections.FirstOrDefault(c => c.Type == "twitch" && c.Default);
_user.NightbotConnection = connections.FirstOrDefault(c => c.Type == "nightbot" && c.Default);
_logger.Information($"Fetched connections from TTS account [count: {connections.Count()}][twitch: {_user.TwitchConnection != null}][nightbot: {_user.NightbotConnection != null}]");
if (_user.TwitchConnection != null)
_twitch.Initialize(_user.TwitchConnection.ClientId, _user.TwitchConnection.AccessToken);
if (_user.NightbotConnection != null)
_nightbot.Initialize(_user.NightbotConnection.ClientId, _user.NightbotConnection.AccessToken);
}
}
}

View File

@@ -0,0 +1,31 @@
using Serilog;
namespace TwitchChatTTS.Hermes.Socket.Requests
{
public class GetDefaultTTSVoiceAck : IRequestAck
{
public string Name => "get_default_tts_voice";
private readonly User _user;
private readonly ILogger _logger;
public GetDefaultTTSVoiceAck(User user, ILogger logger)
{
_user = user;
_logger = logger;
}
public void Acknowledge(string requestId, string? json, IDictionary<string, object>? requestData)
{
string? defaultVoice = json;
if (defaultVoice != null)
{
_user.DefaultTTSVoice = defaultVoice;
_logger.Information($"Default TTS voice was changed [voice: {defaultVoice}]");
}
else
{
_logger.Error($"Failed to load default TTS voice.");
}
}
}
}

View File

@@ -0,0 +1,54 @@
using System.Text.Json;
using HermesSocketLibrary.Requests.Messages;
using Serilog;
using TwitchChatTTS.Chat.Emotes;
namespace TwitchChatTTS.Hermes.Socket.Requests
{
public class GetEmotesAck : IRequestAck
{
public string Name => "get_emotes";
private readonly IEmoteDatabase _emotes;
private readonly JsonSerializerOptions _options;
private readonly ILogger _logger;
public GetEmotesAck(IEmoteDatabase emotes, JsonSerializerOptions options, ILogger logger)
{
_emotes = emotes;
_options = options;
_logger = logger;
}
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);
if (data == null)
{
_logger.Warning("Emotes is null.");
return;
}
var count = 0;
var duplicateNames = 0;
foreach (var emote in data)
{
if (_emotes.Get(emote.Name) == null)
{
_emotes.Add(emote.Name, emote.Id);
count++;
}
else
duplicateNames++;
}
_logger.Information($"Fetched emotes of various sources [count: {count}]");
if (duplicateNames > 0)
_logger.Warning($"Found {duplicateNames} emotes with duplicate names.");
}
}
}

View File

@@ -0,0 +1,47 @@
using System.Text.Json;
using Serilog;
namespace TwitchChatTTS.Hermes.Socket.Requests
{
public class GetEnabledTTSVoicesAck : IRequestAck
{
public string Name => "get_enabled_tts_voices";
private readonly User _user;
private readonly JsonSerializerOptions _options;
private readonly ILogger _logger;
public GetEnabledTTSVoicesAck(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 Voices JSON is null.");
return;
}
var enabledTTSVoices = JsonSerializer.Deserialize<IEnumerable<string>>(json, _options);
if (enabledTTSVoices == null)
{
_logger.Warning("Failed to load enabled tts voices.");
return;
}
if (_user.VoicesEnabled == null)
_user.VoicesEnabled = enabledTTSVoices.ToHashSet();
else
{
_user.VoicesEnabled.Clear();
foreach (var voice in enabledTTSVoices)
_user.VoicesEnabled.Add(voice);
}
_logger.Information($"TTS voices [count: {_user.VoicesEnabled.Count}] have been enabled.");
}
}
}

View File

@@ -0,0 +1,76 @@
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 GetPermissionsAck : IRequestAck
{
public string Name => "get_permissions";
private readonly IGroupPermissionManager _permissions;
private readonly IChatterGroupManager _groups;
private readonly JsonSerializerOptions _options;
private readonly ILogger _logger;
public GetPermissionsAck(
IGroupPermissionManager permissions,
IChatterGroupManager groups,
JsonSerializerOptions options,
ILogger logger)
{
_permissions = permissions;
_groups = groups;
_options = options;
_logger = logger;
}
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);
if (groupInfo == null)
{
_logger.Error("Failed to load groups & permissions: object is null.");
return;
}
_permissions.Clear();
_groups.Clear();
var groupsById = groupInfo.Groups.ToDictionary(g => g.Id, g => g);
foreach (var group in groupInfo.Groups)
{
_logger.Debug($"Adding group [group id: {group.Id}][name: {group.Name}][priority: {group.Priority}]");
_groups.Add(group);
}
foreach (var permission in groupInfo.GroupPermissions)
{
_logger.Debug($"Adding group permission [permission id: {permission.Id}][group id: {permission.GroupId}][path: {permission.Path}][allow: {permission.Allow?.ToString() ?? "null"}]");
if (!groupsById.TryGetValue(permission.GroupId.ToString(), out var group))
{
_logger.Warning($"Failed to find group by id [permission id: {permission.Id}][group id: {permission.GroupId}][path: {permission.Path}]");
continue;
}
var path = $"{group.Name}.{permission.Path}";
_permissions.Set(path, permission.Allow);
_logger.Debug($"Added group permission [id: {permission.Id}][group id: {permission.GroupId}][path: {permission.Path}]");
}
_logger.Information($"Groups [count: {groupInfo.Groups.Count()}] & Permissions [count: {groupInfo.GroupPermissions.Count()}] have been loaded.");
foreach (var chatter in groupInfo.GroupChatters)
if (groupsById.TryGetValue(chatter.GroupId, out var group))
_groups.Add(chatter.ChatterId, group.Id);
_logger.Information($"Users in each group [count: {groupInfo.GroupChatters.Count()}] have been loaded.");
}
}
}

View File

@@ -0,0 +1,58 @@
using System.Text.Json;
using HermesSocketServer.Messages;
using Serilog;
using TwitchChatTTS.Chat.Commands.Limits;
using TwitchChatTTS.Chat.Groups;
namespace TwitchChatTTS.Hermes.Socket.Requests
{
public class GetPoliciesAck : IRequestAck
{
public string Name => "get_policies";
private readonly IChatterGroupManager _groups;
private readonly IUsagePolicy<long> _policies;
private readonly JsonSerializerOptions _options;
private readonly ILogger _logger;
public GetPoliciesAck(
IChatterGroupManager groups,
IUsagePolicy<long> policies,
JsonSerializerOptions options,
ILogger logger)
{
_groups = groups;
_policies = policies;
_options = options;
_logger = logger;
}
public void Acknowledge(string requestId, string? json, IDictionary<string, object>? requestData)
{
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())
{
_logger.Error($"No policies have been found: object is null or empty.");
return;
}
foreach (var policy in policies)
{
var group = _groups.Get(policy.GroupId.ToString());
if (group == null)
{
_logger.Debug($"Policy's group id not found [group id: {policy.GroupId}][policy id: {policy.Id}]");
continue;
}
_logger.Debug($"Policy data loaded [policy id: {policy.Id}][path: {policy.Path}][group id: {policy.GroupId}][group name: {group.Name}]");
_policies.Set(group.Name, policy.Path, policy.Usage, TimeSpan.FromMilliseconds(policy.Span));
}
_logger.Information($"Policies have been loaded [count: {policies.Count()}]");
}
}
}

View File

@@ -0,0 +1,56 @@
using System.Text.Json;
using HermesSocketLibrary.Requests.Messages;
using Serilog;
using TwitchChatTTS.Bus;
using TwitchChatTTS.Bus.Data;
namespace TwitchChatTTS.Hermes.Socket.Requests
{
public class GetRedeemableActionsAck : IRequestAck
{
public string Name => "get_redeemable_actions";
private readonly ServiceBusCentral _bus;
private readonly JsonSerializerOptions _options;
private readonly ILogger _logger;
public GetRedeemableActionsAck(
ServiceBusCentral bus,
JsonSerializerOptions options,
ILogger logger)
{
_bus = bus;
_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;
}
IEnumerable<Redemption>? redemptions = JsonSerializer.Deserialize<IEnumerable<Redemption>>(requestData["redemptions"].ToString() ?? string.Empty, _options);
if (redemptions == null)
{
_logger.Warning($"Failed to read the redemptions while updating redemption actions [class type: {requestData["redemptions"].GetType().Name}]");
return;
}
IEnumerable<RedeemableAction>? actions = JsonSerializer.Deserialize<IEnumerable<RedeemableAction>>(json, _options);
if (actions == null)
{
_logger.Warning("Failed to read the redeemable actions for redemptions.");
return;
}
_logger.Information($"Redeemable actions loaded [count: {actions.Count()}]");
_bus.Send(this, "redemptions_initiation", new RedemptionInitiation()
{
Redemptions = redemptions,
Actions = actions.ToDictionary(a => a.Name, a => a)
});
}
}
}

View File

@@ -0,0 +1,52 @@
using System.Text.Json;
using HermesSocketLibrary.Requests.Callbacks;
using HermesSocketLibrary.Requests.Messages;
using Serilog;
using TwitchChatTTS.Hermes.Socket.Handlers;
namespace TwitchChatTTS.Hermes.Socket.Requests
{
public class GetRedemptionsAck : IRequestAck
{
public string Name => "get_redemptions";
private readonly ICallbackManager<HermesRequestData> _callbacks;
private readonly JsonSerializerOptions _options;
private readonly ILogger _logger;
public GetRedemptionsAck(
ICallbackManager<HermesRequestData> callbacks,
JsonSerializerOptions options,
ILogger logger)
{
_callbacks = callbacks;
_options = options;
_logger = logger;
}
public void Acknowledge(string requestId, string? json, IDictionary<string, object>? requestData)
{
HermesRequestData? hermesRequestData = null;
if (!string.IsNullOrEmpty(requestId))
{
hermesRequestData = _callbacks.Take(requestId);
if (hermesRequestData == null)
_logger.Warning($"Could not find callback for request [request id: {requestId}][type: {Name}]");
else if (hermesRequestData.Data == null)
hermesRequestData.Data = new Dictionary<string, object>();
}
IEnumerable<Redemption>? redemptions = JsonSerializer.Deserialize<IEnumerable<Redemption>>(json, _options);
if (redemptions != null)
{
_logger.Information($"Redemptions loaded [count: {redemptions.Count()}]");
if (hermesRequestData?.Data != null)
{
hermesRequestData.Data.Add("redemptions", redemptions);
_logger.Debug($"Callback was found for request [request id: {requestId}][type: {Name}]");
hermesRequestData.Callback?.Invoke(hermesRequestData.Data);
}
}
}
}
}

View File

@@ -0,0 +1,35 @@
using System.Collections.Concurrent;
using System.Text.Json;
using Serilog;
namespace TwitchChatTTS.Hermes.Socket.Requests
{
public class GetTTSUsersAck : IRequestAck
{
public string Name => "get_tts_users";
private readonly User _user;
private readonly JsonSerializerOptions _options;
private readonly ILogger _logger;
public GetTTSUsersAck(User user, JsonSerializerOptions options, ILogger logger)
{
_user = user;
_options = options;
_logger = logger;
}
public void Acknowledge(string requestId, string? json, IDictionary<string, object>? requestData)
{
var users = JsonSerializer.Deserialize<IDictionary<long, string>>(json, _options);
if (users == null)
return;
var temp = new ConcurrentDictionary<long, string>();
foreach (var entry in users)
temp.TryAdd(entry.Key, entry.Value);
_user.VoicesSelected = temp;
_logger.Information($"Updated chatters' selected voice [count: {temp.Count()}]");
}
}
}

View File

@@ -0,0 +1,40 @@
using System.Collections.Concurrent;
using System.Text.Json;
using HermesSocketLibrary.Requests.Messages;
using Serilog;
namespace TwitchChatTTS.Hermes.Socket.Requests
{
public class GetTTSVoicesAck : IRequestAck
{
public string Name => "get_tts_voices";
private readonly User _user;
private readonly JsonSerializerOptions _options;
private readonly ILogger _logger;
public GetTTSVoicesAck(User user, JsonSerializerOptions options, ILogger logger)
{
_user = user;
_options = options;
_logger = logger;
}
public void Acknowledge(string requestId, string? json, IDictionary<string, object>? requestData)
{
var voices = JsonSerializer.Deserialize<IEnumerable<TTSVoice>>(json, _options);
if (voices == null)
{
_logger.Warning("Voices received is null.");
return;
}
if (!voices.Any())
{
_logger.Warning("Voices received is empty.");
return;
}
_user.VoicesAvailable = new ConcurrentDictionary<string, string>(voices.ToDictionary(e => e.Id, e => e.Name));
_logger.Information($"Fetched all available voices for TTS [count: {_user.VoicesAvailable.Count}]");
}
}
}

View File

@@ -0,0 +1,49 @@
using System.Text.Json;
using System.Text.RegularExpressions;
using HermesSocketLibrary.Requests.Messages;
using Serilog;
namespace TwitchChatTTS.Hermes.Socket.Requests
{
public class GetTTSWordFiltersAck : IRequestAck
{
public string Name => "get_tts_word_filters";
private readonly User _user;
private readonly JsonSerializerOptions _options;
private readonly ILogger _logger;
public GetTTSWordFiltersAck(User user, JsonSerializerOptions options, ILogger logger)
{
_user = user;
_options = options;
_logger = logger;
}
public void Acknowledge(string requestId, string? json, IDictionary<string, object>? requestData)
{
var wordFilters = JsonSerializer.Deserialize<IEnumerable<TTSWordFilter>>(json, _options);
if (wordFilters == null)
{
_logger.Error("Failed to load word filters.");
return;
}
var filters = wordFilters.Where(f => f.Search != null && f.Replace != null).ToList();
foreach (var filter in filters)
{
try
{
var re = new Regex(filter.Search, ((RegexOptions)filter.Flag) | RegexOptions.Compiled);
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}]");
}
}
_user.RegexFilters = filters;
_logger.Information($"TTS word filters [count: {_user.RegexFilters.Count}] have been refreshed.");
}
}
}

View File

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

View File

@@ -0,0 +1,36 @@
using Serilog;
namespace TwitchChatTTS.Hermes.Socket.Requests
{
public class RequestAckManager
{
private readonly IDictionary<string, IRequestAck> _acknowledgements;
private readonly ILogger _logger;
public RequestAckManager(IEnumerable<IRequestAck> acks, ILogger logger)
{
_acknowledgements = acks.ToDictionary(a => a.Name, a => a);
_logger = logger;
}
public void Fulfill(string type, string requestId, string? data, IDictionary<string, object>? requestData)
{
if (!_acknowledgements.TryGetValue(type, out var ack))
{
_logger.Warning($"Found unknown request type when acknowledging [type: {type}]");
return;
}
_logger.Debug($"Request acknowledgement found [type: {type}][data: {data}]");
try
{
ack.Acknowledge(requestId, data, requestData);
_logger.Debug($"Request acknowledged without error [type: {type}][data: {data}]");
}
catch (Exception ex)
{
_logger.Error(ex, "Failed to fulfill a request ackowledgement.");
}
}
}
}

View File

@@ -0,0 +1,37 @@
using Serilog;
namespace TwitchChatTTS.Hermes.Socket.Requests
{
public class UpdateDefaultTTSVoiceAck : IRequestAck
{
public string Name => "update_default_tts_voice";
private readonly User _user;
private readonly ILogger _logger;
public UpdateDefaultTTSVoiceAck(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;
}
if (requestData.TryGetValue("voice", out object? voice) == true && voice is string v)
{
if (_user.VoicesEnabled.Contains(v))
{
_user.DefaultTTSVoice = v;
_logger.Information($"Default TTS voice was changed to '{v}'.");
return;
}
}
_logger.Warning("Failed to update default TTS voice via request.");
}
}
}

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

@@ -0,0 +1,51 @@
using System.Text.Json;
using HermesSocketServer.Messages;
using Serilog;
using TwitchChatTTS.Chat.Commands.Limits;
using TwitchChatTTS.Chat.Groups;
namespace TwitchChatTTS.Hermes.Socket.Requests
{
public class UpdatePolicyAck : IRequestAck
{
public string Name => "update_policy";
private readonly IChatterGroupManager _groups;
private readonly IUsagePolicy<long> _policies;
private readonly JsonSerializerOptions _options;
private readonly ILogger _logger;
public UpdatePolicyAck(IChatterGroupManager groups, IUsagePolicy<long> policies, JsonSerializerOptions options, ILogger logger)
{
_groups = groups;
_policies = policies;
_options = options;
_logger = logger;
}
public void Acknowledge(string requestId, string? json, IDictionary<string, object>? requestData)
{
if (json == null)
{
_logger.Warning($"Policy JSON data is null.");
return;
}
var policy = JsonSerializer.Deserialize<Policy>(json, _options);
if (policy == null)
{
_logger.Warning($"Policy data is null.");
return;
}
var group = _groups.Get(policy.GroupId.ToString());
if (group == null)
{
_logger.Warning($"Policy's group id not found [group id: {policy.GroupId}][policy id: {policy.Id}]");
return;
}
_logger.Debug($"Policy data loaded [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));
_logger.Information($"Policy has been updated [policy id: {policy.Id}]");
}
}
}

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

@@ -0,0 +1,52 @@
using Serilog;
namespace TwitchChatTTS.Hermes.Socket.Requests
{
public class UpdateTTSUserAck : IRequestAck
{
public string Name => "update_tts_user";
private readonly User _user;
private readonly ILogger _logger;
public UpdateTTSUserAck(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;
}
if (!long.TryParse(requestData["chatter"].ToString(), out long chatterId))
{
_logger.Warning($"Failed to parse chatter id [chatter id: {requestData["chatter"]}]");
return;
}
if (chatterId <= 0)
{
_logger.Warning("Chatter Id is invalid.");
return;
}
var voiceId = requestData["voice"].ToString();
if (string.IsNullOrEmpty(voiceId))
{
_logger.Warning("Voice Id is invalid.");
return;
}
if (!_user.VoicesAvailable.TryGetValue(voiceId, out var voiceName))
{
_logger.Warning("Voice Id does not exist.");
return;
}
_user.VoicesSelected[chatterId] = voiceId;
_logger.Information($"Updated a TTS user's voice [user id: {_user.TwitchUserId}][voice: {voiceId}][voice name: {voiceName}]");
}
}
}

View File

@@ -0,0 +1,47 @@
using Serilog;
namespace TwitchChatTTS.Hermes.Socket.Requests
{
public class UpdateTTSVoiceAck : IRequestAck
{
public string Name => "update_tts_voice";
private readonly User _user;
private readonly ILogger _logger;
public UpdateTTSVoiceAck(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 voice = requestData["voice"].ToString();
var voiceId = requestData["idd"].ToString();
if (string.IsNullOrEmpty(voice))
{
_logger.Warning("Voice name is invalid.");
return;
}
if (string.IsNullOrEmpty(voiceId))
{
_logger.Warning("Voice Id is invalid.");
return;
}
if (_user.VoicesAvailable.ContainsKey(voiceId))
{
_logger.Warning($"Voice Id already exists [voice id: {voiceId}]");
return;
}
_user.VoicesAvailable[voiceId] = voice;
_logger.Information($"Created a new tts voice [voice id: {voiceId}][voice name: {voice}]");
}
}
}

View File

@@ -0,0 +1,58 @@
using Serilog;
namespace TwitchChatTTS.Hermes.Socket.Requests
{
public class UpdateTTSVoiceStateAck : IRequestAck
{
public string Name => "update_tts_voice_state";
private readonly User _user;
private readonly ILogger _logger;
public UpdateTTSVoiceStateAck(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;
}
if (!long.TryParse(requestData["chatter"].ToString(), out long chatterId))
{
_logger.Warning($"Failed to parse chatter id [chatter id: {requestData["chatter"]}]");
return;
}
if (chatterId <= 0)
{
_logger.Warning("Chatter Id is invalid.");
return;
}
var userId = requestData["user"].ToString();
var voiceId = requestData["voice"].ToString();
if (string.IsNullOrEmpty(userId))
{
_logger.Warning("User Id is invalid.");
return;
}
if (string.IsNullOrEmpty(voiceId))
{
_logger.Warning("Voice Id is invalid.");
return;
}
if (!_user.VoicesAvailable.TryGetValue(voiceId, out var voiceName))
{
_logger.Warning("Voice Id does not exist.");
return;
}
_user.VoicesSelected[chatterId] = voiceId;
_logger.Information($"Updated a TTS user's voice [user id: {userId}][voice: {voiceId}][voice name: {voiceName}]");
}
}
}

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,12 +23,25 @@ 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; _logger.Information($"Fetched 7tv emotes [count: {details?.EmoteSet.EmoteCount ?? -1}]");
if (details?.EmoteSet == null)
{
_logger.Warning("Could not find 7tv emotes linked to your Twitch account.");
return null;
}
return details.EmoteSet;
} }
catch (JsonException e) catch (JsonException e)
{ {

View File

@@ -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()));
} }
} }
} }

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