Added groups & permissions. Fixed TTS user creation. Better connection handling. Fixed 7tv reconnection.

This commit is contained in:
Tom
2024-07-16 04:48:55 +00:00
parent 9fb966474f
commit e6b3819356
45 changed files with 947 additions and 567 deletions

View File

@ -3,9 +3,9 @@ using TwitchChatTTS;
using System.Text.Json;
using HermesSocketLibrary.Requests.Messages;
using TwitchChatTTS.Hermes;
using TwitchChatTTS.Twitch.Redemptions;
using TwitchChatTTS.Chat.Groups.Permissions;
using TwitchChatTTS.Chat.Groups;
using HermesSocketLibrary.Socket.Data;
public class HermesApiClient
{
@ -50,15 +50,6 @@ public class HermesApiClient
return token;
}
public async Task<IEnumerable<TTSUsernameFilter>> FetchTTSUsernameFilters()
{
var filters = await _web.GetJson<IEnumerable<TTSUsernameFilter>>($"https://{BASE_URL}/api/settings/tts/filter/users");
if (filters == null)
throw new Exception("Failed to fetch TTS username filters from Hermes.");
return filters;
}
public async Task<string> FetchTTSDefaultVoice()
{
var data = await _web.GetJson<string>($"https://{BASE_URL}/api/settings/tts/default");

View File

@ -19,7 +19,6 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
{
if (data is not HeartbeatMessage message || message == null)
return;
if (sender is not HermesSocketClient client)
return;
@ -28,11 +27,7 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
client.LastHeartbeatReceived = DateTime.UtcNow;
if (message.Respond)
await sender.Send(0, new HeartbeatMessage()
{
DateTime = DateTime.UtcNow,
Respond = false
});
await client.SendHeartbeat();
}
}
}

View File

@ -1,7 +1,6 @@
using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common;
using HermesSocketLibrary.Socket.Data;
using Microsoft.Extensions.DependencyInjection;
using Serilog;
namespace TwitchChatTTS.Hermes.Socket.Handlers
@ -22,18 +21,23 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
{
if (data is not LoginAckMessage message || message == null)
return;
if (sender is not HermesSocketClient client)
return;
if (message.AnotherClient)
if (message.AnotherClient && client.LoggedIn)
{
_logger.Warning("Another client has connected to the same account.");
return;
}
if (client.LoggedIn)
{
_logger.Warning("Attempted to log in again while still logged in.");
return;
}
client.UserId = message.UserId;
_user.HermesUserId = message.UserId;
_user.OwnerId = message.OwnerId;
client.LoggedIn = true;
_logger.Information($"Logged in as {_user.TwitchUsername} {(message.WebLogin ? "via web" : "via TTS app")}.");
await client.Send(3, new RequestMessage()
@ -48,6 +52,12 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
Data = new Dictionary<string, object>() { { "user", _user.HermesUserId } }
});
await client.Send(3, new RequestMessage()
{
Type = "get_default_tts_voice",
Data = null
});
await client.Send(3, new RequestMessage()
{
Type = "get_chatter_ids",
@ -59,6 +69,13 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
Type = "get_emotes",
Data = null
});
await client.GetRedemptions();
await Task.Delay(TimeSpan.FromSeconds(3));
_logger.Information("TTS is now ready.");
client.Ready = true;
}
}
}

View File

@ -2,17 +2,21 @@ using System.Collections.Concurrent;
using System.Text.Json;
using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common;
using HermesSocketLibrary.Requests.Callbacks;
using HermesSocketLibrary.Requests.Messages;
using HermesSocketLibrary.Socket.Data;
using Microsoft.Extensions.DependencyInjection;
using Serilog;
using TwitchChatTTS.Seven;
using TwitchChatTTS.Chat.Emotes;
using TwitchChatTTS.Twitch.Redemptions;
namespace TwitchChatTTS.Hermes.Socket.Handlers
{
public class RequestAckHandler : IWebSocketHandler
{
private User _user;
//private readonly RedemptionManager _redemptionManager;
private readonly ICallbackManager<HermesRequestData> _callbackManager;
private readonly IServiceProvider _serviceProvider;
private readonly JsonSerializerOptions _options;
private readonly ILogger _logger;
@ -21,9 +25,19 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
public int OperationCode { get; } = 4;
public RequestAckHandler(User user, IServiceProvider serviceProvider, JsonSerializerOptions options, ILogger logger)
public RequestAckHandler(
User user,
//RedemptionManager redemptionManager,
ICallbackManager<HermesRequestData> callbackManager,
IServiceProvider serviceProvider,
JsonSerializerOptions options,
ILogger logger
)
{
_user = user;
//_redemptionManager = redemptionManager;
_callbackManager = callbackManager;
_serviceProvider = serviceProvider;
_options = options;
_logger = logger;
@ -34,10 +48,22 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
if (data is not RequestAckMessage message || message == null)
return;
if (message.Request == null)
{
_logger.Warning("Received a Hermes request message without a proper request.");
return;
if (_user == null)
return;
}
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>())}]");
if (message.Request.Type == "get_tts_voices")
{
_logger.Verbose("Updating all available voices for TTS.");
@ -54,16 +80,16 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
else if (message.Request.Type == "create_tts_user")
{
_logger.Verbose("Adding new tts voice for user.");
if (!long.TryParse(message.Request.Data["user"].ToString(), out long chatterId))
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 voice = message.Request.Data["voice"].ToString();
string voiceId = message.Request.Data["voice"].ToString();
_user.VoicesSelected.Add(chatterId, voice);
_logger.Information($"Added new TTS voice [voice: {voice}] for user [user id: {userId}]");
_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")
{
@ -74,10 +100,10 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
return;
}
string userId = message.Request.Data["user"].ToString();
string voice = message.Request.Data["voice"].ToString();
string voiceId = message.Request.Data["voice"].ToString();
_user.VoicesSelected[chatterId] = voice;
_logger.Information($"Updated TTS voice [voice: {voice}] for user [user id: {userId}]");
_user.VoicesSelected[chatterId] = voiceId;
_logger.Information($"Updated TTS voice [voice: {voiceId}] for user [user id: {userId}]");
}
else if (message.Request.Type == "create_tts_voice")
{
@ -99,7 +125,7 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
{
_logger.Verbose("Deleting tts voice.");
var voice = message.Request.Data["voice"].ToString();
if (!_user.VoicesAvailable.TryGetValue(voice, out string voiceName) || voiceName == null)
if (!_user.VoicesAvailable.TryGetValue(voice, out string? voiceName) || voiceName == null)
return;
lock (_voicesAvailableLock)
@ -116,7 +142,7 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
string voiceId = message.Request.Data["idd"].ToString();
string voice = message.Request.Data["voice"].ToString();
if (!_user.VoicesAvailable.TryGetValue(voiceId, out string voiceName) || voiceName == null)
if (!_user.VoicesAvailable.TryGetValue(voiceId, out string? voiceName) || voiceName == null)
return;
_user.VoicesAvailable[voiceId] = voice;
@ -153,8 +179,9 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
if (emotes == null)
return;
var emoteDb = _serviceProvider.GetRequiredService<EmoteDatabase>();
var emoteDb = _serviceProvider.GetRequiredService<IEmoteDatabase>();
var count = 0;
var duplicateNames = 0;
foreach (var emote in emotes)
{
if (emoteDb.Get(emote.Name) == null)
@ -162,8 +189,12 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
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 == "update_tts_voice_state")
{
@ -171,7 +202,7 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
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)
if (!_user.VoicesAvailable.TryGetValue(voiceId, out string? voiceName) || voiceName == null)
{
_logger.Warning($"Failed to find voice by id [id: {voiceId}]");
return;
@ -183,6 +214,73 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
_user.VoicesEnabled.Remove(voiceId);
_logger.Information($"Updated voice state [voice: {voiceName}][new state: {(state ? "enabled" : "disabled")}]");
}
else if (message.Request.Type == "get_redemptions")
{
_logger.Verbose("Fetching all the 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")
{
_logger.Verbose("Fetching all the 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 IEnumerable<Redemption> redemptions))
{
_logger.Warning("Failed to read the redemptions while updating redemption actions.");
return;
}
_logger.Information($"Redeemable actions [count: {actions.Count()}] loaded.");
var redemptionManager = _serviceProvider.GetRequiredService<RedemptionManager>();
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);
}
}
}
public class HermesRequestData
{
public Action<IDictionary<string, object>?>? Callback { get; set; }
public IDictionary<string, object>? Data { get; set; }
}
}

View File

@ -1,26 +1,41 @@
using System.Net.WebSockets;
using System.Text.Json;
using System.Timers;
using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common;
using HermesSocketLibrary.Requests.Callbacks;
using HermesSocketLibrary.Requests.Messages;
using HermesSocketLibrary.Socket.Data;
using Microsoft.Extensions.DependencyInjection;
using Serilog;
using TwitchChatTTS.Hermes.Socket.Handlers;
namespace TwitchChatTTS.Hermes.Socket
{
public class HermesSocketClient : WebSocketClient
{
private Configuration _configuration;
public const string BASE_URL = "ws.tomtospeech.com";
private readonly User _user;
private readonly Configuration _configuration;
private readonly ICallbackManager<HermesRequestData> _callbackManager;
private string URL;
public DateTime LastHeartbeatReceived { get; set; }
public DateTime LastHeartbeatSent { get; set; }
public string? UserId { get; set; }
private System.Timers.Timer _heartbeatTimer;
private System.Timers.Timer _reconnectTimer;
public const string BASE_URL = "ws.tomtospeech.com";
public bool Connected { get; set; }
public bool LoggedIn { get; set; }
public bool Ready { get; set; }
public HermesSocketClient(
User user,
Configuration configuration,
ICallbackManager<HermesRequestData> callbackManager,
[FromKeyedServices("hermes")] HandlerManager<WebSocketClient, IWebSocketHandler> handlerManager,
[FromKeyedServices("hermes")] HandlerTypeManager<WebSocketClient, IWebSocketHandler> typeManager,
ILogger logger
@ -30,7 +45,9 @@ namespace TwitchChatTTS.Hermes.Socket
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
})
{
_user = user;
_configuration = configuration;
_callbackManager = callbackManager;
_heartbeatTimer = new System.Timers.Timer(TimeSpan.FromSeconds(15));
_heartbeatTimer.Elapsed += async (sender, e) => await HandleHeartbeat(e);
@ -39,11 +56,208 @@ namespace TwitchChatTTS.Hermes.Socket
_reconnectTimer.Elapsed += async (sender, e) => await Reconnect(e);
LastHeartbeatReceived = LastHeartbeatSent = DateTime.UtcNow;
URL = $"wss://{BASE_URL}";
}
protected override async Task OnConnection()
public async Task Connect()
{
_heartbeatTimer.Enabled = true;
if (Connected)
return;
_logger.Debug($"Attempting to connect to {URL}");
await ConnectAsync(URL);
}
private async Task Disconnect()
{
if (!Connected)
return;
await DisconnectAsync();
}
public async Task CreateTTSVoice(string voiceName)
{
await Send(3, new RequestMessage()
{
Type = "create_tts_voice",
Data = new Dictionary<string, object>() { { "voice", voiceName } }
});
}
public async Task CreateTTSUser(long chatterId, string voiceId)
{
await Send(3, new RequestMessage()
{
Type = "create_tts_user",
Data = new Dictionary<string, object>() { { "chatter", chatterId }, { "voice", voiceId } }
});
}
public async Task DeleteTTSVoice(string voiceId)
{
await Send(3, new RequestMessage()
{
Type = "delete_tts_voice",
Data = new Dictionary<string, object>() { { "voice", voiceId } }
});
}
public async Task GetRedemptions()
{
var requestId = _callbackManager.GenerateKeyForCallback(new HermesRequestData()
{
Callback = async (d) => await GetRedeemableActions(d["redemptions"] as IEnumerable<Redemption>),
Data = new Dictionary<string, object>()
});
await Send(3, new RequestMessage()
{
RequestId = requestId,
Type = "get_redemptions",
Data = null
});
}
public async Task GetRedeemableActions(IEnumerable<Redemption> redemptions)
{
var requestId = _callbackManager.GenerateKeyForCallback(new HermesRequestData()
{
Data = new Dictionary<string, object>() { { "redemptions", redemptions } }
});
await Send(3, new RequestMessage()
{
RequestId = requestId,
Type = "get_redeemable_actions",
Data = null
});
}
public void Initialize()
{
_logger.Information("Initializing Hermes websocket client.");
OnConnected += async (sender, e) =>
{
Connected = true;
_logger.Information("Hermes websocket client connected.");
_reconnectTimer.Enabled = false;
_heartbeatTimer.Enabled = true;
LastHeartbeatReceived = DateTime.UtcNow;
await Send(1, new HermesLoginMessage()
{
ApiKey = _configuration.Hermes!.Token!,
MajorVersion = TTS.MAJOR_VERSION,
MinorVersion = TTS.MINOR_VERSION,
});
};
OnDisconnected += (sender, e) =>
{
Connected = false;
LoggedIn = false;
Ready = false;
_logger.Warning("Hermes websocket client disconnected.");
_heartbeatTimer.Enabled = false;
_reconnectTimer.Enabled = true;
};
}
public async Task SendLoggingMessage(HermesLoggingLevel level, string message)
{
await Send(5, new LoggingMessage(message, level));
}
public async Task SendLoggingMessage(Exception exception, HermesLoggingLevel level, string message)
{
await Send(5, new LoggingMessage(exception, message, level));
}
public async Task SendEmoteUsage(string messageId, long chatterId, ICollection<string> emotes)
{
if (!LoggedIn)
{
_logger.Debug("Not logged in. Cannot sent EmoteUsage message.");
return;
}
await Send(8, new EmoteUsageMessage()
{
MessageId = messageId,
DateTime = DateTime.UtcNow,
BroadcasterId = _user.TwitchUserId,
ChatterId = chatterId,
Emotes = emotes
});
}
public async Task SendChatterDetails(long chatterId, string username)
{
if (!LoggedIn)
{
_logger.Debug("Not logged in. Cannot send Chatter message.");
return;
}
await Send(6, new ChatterMessage()
{
Id = chatterId,
Name = username
});
}
public async Task SendEmoteDetails(IDictionary<string, string> emotes)
{
if (!LoggedIn)
{
_logger.Debug("Not logged in. Cannot send EmoteDetails message.");
return;
}
await Send(7, new EmoteDetailsMessage()
{
Emotes = emotes
});
}
public async Task SendHeartbeat(bool respond = false, DateTime? date = null)
{
await Send(0, new HeartbeatMessage() { DateTime = date ?? DateTime.UtcNow, Respond = respond });
}
public async Task UpdateTTSUser(long chatterId, string voiceId)
{
if (!LoggedIn)
{
_logger.Debug("Not logged in. Cannot send UpdateTTSUser message.");
return;
}
await Send(3, new RequestMessage()
{
Type = "update_tts_user",
Data = new Dictionary<string, object>() { { "chatter", chatterId }, { "voice", voiceId } }
});
}
public async Task UpdateTTSVoiceState(string voiceId, bool state)
{
if (!LoggedIn)
{
_logger.Debug("Not logged in. Cannot send UpdateTTSVoiceState message.");
return;
}
await Send(3, new RequestMessage()
{
Type = "update_tts_voice_state",
Data = new Dictionary<string, object>() { { "voice", voiceId }, { "state", state } }
});
}
private async Task HandleHeartbeat(ElapsedEventArgs e)
@ -58,20 +272,22 @@ namespace TwitchChatTTS.Hermes.Socket
LastHeartbeatSent = DateTime.UtcNow;
try
{
await Send(0, new HeartbeatMessage() { DateTime = LastHeartbeatSent });
await SendHeartbeat(date: LastHeartbeatSent);
}
catch (Exception)
catch (Exception ex)
{
_logger.Error(ex, "Failed to send a heartbeat back to the Hermes websocket server.");
}
}
else if (signalTime - LastHeartbeatReceived > TimeSpan.FromSeconds(120))
{
try
{
await DisconnectAsync();
await Disconnect();
}
catch (Exception)
catch (Exception ex)
{
_logger.Error(ex, "Failed to disconnect from Hermes websocket server.");
}
UserId = null;
_heartbeatTimer.Enabled = false;
@ -84,32 +300,41 @@ namespace TwitchChatTTS.Hermes.Socket
private async Task Reconnect(ElapsedEventArgs e)
{
try
if (Connected)
{
await ConnectAsync($"wss://{HermesSocketClient.BASE_URL}");
Connected = true;
}
catch (Exception)
{
}
finally
{
if (Connected)
try
{
_logger.Information("Reconnected.");
_reconnectTimer.Enabled = false;
_heartbeatTimer.Enabled = true;
LastHeartbeatReceived = DateTime.UtcNow;
if (_configuration.Hermes?.Token != null)
await Send(1, new HermesLoginMessage()
{
ApiKey = _configuration.Hermes.Token,
MajorVersion = TTS.MAJOR_VERSION,
MinorVersion = TTS.MINOR_VERSION,
});
await Disconnect();
}
catch (Exception ex)
{
_logger.Error(ex, "Failed to disconnect from Hermes websocket server.");
}
}
try
{
await Connect();
}
catch (WebSocketException wse) when (wse.Message.Contains("502"))
{
_logger.Error("Hermes websocket server cannot be found.");
}
catch (Exception ex)
{
_logger.Error(ex, "Failed to reconnect to Hermes websocket server.");
}
}
public new async Task Send<T>(int opcode, T message)
{
if (!Connected)
{
_logger.Warning("Hermes websocket client is not connected. Not sending a message.");
return;
}
await base.Send(opcode, message);
}
}
}