Added hermes websocket support. Added chat command support. Added selectable voice command via websocket. Added websocket heartbeat management.

This commit is contained in:
Tom
2024-03-15 12:27:35 +00:00
parent b5cc6b5706
commit d4004d6230
53 changed files with 1227 additions and 461 deletions

View File

@ -4,18 +4,10 @@ using TwitchChatTTS.Hermes;
using System.Text.Json;
public class HermesClient {
private Account? account;
private WebClientWrap _web;
private Configuration Configuration { get; }
public string? Id { get => account?.Id; }
public string? Username { get => account?.Username; }
public HermesClient(Configuration configuration) {
Configuration = configuration;
if (string.IsNullOrWhiteSpace(Configuration.Hermes?.Token)) {
if (string.IsNullOrWhiteSpace(configuration.Hermes?.Token)) {
throw new Exception("Ensure you have written your API key in \".token\" file, in the same folder as this application.");
}
@ -23,54 +15,52 @@ public class HermesClient {
PropertyNameCaseInsensitive = false,
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
});
_web.AddHeader("x-api-key", Configuration.Hermes.Token);
_web.AddHeader("x-api-key", configuration.Hermes.Token);
}
public async Task FetchHermesAccountDetails() {
account = await _web.GetJson<Account>("https://hermes.goblincaves.com/api/account");
public async Task<Account> FetchHermesAccountDetails() {
var account = await _web.GetJson<Account>("https://hermes.goblincaves.com/api/account");
if (account == null || account.Id == null || account.Username == null)
throw new NullReferenceException("Invalid value found while fetching for hermes account data.");
return account;
}
public async Task<TwitchBotToken> FetchTwitchBotToken() {
var token = await _web.GetJson<TwitchBotToken>("https://hermes.goblincaves.com/api/token/bot");
if (token == null) {
if (token == null || token.ClientId == null || token.AccessToken == null || token.RefreshToken == null || token.ClientSecret == null)
throw new Exception("Failed to fetch Twitch API token from Hermes.");
}
return token;
}
public async Task<IEnumerable<TTSUsernameFilter>> FetchTTSUsernameFilters() {
var filters = await _web.GetJson<IEnumerable<TTSUsernameFilter>>("https://hermes.goblincaves.com/api/settings/tts/filter/users");
if (filters == null) {
if (filters == null)
throw new Exception("Failed to fetch TTS username filters from Hermes.");
}
return filters;
}
public async Task<string?> FetchTTSDefaultVoice() {
public async Task<string> FetchTTSDefaultVoice() {
var data = await _web.GetJson<TTSVoice>("https://hermes.goblincaves.com/api/settings/tts/default");
if (data == null) {
if (data == null)
throw new Exception("Failed to fetch TTS default voice from Hermes.");
}
return data.Label;
}
public async Task<IEnumerable<TTSVoice>> FetchTTSEnabledVoices() {
var voices = await _web.GetJson<IEnumerable<TTSVoice>>("https://hermes.goblincaves.com/api/settings/tts");
if (voices == null) {
if (voices == null)
throw new Exception("Failed to fetch TTS enabled voices from Hermes.");
}
return voices;
}
public async Task<IEnumerable<TTSWordFilter>> FetchTTSWordFilters() {
var filters = await _web.GetJson<IEnumerable<TTSWordFilter>>("https://hermes.goblincaves.com/api/settings/tts/filter/words");
if (filters == null) {
if (filters == null)
throw new Exception("Failed to fetch TTS word filters from Hermes.");
}
return filters;
}

View File

@ -0,0 +1,35 @@
using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common;
using HermesSocketLibrary.Socket.Data;
using Microsoft.Extensions.Logging;
namespace TwitchChatTTS.Hermes.Socket.Handlers
{
public class HeartbeatHandler : IWebSocketHandler
{
private ILogger _logger { get; }
public int OperationCode { get; set; } = 0;
public HeartbeatHandler(ILogger<HeartbeatHandler> logger) {
_logger = logger;
}
public async Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data message)
{
if (message is not HeartbeatMessage obj || obj == null)
return;
if (sender is not HermesSocketClient client) {
return;
}
_logger.LogTrace("Received heartbeat.");
client.LastHeartbeat = DateTime.UtcNow;
await sender.Send(0, new HeartbeatMessage() {
DateTime = DateTime.UtcNow
});
}
}
}

View File

@ -0,0 +1,34 @@
using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common;
using HermesSocketLibrary.Socket.Data;
using Microsoft.Extensions.Logging;
namespace TwitchChatTTS.Hermes.Socket.Handlers
{
public class LoginAckHandler : IWebSocketHandler
{
private ILogger _logger { get; }
public int OperationCode { get; set; } = 2;
public LoginAckHandler(ILogger<LoginAckHandler> logger) {
_logger = logger;
}
public async Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data message)
{
if (message is not LoginAckMessage obj || obj == null)
return;
if (sender is not HermesSocketClient client) {
return;
}
if (obj.AnotherClient) {
_logger.LogWarning("Another client has connected to the same account.");
} else {
client.UserId = obj.UserId;
_logger.LogInformation($"Logged in as {client.UserId}.");
}
}
}
}

View File

@ -0,0 +1,106 @@
using System.Collections.Concurrent;
using System.Text.Json;
using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common;
using HermesSocketLibrary.Requests.Messages;
using HermesSocketLibrary.Socket.Data;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace TwitchChatTTS.Hermes.Socket.Handlers
{
public class RequestAckHandler : IWebSocketHandler
{
private readonly IServiceProvider _serviceProvider;
private readonly JsonSerializerOptions _options;
private readonly ILogger _logger;
public int OperationCode { get; set; } = 4;
public RequestAckHandler(IServiceProvider serviceProvider, JsonSerializerOptions options, ILogger<RequestAckHandler> logger) {
_serviceProvider = serviceProvider;
_options = options;
_logger = logger;
}
public async Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data message)
{
if (message is not RequestAckMessage obj || obj == null)
return;
if (obj.Request == null)
return;
var context = _serviceProvider.GetRequiredService<User>();
if (context == null)
return;
if (obj.Request.Type == "get_tts_voices") {
_logger.LogDebug("Updating all available voices.");
var voices = JsonSerializer.Deserialize<IEnumerable<VoiceDetails>>(obj.Data.ToString(), _options);
if (voices == null)
return;
context.VoicesAvailable = voices.ToDictionary(e => e.Id, e => e.Name);
_logger.LogInformation("Updated all available voices.");
} else if (obj.Request.Type == "create_tts_user") {
_logger.LogDebug("Creating new tts voice.");
if (!long.TryParse(obj.Request.Data["@user"], out long userId))
return;
string broadcasterId = obj.Request.Data["@broadcaster"].ToString();
// TODO: validate broadcaster id.
string voice = obj.Request.Data["@voice"].ToString();
context.VoicesSelected.Add(userId, voice);
_logger.LogInformation("Created new tts user.");
} else if (obj.Request.Type == "update_tts_user") {
_logger.LogDebug("Updating user's voice");
if (!long.TryParse(obj.Request.Data["@user"], out long userId))
return;
string broadcasterId = obj.Request.Data["@broadcaster"].ToString();
string voice = obj.Request.Data["@voice"].ToString();
context.VoicesSelected[userId] = voice;
_logger.LogInformation($"Updated user's voice to {voice}.");
} else if (obj.Request.Type == "create_tts_voice") {
_logger.LogDebug("Creating new tts voice.");
string? voice = obj.Request.Data["@voice"];
string? voiceId = obj.Data.ToString();
if (voice == null || voiceId == null)
return;
context.VoicesAvailable.Add(voiceId, voice);
_logger.LogInformation($"Created new tts voice named {voice} (id: {voiceId}).");
} else if (obj.Request.Type == "delete_tts_voice") {
_logger.LogDebug("Deleting tts voice.");
var voice = obj.Request.Data["@voice"];
if (!context.VoicesAvailable.TryGetValue(voice, out string voiceName) || voiceName == null) {
return;
}
context.VoicesAvailable.Remove(voice);
_logger.LogInformation("Deleted a voice, named " + voiceName + ".");
} else if (obj.Request.Type == "update_tts_voice") {
_logger.LogDebug("Updating tts voice.");
string voiceId = obj.Request.Data["@idd"].ToString();
string voice = obj.Request.Data["@voice"].ToString();
if (!context.VoicesAvailable.TryGetValue(voiceId, out string voiceName) || voiceName == null) {
return;
}
context.VoicesAvailable[voiceId] = voice;
_logger.LogInformation("Update tts voice: " + voice);
} else if (obj.Request.Type == "get_tts_users") {
_logger.LogDebug("Attempting to update all chatters' selected voice.");
var users = JsonSerializer.Deserialize<IDictionary<long, string>>(obj.Data.ToString(), _options);
if (users == null)
return;
var temp = new ConcurrentDictionary<long, string>();
foreach (var entry in users)
temp.TryAdd(entry.Key, entry.Value);
context.VoicesSelected = temp;
_logger.LogInformation($"Fetched {temp.Count()} chatters' selected voice.");
}
}
}
}

View File

@ -0,0 +1,24 @@
using System.Text.Json;
using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace TwitchChatTTS.Hermes.Socket
{
public class HermesSocketClient : WebSocketClient {
public DateTime LastHeartbeat { get; set; }
public string? UserId { get; set; }
public HermesSocketClient(
ILogger<HermesSocketClient> logger,
[FromKeyedServices("hermes")] HandlerManager<WebSocketClient, IWebSocketHandler> handlerManager,
[FromKeyedServices("hermes")] HandlerTypeManager<WebSocketClient, IWebSocketHandler> typeManager
) : base(logger, handlerManager, typeManager, new JsonSerializerOptions() {
PropertyNameCaseInsensitive = false,
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
}) {
}
}
}

View File

@ -0,0 +1,36 @@
using CommonSocketLibrary.Common;
using CommonSocketLibrary.Socket.Manager;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace TwitchChatTTS.Hermes.Socket.Managers
{
public class HermesHandlerManager : WebSocketHandlerManager
{
public HermesHandlerManager(ILogger<HermesHandlerManager> logger, IServiceProvider provider) : base(logger) {
//Add(provider.GetRequiredService<HeartbeatHandler>());
try {
var basetype = typeof(IWebSocketHandler);
var assembly = GetType().Assembly;
var types = assembly.GetTypes().Where(t => t.IsClass && basetype.IsAssignableFrom(t) && t.AssemblyQualifiedName?.Contains(".Hermes.") == true);
foreach (var type in types) {
var key = "hermes-" + type.Name.Replace("Handlers", "Hand#lers")
.Replace("Handler", "")
.Replace("Hand#lers", "Handlers")
.ToLower();
var handler = provider.GetKeyedService<IWebSocketHandler>(key);
if (handler == null) {
logger.LogError("Failed to find hermes websocket handler: " + type.AssemblyQualifiedName);
continue;
}
Logger.LogDebug($"Linked type {type.AssemblyQualifiedName} to hermes websocket handlers.");
Add(handler);
}
} catch (Exception e) {
Logger.LogError(e, "Failed to load hermes websocket handler types.");
}
}
}
}

View File

@ -0,0 +1,32 @@
using System.Reflection;
using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common;
using CommonSocketLibrary.Socket.Manager;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace TwitchChatTTS.Hermes.Socket.Managers
{
public class HermesHandlerTypeManager : WebSocketHandlerTypeManager
{
public HermesHandlerTypeManager(
ILogger<HermesHandlerTypeManager> factory,
[FromKeyedServices("hermes")] HandlerManager<WebSocketClient, IWebSocketHandler> handlers
) : base(factory, handlers)
{
}
protected override Type? FetchMessageType(Type handlerType)
{
if (handlerType == null)
return null;
var name = handlerType.Namespace + "." + handlerType.Name;
name = name.Replace(".Handlers.", ".Data.")
.Replace("Handler", "Message")
.Replace("TwitchChatTTS.Hermes.", "HermesSocketLibrary.");
return Assembly.Load("HermesSocketLibrary").GetType(name);
}
}
}

View File

@ -1,5 +1,5 @@
public class TTSUsernameFilter {
public string? Username { get; set; }
public string? Tag { get; set; }
public string? UserId { get; set; }
public string Username { get; set; }
public string Tag { get; set; }
public string UserId { get; set; }
}

View File

@ -1,5 +1,5 @@
public class TTSVoice {
public string? Label { get; set; }
public string Label { get; set; }
public int Value { get; set; }
public string? Gender { get; set; }
public string? Language { get; set; }

View File

@ -1,8 +1,3 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace TwitchChatTTS.Hermes
{
public class TTSWordFilter