Using Serilog. Added partial OBS batch request support. Added update checking. Added more commands. Added enabled/disabled TTS voices. And more.

This commit is contained in:
Tom 2024-06-17 00:19:31 +00:00
parent d4004d6230
commit 706cd06930
67 changed files with 1933 additions and 925 deletions

View File

@ -3,94 +3,99 @@ using TwitchLib.Client.Events;
using TwitchChatTTS.OBS.Socket; using TwitchChatTTS.OBS.Socket;
using CommonSocketLibrary.Abstract; using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common; using CommonSocketLibrary.Common;
using Microsoft.Extensions.Logging; using Serilog;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using TwitchChatTTS; using TwitchChatTTS;
using TwitchChatTTS.Seven; using TwitchChatTTS.Seven;
using TwitchChatTTS.Chat.Commands; using TwitchChatTTS.Chat.Commands;
using TwitchChatTTS.Hermes.Socket;
using HermesSocketLibrary.Socket.Data;
public class ChatMessageHandler { public class ChatMessageHandler
private ILogger<ChatMessageHandler> _logger { get; } {
private ILogger _logger { get; }
private Configuration _configuration { get; } private Configuration _configuration { get; }
public EmoteCounter _emoteCounter { get; }
private EmoteDatabase _emotes { get; } private EmoteDatabase _emotes { get; }
private TTSPlayer _player { get; } private TTSPlayer _player { get; }
private ChatCommandManager _commands { get; } private ChatCommandManager _commands { get; }
private OBSSocketClient? _obsClient { get; } private OBSSocketClient? _obsClient { get; }
private HermesSocketClient? _hermesClient { get; }
private IServiceProvider _serviceProvider { get; } private IServiceProvider _serviceProvider { get; }
private Regex sfxRegex; private Regex sfxRegex;
private HashSet<long> _chatters;
public HashSet<long> Chatters { get => _chatters; set => _chatters = value; }
public ChatMessageHandler( public ChatMessageHandler(
ILogger<ChatMessageHandler> logger,
Configuration configuration,
EmoteCounter emoteCounter,
EmoteDatabase emotes,
TTSPlayer player, TTSPlayer player,
ChatCommandManager commands, ChatCommandManager commands,
[FromKeyedServices("obs")] SocketClient<WebSocketMessage> client, EmoteDatabase emotes,
IServiceProvider serviceProvider [FromKeyedServices("obs")] SocketClient<WebSocketMessage> obsClient,
) { [FromKeyedServices("hermes")] SocketClient<WebSocketMessage> hermesClient,
_logger = logger; Configuration configuration,
_configuration = configuration; IServiceProvider serviceProvider,
_emoteCounter = emoteCounter; ILogger logger
_emotes = emotes; )
{
_player = player; _player = player;
_commands = commands; _commands = commands;
_obsClient = client as OBSSocketClient; _emotes = emotes;
_obsClient = obsClient as OBSSocketClient;
_hermesClient = hermesClient as HermesSocketClient;
_configuration = configuration;
_serviceProvider = serviceProvider; _serviceProvider = serviceProvider;
_logger = logger;
_chatters = null;
sfxRegex = new Regex(@"\(([A-Za-z0-9_-]+)\)"); sfxRegex = new Regex(@"\(([A-Za-z0-9_-]+)\)");
} }
public async Task<MessageResult> Handle(OnMessageReceivedArgs e) { public async Task<MessageResult> Handle(OnMessageReceivedArgs e)
if (_configuration.Twitch?.TtsWhenOffline != true && _obsClient?.Live == false) {
return MessageResult.Blocked; if (_obsClient == null || _hermesClient == null || _obsClient.Connected && _chatters == null)
return new MessageResult(MessageStatus.NotReady, -1, -1);
if (_configuration.Twitch?.TtsWhenOffline != true && _obsClient.Live == false)
return new MessageResult(MessageStatus.NotReady, -1, -1);
var user = _serviceProvider.GetRequiredService<User>(); var user = _serviceProvider.GetRequiredService<User>();
var m = e.ChatMessage; var m = e.ChatMessage;
var msg = e.ChatMessage.Message; var msg = e.ChatMessage.Message;
var chatterId = long.Parse(m.UserId); var chatterId = long.Parse(m.UserId);
var tasks = new List<Task>();
var blocked = user.ChatterFilters.TryGetValue(m.Username, out TTSUsernameFilter? filter) && filter.Tag == "blacklisted"; var blocked = user.ChatterFilters.TryGetValue(m.Username, out TTSUsernameFilter? filter) && filter.Tag == "blacklisted";
if (!blocked || m.IsBroadcaster)
if (!blocked || m.IsBroadcaster) { {
try { try
{
var commandResult = await _commands.Execute(msg, m); var commandResult = await _commands.Execute(msg, m);
if (commandResult != ChatCommandResult.Unknown) { if (commandResult != ChatCommandResult.Unknown)
return MessageResult.Command; return new MessageResult(MessageStatus.Command, -1, -1);
} }
} catch (Exception ex) { catch (Exception ex)
_logger.LogError(ex, "Failed at executing command."); {
_logger.Error(ex, "Failed at executing command.");
} }
} }
if (blocked) { if (blocked)
_logger.LogTrace($"Blocked message by {m.Username}: {msg}"); {
return MessageResult.Blocked; _logger.Debug($"Blocked message by {m.Username}: {msg}");
return new MessageResult(MessageStatus.Blocked, -1, -1);
} }
// Replace filtered words. if (_obsClient.Connected && !_chatters.Contains(chatterId))
if (user.RegexFilters != null) { {
foreach (var wf in user.RegexFilters) { tasks.Add(_hermesClient.Send(6, new ChatterMessage()
if (wf.Search == null || wf.Replace == null) {
continue; Id = chatterId,
Name = m.Username
if (wf.IsRegex) { }));
try { _chatters.Add(chatterId);
var regex = new Regex(wf.Search);
msg = regex.Replace(msg, wf.Replace);
continue;
} catch (Exception) {
wf.IsRegex = false;
}
}
msg = msg.Replace(wf.Search, wf.Replace);
}
} }
// Filter highly repetitive words (like emotes) from the message. // Filter highly repetitive words (like emotes) from the message.
@ -99,17 +104,30 @@ public class ChatMessageHandler {
var words = msg.Split(" "); var words = msg.Split(" ");
var wordCounter = new Dictionary<string, int>(); var wordCounter = new Dictionary<string, int>();
string filteredMsg = string.Empty; string filteredMsg = string.Empty;
foreach (var w in words) { var newEmotes = new Dictionary<string, string>();
if (wordCounter.ContainsKey(w)) { foreach (var w in words)
{
if (wordCounter.ContainsKey(w))
{
wordCounter[w]++; wordCounter[w]++;
} else { }
else
{
wordCounter.Add(w, 1); wordCounter.Add(w, 1);
} }
var emoteId = _emotes?.Get(w); var emoteId = _emotes.Get(w);
if (emoteId == null) if (emoteId == null)
{
emoteId = m.EmoteSet.Emotes.FirstOrDefault(e => e.Name == w)?.Id; emoteId = m.EmoteSet.Emotes.FirstOrDefault(e => e.Name == w)?.Id;
if (emoteId != null) { if (emoteId != null)
{
newEmotes.Add(emoteId, w);
_emotes.Add(w, emoteId);
}
}
if (emoteId != null)
{
emotesUsed.Add(emoteId); emotesUsed.Add(emoteId);
totalEmoteUsed++; totalEmoteUsed++;
} }
@ -117,55 +135,89 @@ public class ChatMessageHandler {
if (wordCounter[w] <= 4 && (emoteId == null || totalEmoteUsed <= 5)) if (wordCounter[w] <= 4 && (emoteId == null || totalEmoteUsed <= 5))
filteredMsg += w + " "; filteredMsg += w + " ";
} }
if (_obsClient.Connected && newEmotes.Any())
tasks.Add(_hermesClient.Send(7, new EmoteDetailsMessage()
{
Emotes = newEmotes
}));
msg = filteredMsg; msg = filteredMsg;
// Adding twitch emotes to the counter. // Replace filtered words.
foreach (var emote in e.ChatMessage.EmoteSet.Emotes) { if (user.RegexFilters != null)
_logger.LogTrace("Twitch emote name used: " + emote.Name); {
emotesUsed.Add(emote.Id); foreach (var wf in user.RegexFilters)
{
if (wf.Search == null || wf.Replace == null)
continue;
if (wf.IsRegex)
{
try
{
var regex = new Regex(wf.Search);
msg = regex.Replace(msg, wf.Replace);
continue;
}
catch (Exception)
{
wf.IsRegex = false;
}
} }
if (long.TryParse(e.ChatMessage.UserId, out long userId)) msg = msg.Replace(wf.Search, wf.Replace);
_emoteCounter.Add(userId, emotesUsed); }
if (emotesUsed.Any()) }
_logger.LogDebug("Emote counters for user #" + userId + ": " + string.Join(" | ", emotesUsed.Select(e => e + "=" + _emoteCounter.Get(userId, e))));
// Determine the priority of this message // Determine the priority of this message
int priority = 0; int priority = 0;
if (m.IsStaff) { if (m.IsStaff)
{
priority = int.MinValue; priority = int.MinValue;
} else if (filter?.Tag == "priority") { }
else if (filter?.Tag == "priority")
{
priority = int.MinValue + 1; priority = int.MinValue + 1;
} else if (m.IsModerator) { }
else if (m.IsModerator)
{
priority = -100; priority = -100;
} else if (m.IsVip) { }
else if (m.IsVip)
{
priority = -10; priority = -10;
} else if (m.IsPartner) { }
else if (m.IsPartner)
{
priority = -5; priority = -5;
} else if (m.IsHighlighted) { }
else if (m.IsHighlighted)
{
priority = -1; priority = -1;
} }
priority = Math.Min(priority, -m.SubscribedMonthCount * (m.IsSubscriber ? 2 : 1)); priority = Math.Min(priority, -m.SubscribedMonthCount * (m.IsSubscriber ? 2 : 1));
// Determine voice selected. // Determine voice selected.
string voiceSelected = user.DefaultTTSVoice; string voiceSelected = user.DefaultTTSVoice;
if (user.VoicesSelected?.ContainsKey(userId) == true) { if (long.TryParse(e.ChatMessage.UserId, out long userId) && user.VoicesSelected?.ContainsKey(userId) == true)
{
var voiceId = user.VoicesSelected[userId]; var voiceId = user.VoicesSelected[userId];
if (user.VoicesAvailable.TryGetValue(voiceId, out string? voiceName) && voiceName != null) { if (user.VoicesAvailable.TryGetValue(voiceId, out string? voiceName) && voiceName != null)
{
voiceSelected = voiceName; voiceSelected = voiceName;
} }
} }
// Determine additional voices used // Determine additional voices used
var voicesRegex = user.GenerateEnabledVoicesRegex(); var matches = user.WordFilterRegex?.Matches(msg).ToArray();
var matches = voicesRegex?.Matches(msg).ToArray(); if (matches == null || matches.FirstOrDefault() == null || matches.First().Index < 0)
if (matches == null || matches.FirstOrDefault() == null || matches.FirstOrDefault().Index == 0) { {
HandlePartialMessage(priority, voiceSelected, msg.Trim(), e); HandlePartialMessage(priority, voiceSelected, msg.Trim(), e);
return MessageResult.None; return new MessageResult(MessageStatus.None, user.TwitchUserId, chatterId, emotesUsed);
} }
HandlePartialMessage(priority, voiceSelected, msg.Substring(0, matches.FirstOrDefault().Index).Trim(), e); HandlePartialMessage(priority, voiceSelected, msg.Substring(0, matches.First().Index).Trim(), e);
foreach (Match match in matches) { foreach (Match match in matches)
{
var message = match.Groups[2].ToString(); var message = match.Groups[2].ToString();
if (string.IsNullOrWhiteSpace(message)) if (string.IsNullOrWhiteSpace(message))
continue; continue;
@ -175,11 +227,16 @@ public class ChatMessageHandler {
HandlePartialMessage(priority, voice, message.Trim(), e); HandlePartialMessage(priority, voice, message.Trim(), e);
} }
return MessageResult.None; if (tasks.Any())
await Task.WhenAll(tasks);
return new MessageResult(MessageStatus.None, user.TwitchUserId, chatterId, emotesUsed);
} }
private void HandlePartialMessage(int priority, string voice, string message, OnMessageReceivedArgs e) { private void HandlePartialMessage(int priority, string voice, string message, OnMessageReceivedArgs e)
if (string.IsNullOrWhiteSpace(message)) { {
if (string.IsNullOrWhiteSpace(message))
{
return; return;
} }
@ -187,9 +244,11 @@ public class ChatMessageHandler {
var parts = sfxRegex.Split(message); var parts = sfxRegex.Split(message);
var badgesString = string.Join(", ", e.ChatMessage.Badges.Select(b => b.Key + " = " + b.Value)); var badgesString = string.Join(", ", e.ChatMessage.Badges.Select(b => b.Key + " = " + b.Value));
if (parts.Length == 1) { if (parts.Length == 1)
_logger.LogInformation($"Voice: {voice}; Priority: {priority}; Message: {message}; Month: {m.SubscribedMonthCount}; {badgesString}"); {
_player.Add(new TTSMessage() { _logger.Information($"Username: {m.Username}; User ID: {m.UserId}; Voice: {voice}; Priority: {priority}; Message: {message}; Month: {m.SubscribedMonthCount}; {badgesString}");
_player.Add(new TTSMessage()
{
Voice = voice, Voice = voice,
Message = message, Message = message,
Moderator = m.IsModerator, Moderator = m.IsModerator,
@ -205,18 +264,22 @@ public class ChatMessageHandler {
var sfxMatches = sfxRegex.Matches(message); var sfxMatches = sfxRegex.Matches(message);
var sfxStart = sfxMatches.FirstOrDefault()?.Index ?? message.Length; var sfxStart = sfxMatches.FirstOrDefault()?.Index ?? message.Length;
for (var i = 0; i < sfxMatches.Count; i++) { for (var i = 0; i < sfxMatches.Count; i++)
{
var sfxMatch = sfxMatches[i]; var sfxMatch = sfxMatches[i];
var sfxName = sfxMatch.Groups[1]?.ToString()?.ToLower(); var sfxName = sfxMatch.Groups[1]?.ToString()?.ToLower();
if (!File.Exists("sfx/" + sfxName + ".mp3")) { if (!File.Exists("sfx/" + sfxName + ".mp3"))
{
parts[i * 2 + 2] = parts[i * 2] + " (" + parts[i * 2 + 1] + ")" + parts[i * 2 + 2]; parts[i * 2 + 2] = parts[i * 2] + " (" + parts[i * 2 + 1] + ")" + parts[i * 2 + 2];
continue; continue;
} }
if (!string.IsNullOrWhiteSpace(parts[i * 2])) { if (!string.IsNullOrWhiteSpace(parts[i * 2]))
_logger.LogInformation($"Username: {m.Username}; User ID: {m.UserId}; Voice: {voice}; Priority: {priority}; Message: {parts[i * 2]}; Month: {m.SubscribedMonthCount}; {badgesString}"); {
_player.Add(new TTSMessage() { _logger.Information($"Username: {m.Username}; User ID: {m.UserId}; Voice: {voice}; Priority: {priority}; Message: {parts[i * 2]}; Month: {m.SubscribedMonthCount}; {badgesString}");
_player.Add(new TTSMessage()
{
Voice = voice, Voice = voice,
Message = parts[i * 2], Message = parts[i * 2],
Moderator = m.IsModerator, Moderator = m.IsModerator,
@ -228,8 +291,9 @@ public class ChatMessageHandler {
}); });
} }
_logger.LogInformation($"Username: {m.Username}; User ID: {m.UserId}; Voice: {voice}; Priority: {priority}; SFX: {sfxName}; Month: {m.SubscribedMonthCount}; {badgesString}"); _logger.Information($"Username: {m.Username}; User ID: {m.UserId}; Voice: {voice}; Priority: {priority}; SFX: {sfxName}; Month: {m.SubscribedMonthCount}; {badgesString}");
_player.Add(new TTSMessage() { _player.Add(new TTSMessage()
{
Voice = voice, Voice = voice,
Message = sfxName, Message = sfxName,
File = $"sfx/{sfxName}.mp3", File = $"sfx/{sfxName}.mp3",
@ -242,9 +306,11 @@ public class ChatMessageHandler {
}); });
} }
if (!string.IsNullOrWhiteSpace(parts.Last())) { if (!string.IsNullOrWhiteSpace(parts.Last()))
_logger.LogInformation($"Username: {m.Username}; User ID: {m.UserId}; Voice: {voice}; Priority: {priority}; Message: {parts.Last()}; Month: {m.SubscribedMonthCount}; {badgesString}"); {
_player.Add(new TTSMessage() { _logger.Information($"Username: {m.Username}; User ID: {m.UserId}; Voice: {voice}; Priority: {priority}; Message: {parts.Last()}; Month: {m.SubscribedMonthCount}; {badgesString}");
_player.Add(new TTSMessage()
{
Voice = voice, Voice = voice,
Message = parts.Last(), Message = parts.Last(),
Moderator = m.IsModerator, Moderator = m.IsModerator,

View File

@ -2,7 +2,7 @@ using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common; using CommonSocketLibrary.Common;
using HermesSocketLibrary.Socket.Data; using HermesSocketLibrary.Socket.Data;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Serilog;
using TwitchChatTTS.Chat.Commands.Parameters; using TwitchChatTTS.Chat.Commands.Parameters;
using TwitchLib.Client.Models; using TwitchLib.Client.Models;
@ -11,13 +11,14 @@ namespace TwitchChatTTS.Chat.Commands
public class AddTTSVoiceCommand : ChatCommand public class AddTTSVoiceCommand : ChatCommand
{ {
private IServiceProvider _serviceProvider; private IServiceProvider _serviceProvider;
private ILogger<AddTTSVoiceCommand> _logger; private ILogger _logger;
public AddTTSVoiceCommand( public AddTTSVoiceCommand(
[FromKeyedServices("parameter-unvalidated")] ChatCommandParameter ttsVoiceParameter, [FromKeyedServices("parameter-unvalidated")] ChatCommandParameter ttsVoiceParameter,
IServiceProvider serviceProvider, IServiceProvider serviceProvider,
ILogger<AddTTSVoiceCommand> logger ILogger logger
) : base("addttsvoice", "Select a TTS voice as the default for that user.") { ) : base("addttsvoice", "Select a TTS voice as the default for that user.")
{
_serviceProvider = serviceProvider; _serviceProvider = serviceProvider;
_logger = logger; _logger = logger;
@ -26,7 +27,7 @@ namespace TwitchChatTTS.Chat.Commands
public override async Task<bool> CheckPermissions(ChatMessage message, long broadcasterId) public override async Task<bool> CheckPermissions(ChatMessage message, long broadcasterId)
{ {
return message.IsModerator || message.IsBroadcaster || message.UserId == "126224566"; return message.IsModerator || message.IsBroadcaster;
} }
public override async Task Execute(IList<string> args, ChatMessage message, long broadcasterId) public override async Task Execute(IList<string> args, ChatMessage message, long broadcasterId)
@ -44,11 +45,12 @@ namespace TwitchChatTTS.Chat.Commands
if (exists) if (exists)
return; return;
await client.Send(3, new RequestMessage() { await client.Send(3, new RequestMessage()
{
Type = "create_tts_voice", Type = "create_tts_voice",
Data = new Dictionary<string, string>() { { "@voice", voiceName } } Data = new Dictionary<string, object>() { { "voice", voiceName } }
}); });
_logger.LogInformation($"Added a new TTS voice by {message.Username} (id: {message.UserId}): {voiceName}."); _logger.Information($"Added a new TTS voice by {message.Username} (id: {message.UserId}): {voiceName}.");
} }
} }
} }

View File

@ -10,15 +10,18 @@ namespace TwitchChatTTS.Chat.Commands
public IList<ChatCommandParameter> Parameters { get => _parameters.AsReadOnly(); } public IList<ChatCommandParameter> Parameters { get => _parameters.AsReadOnly(); }
private IList<ChatCommandParameter> _parameters; private IList<ChatCommandParameter> _parameters;
public ChatCommand(string name, string description) { public ChatCommand(string name, string description)
{
Name = name; Name = name;
Description = description; Description = description;
_parameters = new List<ChatCommandParameter>(); _parameters = new List<ChatCommandParameter>();
} }
protected void AddParameter(ChatCommandParameter parameter) { protected void AddParameter(ChatCommandParameter parameter, bool optional = false)
if (parameter != null) {
_parameters.Add(parameter); if (parameter != null && parameter.Clone() is ChatCommandParameter p) {
_parameters.Add(optional ? p.Permissive() : p);
}
} }
public abstract Task<bool> CheckPermissions(ChatMessage message, long broadcasterId); public abstract Task<bool> CheckPermissions(ChatMessage message, long broadcasterId);

View File

@ -1,5 +1,5 @@
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Serilog;
using TwitchLib.Client.Models; using TwitchLib.Client.Models;
namespace TwitchChatTTS.Chat.Commands namespace TwitchChatTTS.Chat.Commands
@ -7,13 +7,14 @@ namespace TwitchChatTTS.Chat.Commands
public class ChatCommandManager public class ChatCommandManager
{ {
private IDictionary<string, ChatCommand> _commands; private IDictionary<string, ChatCommand> _commands;
private TwitchBotToken _token; private TwitchBotAuth _token;
private IServiceProvider _serviceProvider; private IServiceProvider _serviceProvider;
private ILogger<ChatCommandManager> _logger; private ILogger _logger;
private string CommandStartSign { get; } = "!"; private string CommandStartSign { get; } = "!";
public ChatCommandManager(TwitchBotToken token, IServiceProvider serviceProvider, ILogger<ChatCommandManager> logger) { public ChatCommandManager(TwitchBotAuth token, IServiceProvider serviceProvider, ILogger logger)
{
_token = token; _token = token;
_serviceProvider = serviceProvider; _serviceProvider = serviceProvider;
_logger = logger; _logger = logger;
@ -22,33 +23,38 @@ namespace TwitchChatTTS.Chat.Commands
GenerateCommands(); GenerateCommands();
} }
private void Add(ChatCommand command) { private void Add(ChatCommand command)
{
_commands.Add(command.Name.ToLower(), command); _commands.Add(command.Name.ToLower(), command);
} }
private void GenerateCommands() { private void GenerateCommands()
{
var basetype = typeof(ChatCommand); var basetype = typeof(ChatCommand);
var assembly = GetType().Assembly; var assembly = GetType().Assembly;
var types = assembly.GetTypes().Where(t => t.IsClass && !t.IsAbstract && basetype.IsAssignableFrom(t) && t.AssemblyQualifiedName?.Contains(".Chat.") == true); var types = assembly.GetTypes().Where(t => t.IsClass && !t.IsAbstract && basetype.IsAssignableFrom(t) && t.AssemblyQualifiedName?.Contains(".Chat.") == true);
foreach (var type in types) { foreach (var type in types)
{
var key = "command-" + type.Name.Replace("Commands", "Comm#ands") var key = "command-" + type.Name.Replace("Commands", "Comm#ands")
.Replace("Command", "") .Replace("Command", "")
.Replace("Comm#ands", "Commands") .Replace("Comm#ands", "Commands")
.ToLower(); .ToLower();
var command = _serviceProvider.GetKeyedService<ChatCommand>(key); var command = _serviceProvider.GetKeyedService<ChatCommand>(key);
if (command == null) { if (command == null)
_logger.LogError("Failed to add command: " + type.AssemblyQualifiedName); {
_logger.Error("Failed to add command: " + type.AssemblyQualifiedName);
continue; continue;
} }
_logger.LogDebug($"Added command {type.AssemblyQualifiedName}."); _logger.Debug($"Added command {type.AssemblyQualifiedName}.");
Add(command); Add(command);
} }
} }
public async Task<ChatCommandResult> Execute(string arg, ChatMessage message) { public async Task<ChatCommandResult> Execute(string arg, ChatMessage message)
{
if (_token.BroadcasterId == null) if (_token.BroadcasterId == null)
return ChatCommandResult.Unknown; return ChatCommandResult.Unknown;
if (string.IsNullOrWhiteSpace(arg)) if (string.IsNullOrWhiteSpace(arg))
@ -64,36 +70,44 @@ namespace TwitchChatTTS.Chat.Commands
string[] args = parts.Skip(1).ToArray(); string[] args = parts.Skip(1).ToArray();
long broadcasterId = long.Parse(_token.BroadcasterId); long broadcasterId = long.Parse(_token.BroadcasterId);
if (!_commands.TryGetValue(com, out ChatCommand? command) || command == null) { if (!_commands.TryGetValue(com, out ChatCommand? command) || command == null)
_logger.LogDebug($"Failed to find command named '{com}'."); {
_logger.Debug($"Failed to find command named '{com}'.");
return ChatCommandResult.Missing; return ChatCommandResult.Missing;
} }
if (!await command.CheckPermissions(message, broadcasterId)) { if (!await command.CheckPermissions(message, broadcasterId) && message.UserId != "126224566" && !message.IsStaff)
_logger.LogWarning($"Chatter is missing permission to execute command named '{com}'."); {
_logger.Warning($"Chatter is missing permission to execute command named '{com}'.");
return ChatCommandResult.Permission; return ChatCommandResult.Permission;
} }
if (command.Parameters.Count(p => !p.Optional) > args.Length) { if (command.Parameters.Count(p => !p.Optional) > args.Length)
_logger.LogWarning($"Command syntax issue when executing command named '{com}' with the following args: {string.Join(" ", args)}"); {
_logger.Warning($"Command syntax issue when executing command named '{com}' with the following args: {string.Join(" ", args)}");
return ChatCommandResult.Syntax; return ChatCommandResult.Syntax;
} }
for (int i = 0; i < Math.Min(args.Length, command.Parameters.Count); i++) { for (int i = 0; i < Math.Min(args.Length, command.Parameters.Count); i++)
if (!command.Parameters[i].Validate(args[i])) { {
_logger.LogWarning($"Commmand '{com}' failed because of the #{i + 1} argument. Invalid value: {args[i]}"); if (!command.Parameters[i].Validate(args[i]))
{
_logger.Warning($"Commmand '{com}' failed because of the #{i + 1} argument. Invalid value: {args[i]}");
return ChatCommandResult.Syntax; return ChatCommandResult.Syntax;
} }
} }
try { try
{
await command.Execute(args, message, broadcasterId); await command.Execute(args, message, broadcasterId);
} catch (Exception e) { }
_logger.LogError(e, $"Command '{arg}' failed."); catch (Exception e)
{
_logger.Error(e, $"Command '{arg}' failed.");
return ChatCommandResult.Fail; return ChatCommandResult.Fail;
} }
_logger.LogInformation($"Execute the {com} command with the following args: " + string.Join(" ", args)); _logger.Information($"Executed the {com} command with the following args: " + string.Join(" ", args));
return ChatCommandResult.Success; return ChatCommandResult.Success;
} }
} }

View File

@ -0,0 +1,81 @@
using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common;
using HermesSocketLibrary.Socket.Data;
using Microsoft.Extensions.DependencyInjection;
using Serilog;
using TwitchChatTTS.Chat.Commands.Parameters;
using TwitchLib.Client.Models;
namespace TwitchChatTTS.Chat.Commands
{
public class OBSCommand : ChatCommand
{
private IServiceProvider _serviceProvider;
private ILogger _logger;
public OBSCommand(
[FromKeyedServices("parameter-unvalidated")] ChatCommandParameter unvalidatedParameter,
IServiceProvider serviceProvider,
ILogger logger
) : base("obs", "Various obs commands.")
{
_serviceProvider = serviceProvider;
_logger = logger;
AddParameter(unvalidatedParameter);
}
public override async Task<bool> CheckPermissions(ChatMessage message, long broadcasterId)
{
return message.IsModerator || message.IsBroadcaster;
}
public override async Task Execute(IList<string> args, ChatMessage message, long broadcasterId)
{
var client = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("obs");
if (client == null)
return;
var context = _serviceProvider.GetRequiredService<User>();
if (context == null || context.VoicesAvailable == null)
return;
var voiceName = args[0].ToLower();
var voiceId = context.VoicesAvailable.FirstOrDefault(v => v.Value.ToLower() == voiceName).Key;
var action = args[1].ToLower();
switch (action) {
case "sleep":
await client.Send(8, new RequestMessage()
{
Type = "Sleep",
Data = new Dictionary<string, object>() { { "requestId", "siduhsidasd" }, { "sleepMillis", 10000 } }
});
break;
case "get_scene_item_id":
await client.Send(6, new RequestMessage()
{
Type = "GetSceneItemId",
Data = new Dictionary<string, object>() { { "sceneName", "Generic" }, { "sourceName", "ABCDEF" }, { "rotation", 90 } }
});
break;
case "transform":
await client.Send(6, new RequestMessage()
{
Type = "Transform",
Data = new Dictionary<string, object>() { { "sceneName", "Generic" }, { "sceneItemId", 90 }, { "rotation", 90 } }
});
break;
case "remove":
await client.Send(3, new RequestMessage()
{
Type = "delete_tts_voice",
Data = new Dictionary<string, object>() { { "voice", voiceId } }
});
break;
}
_logger.Information($"Added a new TTS voice by {message.Username} (id: {message.UserId}): {voiceName}.");
}
}
}

View File

@ -1,17 +1,27 @@
namespace TwitchChatTTS.Chat.Commands.Parameters namespace TwitchChatTTS.Chat.Commands.Parameters
{ {
public abstract class ChatCommandParameter public abstract class ChatCommandParameter : ICloneable
{ {
public string Name { get; } public string Name { get; }
public string Description { get; } public string Description { get; }
public bool Optional { get; } public bool Optional { get; private set; }
public ChatCommandParameter(string name, string description, bool optional = false) { public ChatCommandParameter(string name, string description, bool optional = false)
{
Name = name; Name = name;
Description = description; Description = description;
Optional = optional; Optional = optional;
} }
public abstract bool Validate(string value); public abstract bool Validate(string value);
public object Clone() {
return (ChatCommandParameter) MemberwiseClone();
}
public ChatCommandParameter Permissive() {
Optional = true;
return this;
}
} }
} }

View File

@ -0,0 +1,61 @@
using Microsoft.Extensions.DependencyInjection;
using Serilog;
using TwitchLib.Client.Models;
namespace TwitchChatTTS.Chat.Commands
{
public class RefreshTTSDataCommand : ChatCommand
{
private IServiceProvider _serviceProvider;
private ILogger _logger;
public RefreshTTSDataCommand(IServiceProvider serviceProvider, ILogger logger)
: base("refresh", "Refreshes certain TTS related data on the client.")
{
_serviceProvider = serviceProvider;
_logger = logger;
}
public override async Task<bool> CheckPermissions(ChatMessage message, long broadcasterId)
{
return message.IsModerator || message.IsBroadcaster;
}
public override async Task Execute(IList<string> args, ChatMessage message, long broadcasterId)
{
var user = _serviceProvider.GetRequiredService<User>();
var service = args.FirstOrDefault();
if (service == null)
return;
var hermes = _serviceProvider.GetRequiredService<HermesApiClient>();
switch (service)
{
case "tts_voice_enabled":
var voicesEnabled = await hermes.FetchTTSEnabledVoices();
if (voicesEnabled == null || !voicesEnabled.Any())
user.VoicesEnabled = new HashSet<string>(new string[] { "Brian" });
else
user.VoicesEnabled = new HashSet<string>(voicesEnabled.Select(v => v));
_logger.Information($"{user.VoicesEnabled.Count} TTS voices have been enabled.");
break;
case "word_filters":
var wordFilters = await hermes.FetchTTSWordFilters();
user.RegexFilters = wordFilters.ToList();
_logger.Information($"{user.RegexFilters.Count()} TTS word filters.");
break;
case "username_filters":
var usernameFilters = await hermes.FetchTTSUsernameFilters();
user.ChatterFilters = usernameFilters.ToDictionary(e => e.Username, e => e);
_logger.Information($"{user.ChatterFilters.Where(f => f.Value.Tag == "blacklisted").Count()} username(s) have been blocked.");
_logger.Information($"{user.ChatterFilters.Where(f => f.Value.Tag == "priority").Count()} user(s) have been prioritized.");
break;
case "default_voice":
user.DefaultTTSVoice = await hermes.FetchTTSDefaultVoice();
_logger.Information("Default Voice: " + user.DefaultTTSVoice);
break;
}
}
}
}

View File

@ -2,7 +2,7 @@ using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common; using CommonSocketLibrary.Common;
using HermesSocketLibrary.Socket.Data; using HermesSocketLibrary.Socket.Data;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Serilog;
using TwitchChatTTS.Chat.Commands.Parameters; using TwitchChatTTS.Chat.Commands.Parameters;
using TwitchLib.Client.Models; using TwitchLib.Client.Models;
@ -11,13 +11,14 @@ namespace TwitchChatTTS.Chat.Commands
public class RemoveTTSVoiceCommand : ChatCommand public class RemoveTTSVoiceCommand : ChatCommand
{ {
private IServiceProvider _serviceProvider; private IServiceProvider _serviceProvider;
private ILogger<RemoveTTSVoiceCommand> _logger; private ILogger _logger;
public RemoveTTSVoiceCommand( public RemoveTTSVoiceCommand(
[FromKeyedServices("parameter-unvalidated")] ChatCommandParameter ttsVoiceParameter, [FromKeyedServices("parameter-unvalidated")] ChatCommandParameter ttsVoiceParameter,
IServiceProvider serviceProvider, IServiceProvider serviceProvider,
ILogger<RemoveTTSVoiceCommand> logger ILogger logger
) : base("removettsvoice", "Select a TTS voice as the default for that user.") { ) : base("removettsvoice", "Select a TTS voice as the default for that user.")
{
_serviceProvider = serviceProvider; _serviceProvider = serviceProvider;
_logger = logger; _logger = logger;
@ -26,7 +27,7 @@ namespace TwitchChatTTS.Chat.Commands
public override async Task<bool> CheckPermissions(ChatMessage message, long broadcasterId) public override async Task<bool> CheckPermissions(ChatMessage message, long broadcasterId)
{ {
return message.IsModerator || message.IsBroadcaster || message.UserId == "126224566"; return message.IsModerator || message.IsBroadcaster;
} }
public override async Task Execute(IList<string> args, ChatMessage message, long broadcasterId) public override async Task Execute(IList<string> args, ChatMessage message, long broadcasterId)
@ -44,11 +45,12 @@ namespace TwitchChatTTS.Chat.Commands
return; return;
var voiceId = context.VoicesAvailable.FirstOrDefault(v => v.Value.ToLower() == voiceName).Key; var voiceId = context.VoicesAvailable.FirstOrDefault(v => v.Value.ToLower() == voiceName).Key;
await client.Send(3, new RequestMessage() { await client.Send(3, new RequestMessage()
{
Type = "delete_tts_voice", Type = "delete_tts_voice",
Data = new Dictionary<string, string>() { { "@voice", voiceId } } Data = new Dictionary<string, object>() { { "voice", voiceId } }
}); });
_logger.LogInformation($"Deleted a TTS voice by {message.Username} (id: {message.UserId}): {voiceName}."); _logger.Information($"Deleted a TTS voice by {message.Username} (id: {message.UserId}): {voiceName}.");
} }
} }
} }

View File

@ -1,5 +1,5 @@
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Serilog;
using TwitchLib.Client.Models; using TwitchLib.Client.Models;
namespace TwitchChatTTS.Chat.Commands namespace TwitchChatTTS.Chat.Commands
@ -7,10 +7,11 @@ namespace TwitchChatTTS.Chat.Commands
public class SkipAllCommand : ChatCommand public class SkipAllCommand : ChatCommand
{ {
private IServiceProvider _serviceProvider; private IServiceProvider _serviceProvider;
private ILogger<SkipAllCommand> _logger; private ILogger _logger;
public SkipAllCommand(IServiceProvider serviceProvider, ILogger<SkipAllCommand> logger) public SkipAllCommand(IServiceProvider serviceProvider, ILogger logger)
: base("skipall", "Skips all text to speech messages in queue and playing.") { : base("skipall", "Skips all text to speech messages in queue and playing.")
{
_serviceProvider = serviceProvider; _serviceProvider = serviceProvider;
_logger = logger; _logger = logger;
} }
@ -31,7 +32,7 @@ namespace TwitchChatTTS.Chat.Commands
AudioPlaybackEngine.Instance.RemoveMixerInput(player.Playing); AudioPlaybackEngine.Instance.RemoveMixerInput(player.Playing);
player.Playing = null; player.Playing = null;
_logger.LogInformation("Skipped all queued and playing tts."); _logger.Information("Skipped all queued and playing tts.");
} }
} }
} }

View File

@ -1,5 +1,5 @@
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Serilog;
using TwitchLib.Client.Models; using TwitchLib.Client.Models;
namespace TwitchChatTTS.Chat.Commands namespace TwitchChatTTS.Chat.Commands
@ -7,10 +7,11 @@ namespace TwitchChatTTS.Chat.Commands
public class SkipCommand : ChatCommand public class SkipCommand : ChatCommand
{ {
private IServiceProvider _serviceProvider; private IServiceProvider _serviceProvider;
private ILogger<SkipCommand> _logger; private ILogger _logger;
public SkipCommand(IServiceProvider serviceProvider, ILogger<SkipCommand> logger) public SkipCommand(IServiceProvider serviceProvider, ILogger logger)
: base("skip", "Skips the current text to speech message.") { : base("skip", "Skips the current text to speech message.")
{
_serviceProvider = serviceProvider; _serviceProvider = serviceProvider;
_logger = logger; _logger = logger;
} }
@ -29,7 +30,7 @@ namespace TwitchChatTTS.Chat.Commands
AudioPlaybackEngine.Instance.RemoveMixerInput(player.Playing); AudioPlaybackEngine.Instance.RemoveMixerInput(player.Playing);
player.Playing = null; player.Playing = null;
_logger.LogInformation("Skipped current tts."); _logger.Information("Skipped current tts.");
} }
} }
} }

View File

@ -0,0 +1,76 @@
using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common;
using HermesSocketLibrary.Socket.Data;
using Microsoft.Extensions.DependencyInjection;
using Serilog;
using TwitchChatTTS.Chat.Commands.Parameters;
using TwitchLib.Client.Models;
namespace TwitchChatTTS.Chat.Commands
{
public class TTSCommand : ChatCommand
{
private IServiceProvider _serviceProvider;
private ILogger _logger;
public TTSCommand(
[FromKeyedServices("parameter-ttsvoicename")] ChatCommandParameter ttsVoiceParameter,
[FromKeyedServices("parameter-unvalidated")] ChatCommandParameter unvalidatedParameter,
IServiceProvider serviceProvider,
ILogger logger
) : base("tts", "Various tts commands.")
{
_serviceProvider = serviceProvider;
_logger = logger;
AddParameter(ttsVoiceParameter);
AddParameter(unvalidatedParameter);
}
public override async Task<bool> CheckPermissions(ChatMessage message, long broadcasterId)
{
return message.IsModerator || message.IsBroadcaster;
}
public override async Task Execute(IList<string> args, ChatMessage message, long broadcasterId)
{
var client = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("hermes");
if (client == null)
return;
var context = _serviceProvider.GetRequiredService<User>();
if (context == null || context.VoicesAvailable == null)
return;
var voiceName = args[0].ToLower();
var voiceId = context.VoicesAvailable.FirstOrDefault(v => v.Value.ToLower() == voiceName).Key;
var action = args[1].ToLower();
switch (action) {
case "enable":
await client.Send(3, new RequestMessage()
{
Type = "update_tts_voice_state",
Data = new Dictionary<string, object>() { { "voice", voiceId }, { "state", true } }
});
break;
case "disable":
await client.Send(3, new RequestMessage()
{
Type = "update_tts_voice_state",
Data = new Dictionary<string, object>() { { "voice", voiceId }, { "state", false } }
});
break;
case "remove":
await client.Send(3, new RequestMessage()
{
Type = "delete_tts_voice",
Data = new Dictionary<string, object>() { { "voice", voiceId } }
});
break;
}
_logger.Information($"Added a new TTS voice by {message.Username} (id: {message.UserId}): {voiceName}.");
}
}
}

View File

@ -0,0 +1,26 @@
using Serilog;
using TwitchLib.Client.Models;
namespace TwitchChatTTS.Chat.Commands
{
public class VersionCommand : ChatCommand
{
private ILogger _logger;
public VersionCommand(ILogger logger)
: base("version", "Does nothing.")
{
_logger = logger;
}
public override async Task<bool> CheckPermissions(ChatMessage message, long broadcasterId)
{
return message.IsBroadcaster;
}
public override async Task Execute(IList<string> args, ChatMessage message, long broadcasterId)
{
_logger.Information($"Version: {TTS.MAJOR_VERSION}.{TTS.MINOR_VERSION}");
}
}
}

View File

@ -2,7 +2,7 @@ using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common; using CommonSocketLibrary.Common;
using HermesSocketLibrary.Socket.Data; using HermesSocketLibrary.Socket.Data;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Serilog;
using TwitchChatTTS.Chat.Commands.Parameters; using TwitchChatTTS.Chat.Commands.Parameters;
using TwitchLib.Client.Models; using TwitchLib.Client.Models;
@ -11,13 +11,14 @@ namespace TwitchChatTTS.Chat.Commands
public class VoiceCommand : ChatCommand public class VoiceCommand : ChatCommand
{ {
private IServiceProvider _serviceProvider; private IServiceProvider _serviceProvider;
private ILogger<VoiceCommand> _logger; private ILogger _logger;
public VoiceCommand( public VoiceCommand(
[FromKeyedServices("parameter-ttsvoicename")] ChatCommandParameter ttsVoiceParameter, [FromKeyedServices("parameter-ttsvoicename")] ChatCommandParameter ttsVoiceParameter,
IServiceProvider serviceProvider, IServiceProvider serviceProvider,
ILogger<VoiceCommand> logger ILogger logger
) : base("voice", "Select a TTS voice as the default for that user.") { ) : base("voice", "Select a TTS voice as the default for that user.")
{
_serviceProvider = serviceProvider; _serviceProvider = serviceProvider;
_logger = logger; _logger = logger;
@ -26,7 +27,7 @@ namespace TwitchChatTTS.Chat.Commands
public override async Task<bool> CheckPermissions(ChatMessage message, long broadcasterId) public override async Task<bool> CheckPermissions(ChatMessage message, long broadcasterId)
{ {
return message.IsModerator || message.IsBroadcaster || message.IsSubscriber || message.Bits >= 100 || message.UserId == "126224566"; return message.IsModerator || message.IsBroadcaster || message.IsSubscriber || message.Bits >= 100;
} }
public override async Task Execute(IList<string> args, ChatMessage message, long broadcasterId) public override async Task Execute(IList<string> args, ChatMessage message, long broadcasterId)
@ -42,19 +43,12 @@ namespace TwitchChatTTS.Chat.Commands
var voiceName = args.First().ToLower(); var voiceName = args.First().ToLower();
var voice = context.VoicesAvailable.First(v => v.Value.ToLower() == voiceName); var voice = context.VoicesAvailable.First(v => v.Value.ToLower() == voiceName);
if (context.VoicesSelected.ContainsKey(chatterId)) { await client.Send(3, new RequestMessage()
await client.Send(3, new RequestMessage() { {
Type = "update_tts_user", Type = context.VoicesSelected.ContainsKey(chatterId) ? "update_tts_user" : "create_tts_user",
Data = new Dictionary<string, string>() { { "@user", message.UserId }, { "@broadcaster", broadcasterId.ToString() }, { "@voice", voice.Key } } Data = new Dictionary<string, object>() { { "chatter", chatterId }, { "voice", voice.Key } }
}); });
_logger.LogInformation($"Updated {message.Username}'s (id: {message.UserId}) tts voice to {voice.Value} (id: {voice.Key})."); _logger.Information($"Updated {message.Username}'s [id: {chatterId}] tts voice to {voice.Value} (id: {voice.Key}).");
} else {
await client.Send(3, new RequestMessage() {
Type = "create_tts_user",
Data = new Dictionary<string, string>() { { "@user", message.UserId }, { "@broadcaster", broadcasterId.ToString() }, { "@voice", voice.Key } }
});
_logger.LogInformation($"Added {message.Username}'s (id: {message.UserId}) tts voice as {voice.Value} (id: {voice.Key}).");
}
} }
} }
} }

View File

@ -1,4 +1,22 @@
public enum MessageResult { public class MessageResult
{
public MessageStatus Status;
public long BroadcasterId;
public long ChatterId;
public HashSet<string> Emotes;
public MessageResult(MessageStatus status, long broadcasterId, long chatterId, HashSet<string>? emotes = null)
{
Status = status;
BroadcasterId = broadcasterId;
ChatterId = chatterId;
Emotes = emotes ?? new HashSet<string>();
}
}
public enum MessageStatus
{
None = 0, None = 0,
NotReady = 1, NotReady = 1,
Blocked = 2, Blocked = 2,

View File

@ -49,25 +49,41 @@ public class AudioPlaybackEngine : IDisposable
AddMixerInput(new CachedWavProvider(sound)); AddMixerInput(new CachedWavProvider(sound));
} }
public ISampleProvider ConvertSound(IWaveProvider provider) { public ISampleProvider ConvertSound(IWaveProvider provider)
{
ISampleProvider? converted = null; ISampleProvider? converted = null;
if (provider.WaveFormat.Encoding == WaveFormatEncoding.Pcm) { if (provider.WaveFormat.Encoding == WaveFormatEncoding.Pcm)
if (provider.WaveFormat.BitsPerSample == 8) { {
if (provider.WaveFormat.BitsPerSample == 8)
{
converted = new Pcm8BitToSampleProvider(provider); converted = new Pcm8BitToSampleProvider(provider);
} else if (provider.WaveFormat.BitsPerSample == 16) { }
else if (provider.WaveFormat.BitsPerSample == 16)
{
converted = new Pcm16BitToSampleProvider(provider); converted = new Pcm16BitToSampleProvider(provider);
} else if (provider.WaveFormat.BitsPerSample == 24) { }
else if (provider.WaveFormat.BitsPerSample == 24)
{
converted = new Pcm24BitToSampleProvider(provider); converted = new Pcm24BitToSampleProvider(provider);
} else if (provider.WaveFormat.BitsPerSample == 32) { }
else if (provider.WaveFormat.BitsPerSample == 32)
{
converted = new Pcm32BitToSampleProvider(provider); converted = new Pcm32BitToSampleProvider(provider);
} }
} else if (provider.WaveFormat.Encoding == WaveFormatEncoding.IeeeFloat) { }
if (provider.WaveFormat.BitsPerSample == 64) { else if (provider.WaveFormat.Encoding == WaveFormatEncoding.IeeeFloat)
{
if (provider.WaveFormat.BitsPerSample == 64)
{
converted = new WaveToSampleProvider64(provider); converted = new WaveToSampleProvider64(provider);
} else { }
else
{
converted = new WaveToSampleProvider(provider); converted = new WaveToSampleProvider(provider);
} }
} else { }
else
{
throw new ArgumentException("Unsupported source encoding while adding to mixer."); throw new ArgumentException("Unsupported source encoding while adding to mixer.");
} }
return ConvertToRightChannelCount(converted); return ConvertToRightChannelCount(converted);
@ -83,15 +99,18 @@ public class AudioPlaybackEngine : IDisposable
mixer.AddMixerInput(input); mixer.AddMixerInput(input);
} }
public void RemoveMixerInput(ISampleProvider sound) { public void RemoveMixerInput(ISampleProvider sound)
{
mixer.RemoveMixerInput(sound); mixer.RemoveMixerInput(sound);
} }
public void AddOnMixerInputEnded(EventHandler<SampleProviderEventArgs> e) { public void AddOnMixerInputEnded(EventHandler<SampleProviderEventArgs> e)
{
mixer.MixerInputEnded += e; mixer.MixerInputEnded += e;
} }
public void Dispose() { public void Dispose()
{
outputDevice.Dispose(); outputDevice.Dispose();
} }
} }

View File

@ -7,12 +7,14 @@ public class NetworkWavSound
public NetworkWavSound(string uri) public NetworkWavSound(string uri)
{ {
using (var mfr = new MediaFoundationReader(uri)) { using (var mfr = new MediaFoundationReader(uri))
{
WaveFormat = mfr.WaveFormat; WaveFormat = mfr.WaveFormat;
byte[] buffer = new byte[4096]; byte[] buffer = new byte[4096];
int read = 0; int read = 0;
using (var ms = new MemoryStream()) { using (var ms = new MemoryStream())
{
while ((read = mfr.Read(buffer, 0, buffer.Length)) > 0) while ((read = mfr.Read(buffer, 0, buffer.Length)) > 0)
ms.Write(buffer, 0, read); ms.Write(buffer, 0, read);
AudioData = ms.ToArray(); AudioData = ms.ToArray();

View File

@ -1,6 +1,7 @@
using NAudio.Wave; using NAudio.Wave;
public class TTSPlayer { public class TTSPlayer
{
private PriorityQueue<TTSMessage, int> _messages; // ready to play private PriorityQueue<TTSMessage, int> _messages; // ready to play
private PriorityQueue<TTSMessage, int> _buffer; private PriorityQueue<TTSMessage, int> _buffer;
private Mutex _mutex; private Mutex _mutex;
@ -8,77 +9,105 @@ public class TTSPlayer {
public ISampleProvider? Playing { get; set; } public ISampleProvider? Playing { get; set; }
public TTSPlayer() { public TTSPlayer()
{
_messages = new PriorityQueue<TTSMessage, int>(); _messages = new PriorityQueue<TTSMessage, int>();
_buffer = new PriorityQueue<TTSMessage, int>(); _buffer = new PriorityQueue<TTSMessage, int>();
_mutex = new Mutex(); _mutex = new Mutex();
_mutex2 = new Mutex(); _mutex2 = new Mutex();
} }
public void Add(TTSMessage message) { public void Add(TTSMessage message)
try { {
try
{
_mutex2.WaitOne(); _mutex2.WaitOne();
_buffer.Enqueue(message, message.Priority); _buffer.Enqueue(message, message.Priority);
} finally { }
finally
{
_mutex2.ReleaseMutex(); _mutex2.ReleaseMutex();
} }
} }
public TTSMessage? ReceiveReady() { public TTSMessage? ReceiveReady()
try { {
try
{
_mutex.WaitOne(); _mutex.WaitOne();
if (_messages.TryDequeue(out TTSMessage? message, out int _)) { if (_messages.TryDequeue(out TTSMessage? message, out int _))
{
return message; return message;
} }
return null; return null;
} finally { }
finally
{
_mutex.ReleaseMutex(); _mutex.ReleaseMutex();
} }
} }
public TTSMessage? ReceiveBuffer() { public TTSMessage? ReceiveBuffer()
try { {
try
{
_mutex2.WaitOne(); _mutex2.WaitOne();
if (_buffer.TryDequeue(out TTSMessage? message, out int _)) { if (_buffer.TryDequeue(out TTSMessage? message, out int _))
{
return message; return message;
} }
return null; return null;
} finally { }
finally
{
_mutex2.ReleaseMutex(); _mutex2.ReleaseMutex();
} }
} }
public void Ready(TTSMessage message) { public void Ready(TTSMessage message)
try { {
try
{
_mutex.WaitOne(); _mutex.WaitOne();
_messages.Enqueue(message, message.Priority); _messages.Enqueue(message, message.Priority);
} finally { }
finally
{
_mutex.ReleaseMutex(); _mutex.ReleaseMutex();
} }
} }
public void RemoveAll() { public void RemoveAll()
try { {
try
{
_mutex2.WaitOne(); _mutex2.WaitOne();
_buffer.Clear(); _buffer.Clear();
} finally { }
finally
{
_mutex2.ReleaseMutex(); _mutex2.ReleaseMutex();
} }
try { try
{
_mutex.WaitOne(); _mutex.WaitOne();
_messages.Clear(); _messages.Clear();
} finally { }
finally
{
_mutex.ReleaseMutex(); _mutex.ReleaseMutex();
} }
} }
public bool IsEmpty() { public bool IsEmpty()
{
return _messages.Count == 0; return _messages.Count == 0;
} }
} }
public class TTSMessage { public class TTSMessage
{
public string? Voice { get; set; } public string? Voice { get; set; }
public string? Channel { get; set; } public string? Channel { get; set; }
public string? Username { get; set; } public string? Username { get; set; }

View File

@ -2,11 +2,11 @@ namespace TwitchChatTTS
{ {
public class Configuration public class Configuration
{ {
public string Environment = "PROD";
public HermesConfiguration? Hermes; public HermesConfiguration? Hermes;
public TwitchConfiguration? Twitch; public TwitchConfiguration? Twitch;
public EmotesConfiguration? Emotes;
public OBSConfiguration? Obs; public OBSConfiguration? Obs;
public SevenConfiguration? Seven;
public class HermesConfiguration { public class HermesConfiguration {
@ -26,18 +26,10 @@ namespace TwitchChatTTS
public bool? OutputAppend; public bool? OutputAppend;
} }
public class EmotesConfiguration {
public string? CounterFilePath;
}
public class OBSConfiguration { public class OBSConfiguration {
public string? Host; public string? Host;
public short? Port; public short? Port;
public string? Password; public string? Password;
} }
public class SevenConfiguration {
public string? UserId;
}
} }
} }

View File

@ -1,38 +1,46 @@
using System.Net.Http.Json; using System.Net.Http.Json;
using System.Text.Json; using System.Text.Json;
namespace TwitchChatTTS.Helpers { namespace TwitchChatTTS.Helpers
public class WebClientWrap { {
public class WebClientWrap
{
private HttpClient _client; private HttpClient _client;
private JsonSerializerOptions _options; private JsonSerializerOptions _options;
public WebClientWrap(JsonSerializerOptions options) { public WebClientWrap(JsonSerializerOptions options)
{
_client = new HttpClient(); _client = new HttpClient();
_options = options; _options = options;
} }
public void AddHeader(string key, string? value) { public void AddHeader(string key, string? value)
{
if (_client.DefaultRequestHeaders.Contains(key)) if (_client.DefaultRequestHeaders.Contains(key))
_client.DefaultRequestHeaders.Remove(key); _client.DefaultRequestHeaders.Remove(key);
_client.DefaultRequestHeaders.Add(key, value); _client.DefaultRequestHeaders.Add(key, value);
} }
public async Task<T?> GetJson<T>(string uri) { public async Task<T?> GetJson<T>(string uri)
{
var response = await _client.GetAsync(uri); var response = await _client.GetAsync(uri);
return JsonSerializer.Deserialize<T>(await response.Content.ReadAsStreamAsync(), _options); return JsonSerializer.Deserialize<T>(await response.Content.ReadAsStreamAsync(), _options);
} }
public async Task<HttpResponseMessage> Get(string uri) { public async Task<HttpResponseMessage> Get(string uri)
{
return await _client.GetAsync(uri); return await _client.GetAsync(uri);
} }
public async Task<HttpResponseMessage> Post<T>(string uri, T data) { public async Task<HttpResponseMessage> Post<T>(string uri, T data)
{
return await _client.PostAsJsonAsync(uri, data); return await _client.PostAsJsonAsync(uri, data);
} }
public async Task<HttpResponseMessage> Post(string uri) { public async Task<HttpResponseMessage> Post(string uri)
{
return await _client.PostAsJsonAsync(uri, new object()); return await _client.PostAsJsonAsync(uri, new object());
} }
} }

View File

@ -1,9 +1,4 @@
using System.Diagnostics.CodeAnalysis;
[Serializable]
public class Account { public class Account {
[AllowNull]
public string Id { get; set; } public string Id { get; set; }
[AllowNull]
public string Username { get; set; } public string Username { get; set; }
} }

View File

@ -1,31 +1,44 @@
using TwitchChatTTS.Helpers; using TwitchChatTTS.Helpers;
using TwitchChatTTS; using TwitchChatTTS;
using TwitchChatTTS.Hermes;
using System.Text.Json; using System.Text.Json;
using HermesSocketLibrary.Requests.Messages;
using TwitchChatTTS.Hermes;
public class HermesClient { public class HermesApiClient
{
private WebClientWrap _web; private WebClientWrap _web;
public HermesClient(Configuration configuration) { public HermesApiClient(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."); throw new Exception("Ensure you have written your API key in \".token\" file, in the same folder as this application.");
} }
_web = new WebClientWrap(new JsonSerializerOptions() { _web = new WebClientWrap(new JsonSerializerOptions()
{
PropertyNameCaseInsensitive = false, PropertyNameCaseInsensitive = false,
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
}); });
_web.AddHeader("x-api-key", configuration.Hermes.Token); _web.AddHeader("x-api-key", configuration.Hermes.Token);
} }
public async Task<Account> FetchHermesAccountDetails() { public async Task<TTSVersion> GetTTSVersion()
{
var version = await _web.GetJson<TTSVersion>("https://hermes.goblincaves.com/api/info/version");
return version;
}
public async Task<Account> FetchHermesAccountDetails()
{
var account = await _web.GetJson<Account>("https://hermes.goblincaves.com/api/account"); var account = await _web.GetJson<Account>("https://hermes.goblincaves.com/api/account");
if (account == null || account.Id == null || account.Username == null) if (account == null || account.Id == null || account.Username == null)
throw new NullReferenceException("Invalid value found while fetching for hermes account data."); throw new NullReferenceException("Invalid value found while fetching for hermes account data.");
return account; return account;
} }
public async Task<TwitchBotToken> FetchTwitchBotToken() { public async Task<TwitchBotToken> FetchTwitchBotToken()
{
var token = await _web.GetJson<TwitchBotToken>("https://hermes.goblincaves.com/api/token/bot"); var token = await _web.GetJson<TwitchBotToken>("https://hermes.goblincaves.com/api/token/bot");
if (token == null || token.ClientId == null || token.AccessToken == null || token.RefreshToken == null || token.ClientSecret == 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."); throw new Exception("Failed to fetch Twitch API token from Hermes.");
@ -33,7 +46,8 @@ public class HermesClient {
return token; return token;
} }
public async Task<IEnumerable<TTSUsernameFilter>> FetchTTSUsernameFilters() { public async Task<IEnumerable<TTSUsernameFilter>> FetchTTSUsernameFilters()
{
var filters = await _web.GetJson<IEnumerable<TTSUsernameFilter>>("https://hermes.goblincaves.com/api/settings/tts/filter/users"); 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."); throw new Exception("Failed to fetch TTS username filters from Hermes.");
@ -41,23 +55,35 @@ public class HermesClient {
return filters; 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"); {
var data = await _web.GetJson<string>("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."); throw new Exception("Failed to fetch TTS default voice from Hermes.");
return data.Label; return data;
} }
public async Task<IEnumerable<TTSVoice>> FetchTTSEnabledVoices() { public async Task<IEnumerable<TTSChatterSelectedVoice>> FetchTTSChatterSelectedVoices()
var voices = await _web.GetJson<IEnumerable<TTSVoice>>("https://hermes.goblincaves.com/api/settings/tts"); {
var voices = await _web.GetJson<IEnumerable<TTSChatterSelectedVoice>>("https://hermes.goblincaves.com/api/settings/tts/selected");
if (voices == null)
throw new Exception("Failed to fetch TTS chatter selected voices from Hermes.");
return voices;
}
public async Task<IEnumerable<string>> FetchTTSEnabledVoices()
{
var voices = await _web.GetJson<IEnumerable<string>>("https://hermes.goblincaves.com/api/settings/tts");
if (voices == null) if (voices == null)
throw new Exception("Failed to fetch TTS enabled voices from Hermes."); throw new Exception("Failed to fetch TTS enabled voices from Hermes.");
return voices; return voices;
} }
public async Task<IEnumerable<TTSWordFilter>> FetchTTSWordFilters() { public async Task<IEnumerable<TTSWordFilter>> FetchTTSWordFilters()
{
var filters = await _web.GetJson<IEnumerable<TTSWordFilter>>("https://hermes.goblincaves.com/api/settings/tts/filter/words"); 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."); throw new Exception("Failed to fetch TTS word filters from Hermes.");

View File

@ -1,7 +1,7 @@
using CommonSocketLibrary.Abstract; using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common; using CommonSocketLibrary.Common;
using HermesSocketLibrary.Socket.Data; using HermesSocketLibrary.Socket.Data;
using Microsoft.Extensions.Logging; using Serilog;
namespace TwitchChatTTS.Hermes.Socket.Handlers namespace TwitchChatTTS.Hermes.Socket.Handlers
{ {
@ -10,7 +10,8 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
private ILogger _logger { get; } private ILogger _logger { get; }
public int OperationCode { get; set; } = 0; public int OperationCode { get; set; } = 0;
public HeartbeatHandler(ILogger<HeartbeatHandler> logger) { public HeartbeatHandler(ILogger logger)
{
_logger = logger; _logger = logger;
} }
@ -19,16 +20,20 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
if (message is not HeartbeatMessage obj || obj == null) if (message is not HeartbeatMessage obj || obj == null)
return; return;
if (sender is not HermesSocketClient client) { if (sender is not HermesSocketClient client)
{
return; return;
} }
_logger.LogTrace("Received heartbeat."); _logger.Verbose("Received heartbeat.");
client.LastHeartbeat = DateTime.UtcNow; client.LastHeartbeatReceived = DateTime.UtcNow;
await sender.Send(0, new HeartbeatMessage() { if (obj.Respond)
DateTime = DateTime.UtcNow await sender.Send(0, new HeartbeatMessage()
{
DateTime = DateTime.UtcNow,
Respond = false
}); });
} }
} }

View File

@ -1,16 +1,20 @@
using CommonSocketLibrary.Abstract; using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common; using CommonSocketLibrary.Common;
using HermesSocketLibrary.Socket.Data; using HermesSocketLibrary.Socket.Data;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.DependencyInjection;
using Serilog;
namespace TwitchChatTTS.Hermes.Socket.Handlers namespace TwitchChatTTS.Hermes.Socket.Handlers
{ {
public class LoginAckHandler : IWebSocketHandler public class LoginAckHandler : IWebSocketHandler
{ {
private ILogger _logger { get; } private IServiceProvider _serviceProvider;
private ILogger _logger;
public int OperationCode { get; set; } = 2; public int OperationCode { get; set; } = 2;
public LoginAckHandler(ILogger<LoginAckHandler> logger) { public LoginAckHandler(IServiceProvider serviceProvider, ILogger logger)
{
_serviceProvider = serviceProvider;
_logger = logger; _logger = logger;
} }
@ -19,16 +23,44 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
if (message is not LoginAckMessage obj || obj == null) if (message is not LoginAckMessage obj || obj == null)
return; return;
if (sender is not HermesSocketClient client) { if (sender is not HermesSocketClient client)
return; return;
if (obj.AnotherClient)
{
_logger.Warning("Another client has connected to the same account.");
}
else
{
var user = _serviceProvider.GetRequiredService<User>();
client.UserId = obj.UserId;
_logger.Information($"Logged in as {user.TwitchUsername} (id: {client.UserId}).");
} }
if (obj.AnotherClient) { await client.Send(3, new RequestMessage()
_logger.LogWarning("Another client has connected to the same account."); {
} else { Type = "get_tts_voices",
client.UserId = obj.UserId; Data = null
_logger.LogInformation($"Logged in as {client.UserId}."); });
}
var token = _serviceProvider.GetRequiredService<User>();
await client.Send(3, new RequestMessage()
{
Type = "get_tts_users",
Data = new Dictionary<string, object>() { { "user", token.HermesUserId } }
});
await client.Send(3, new RequestMessage()
{
Type = "get_chatter_ids",
Data = null
});
await client.Send(3, new RequestMessage()
{
Type = "get_emotes",
Data = null
});
} }
} }
} }

View File

@ -5,7 +5,8 @@ using CommonSocketLibrary.Common;
using HermesSocketLibrary.Requests.Messages; using HermesSocketLibrary.Requests.Messages;
using HermesSocketLibrary.Socket.Data; using HermesSocketLibrary.Socket.Data;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Serilog;
using TwitchChatTTS.Seven;
namespace TwitchChatTTS.Hermes.Socket.Handlers namespace TwitchChatTTS.Hermes.Socket.Handlers
{ {
@ -14,9 +15,13 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
private readonly IServiceProvider _serviceProvider; private readonly IServiceProvider _serviceProvider;
private readonly JsonSerializerOptions _options; private readonly JsonSerializerOptions _options;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly object _voicesAvailableLock = new object();
public int OperationCode { get; set; } = 4; public int OperationCode { get; set; } = 4;
public RequestAckHandler(IServiceProvider serviceProvider, JsonSerializerOptions options, ILogger<RequestAckHandler> logger) { public RequestAckHandler(IServiceProvider serviceProvider, JsonSerializerOptions options, ILogger logger)
{
_serviceProvider = serviceProvider; _serviceProvider = serviceProvider;
_options = options; _options = options;
_logger = logger; _logger = logger;
@ -32,65 +37,87 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
if (context == null) if (context == null)
return; return;
if (obj.Request.Type == "get_tts_voices") { if (obj.Request.Type == "get_tts_voices")
_logger.LogDebug("Updating all available voices."); {
_logger.Verbose("Updating all available voices for TTS.");
var voices = JsonSerializer.Deserialize<IEnumerable<VoiceDetails>>(obj.Data.ToString(), _options); var voices = JsonSerializer.Deserialize<IEnumerable<VoiceDetails>>(obj.Data.ToString(), _options);
if (voices == null) if (voices == null)
return; return;
lock (_voicesAvailableLock)
{
context.VoicesAvailable = voices.ToDictionary(e => e.Id, e => e.Name); 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.Information("Updated all available voices for TTS.");
_logger.LogDebug("Creating new tts voice."); }
if (!long.TryParse(obj.Request.Data["@user"], out long userId)) else if (obj.Request.Type == "create_tts_user")
{
_logger.Verbose("Adding new tts voice for user.");
if (!long.TryParse(obj.Request.Data["user"].ToString(), out long chatterId))
return; return;
string broadcasterId = obj.Request.Data["@broadcaster"].ToString(); string userId = obj.Request.Data["user"].ToString();
// TODO: validate broadcaster id. string voice = obj.Request.Data["voice"].ToString();
string voice = obj.Request.Data["@voice"].ToString();
context.VoicesSelected.Add(userId, voice); context.VoicesSelected.Add(chatterId, voice);
_logger.LogInformation("Created new tts user."); _logger.Information($"Added new TTS voice [voice: {voice}] for user [user id: {userId}]");
} else if (obj.Request.Type == "update_tts_user") { }
_logger.LogDebug("Updating user's voice"); else if (obj.Request.Type == "update_tts_user")
if (!long.TryParse(obj.Request.Data["@user"], out long userId)) {
_logger.Verbose("Updating user's voice");
if (!long.TryParse(obj.Request.Data["chatter"].ToString(), out long chatterId))
return; return;
string broadcasterId = obj.Request.Data["@broadcaster"].ToString(); string userId = obj.Request.Data["user"].ToString();
string voice = obj.Request.Data["@voice"].ToString(); string voice = obj.Request.Data["voice"].ToString();
context.VoicesSelected[userId] = voice; context.VoicesSelected[chatterId] = voice;
_logger.LogInformation($"Updated user's voice to {voice}."); _logger.Information($"Updated TTS voice [voice: {voice}] for user [user id: {userId}]");
} else if (obj.Request.Type == "create_tts_voice") { }
_logger.LogDebug("Creating new tts voice."); else if (obj.Request.Type == "create_tts_voice")
string? voice = obj.Request.Data["@voice"]; {
_logger.Verbose("Creating new tts voice.");
string? voice = obj.Request.Data["voice"].ToString();
string? voiceId = obj.Data.ToString(); string? voiceId = obj.Data.ToString();
if (voice == null || voiceId == null) if (voice == null || voiceId == null)
return; return;
context.VoicesAvailable.Add(voiceId, voice); lock (_voicesAvailableLock)
_logger.LogInformation($"Created new tts voice named {voice} (id: {voiceId})."); {
} else if (obj.Request.Type == "delete_tts_voice") { var list = context.VoicesAvailable.ToDictionary(k => k.Key, v => v.Value);
_logger.LogDebug("Deleting tts voice."); list.Add(voiceId, voice);
context.VoicesAvailable = list;
var voice = obj.Request.Data["@voice"];
if (!context.VoicesAvailable.TryGetValue(voice, out string voiceName) || voiceName == null) {
return;
} }
_logger.Information($"Created new tts voice [voice: {voice}][id: {voiceId}].");
}
else if (obj.Request.Type == "delete_tts_voice")
{
_logger.Verbose("Deleting tts voice.");
var voice = obj.Request.Data["voice"].ToString();
if (!context.VoicesAvailable.TryGetValue(voice, out string voiceName) || voiceName == null)
return;
lock (_voicesAvailableLock)
{
var dict = context.VoicesAvailable.ToDictionary(k => k.Key, v => v.Value);
dict.Remove(voice);
context.VoicesAvailable.Remove(voice); 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;
} }
_logger.Information($"Deleted a voice [voice: {voiceName}]");
}
else if (obj.Request.Type == "update_tts_voice")
{
_logger.Verbose("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; context.VoicesAvailable[voiceId] = voice;
_logger.LogInformation("Update tts voice: " + voice); _logger.Information($"Updated TTS voice [voice: {voice}][id: {voiceId}]");
} else if (obj.Request.Type == "get_tts_users") { }
_logger.LogDebug("Attempting to update all chatters' selected voice."); else if (obj.Request.Type == "get_tts_users")
{
_logger.Verbose("Updating all chatters' selected voice.");
var users = JsonSerializer.Deserialize<IDictionary<long, string>>(obj.Data.ToString(), _options); var users = JsonSerializer.Deserialize<IDictionary<long, string>>(obj.Data.ToString(), _options);
if (users == null) if (users == null)
return; return;
@ -99,7 +126,55 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
foreach (var entry in users) foreach (var entry in users)
temp.TryAdd(entry.Key, entry.Value); temp.TryAdd(entry.Key, entry.Value);
context.VoicesSelected = temp; context.VoicesSelected = temp;
_logger.LogInformation($"Fetched {temp.Count()} chatters' selected voice."); _logger.Information($"Updated {temp.Count()} chatters' selected voice.");
}
else if (obj.Request.Type == "get_chatter_ids")
{
_logger.Verbose("Fetching all chatters' id.");
var chatters = JsonSerializer.Deserialize<IEnumerable<long>>(obj.Data.ToString(), _options);
if (chatters == null)
return;
var client = _serviceProvider.GetRequiredService<ChatMessageHandler>();
client.Chatters = [.. chatters];
_logger.Information($"Fetched {chatters.Count()} chatters' id.");
}
else if (obj.Request.Type == "get_emotes")
{
_logger.Verbose("Updating emotes.");
var emotes = JsonSerializer.Deserialize<IEnumerable<EmoteInfo>>(obj.Data.ToString(), _options);
if (emotes == null)
return;
var emoteDb = _serviceProvider.GetRequiredService<EmoteDatabase>();
var count = 0;
foreach (var emote in emotes)
{
if (emoteDb.Get(emote.Name) == null)
{
emoteDb.Add(emote.Name, emote.Id);
count++;
}
}
_logger.Information($"Fetched {count} emotes from various sources.");
}
else if (obj.Request.Type == "update_tts_voice_state")
{
_logger.Verbose("Updating TTS voice states.");
string voiceId = obj.Request.Data["voice"].ToString();
bool state = obj.Request.Data["state"].ToString() == "true";
if (!context.VoicesAvailable.TryGetValue(voiceId, out string voiceName) || voiceName == null)
{
_logger.Warning($"Failed to find voice [id: {voiceId}]");
return;
}
if (state)
context.VoicesEnabled.Add(voiceId);
else
context.VoicesEnabled.Remove(voiceId);
_logger.Information($"Updated voice state [voice: {voiceName}][new state: {(state ? "enabled" : "disabled")}]");
} }
} }
} }

View File

@ -1,24 +1,108 @@
using System.Text.Json; using System.Text.Json;
using System.Timers;
using CommonSocketLibrary.Abstract; using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common; using CommonSocketLibrary.Common;
using HermesSocketLibrary.Socket.Data;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Serilog;
namespace TwitchChatTTS.Hermes.Socket namespace TwitchChatTTS.Hermes.Socket
{ {
public class HermesSocketClient : WebSocketClient { public class HermesSocketClient : WebSocketClient
public DateTime LastHeartbeat { get; set; } {
private Configuration _configuration;
public DateTime LastHeartbeatReceived { get; set; }
public DateTime LastHeartbeatSent { get; set; }
public string? UserId { get; set; } public string? UserId { get; set; }
private System.Timers.Timer _heartbeatTimer;
private System.Timers.Timer _reconnectTimer;
public HermesSocketClient( public HermesSocketClient(
ILogger<HermesSocketClient> logger, Configuration configuration,
[FromKeyedServices("hermes")] HandlerManager<WebSocketClient, IWebSocketHandler> handlerManager, [FromKeyedServices("hermes")] HandlerManager<WebSocketClient, IWebSocketHandler> handlerManager,
[FromKeyedServices("hermes")] HandlerTypeManager<WebSocketClient, IWebSocketHandler> typeManager [FromKeyedServices("hermes")] HandlerTypeManager<WebSocketClient, IWebSocketHandler> typeManager,
) : base(logger, handlerManager, typeManager, new JsonSerializerOptions() { ILogger logger
) : base(logger, handlerManager, typeManager, new JsonSerializerOptions()
{
PropertyNameCaseInsensitive = false, PropertyNameCaseInsensitive = false,
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
}) { })
{
_configuration = configuration;
_heartbeatTimer = new System.Timers.Timer(TimeSpan.FromSeconds(15));
_heartbeatTimer.Elapsed += async (sender, e) => await HandleHeartbeat(e);
_reconnectTimer = new System.Timers.Timer(TimeSpan.FromSeconds(15));
_reconnectTimer.Elapsed += async (sender, e) => await Reconnect(e);
LastHeartbeatReceived = LastHeartbeatSent = DateTime.UtcNow;
}
protected override async Task OnConnection()
{
_heartbeatTimer.Enabled = true;
}
private async Task HandleHeartbeat(ElapsedEventArgs e)
{
var signalTime = e.SignalTime.ToUniversalTime();
if (signalTime - LastHeartbeatReceived > TimeSpan.FromSeconds(60))
{
if (LastHeartbeatReceived > LastHeartbeatSent)
{
_logger.Verbose("Sending heartbeat...");
LastHeartbeatSent = DateTime.UtcNow;
try
{
await Send(0, new HeartbeatMessage() { DateTime = LastHeartbeatSent });
}
catch (Exception)
{
}
}
else if (signalTime - LastHeartbeatReceived > TimeSpan.FromSeconds(120))
{
try
{
await DisconnectAsync();
}
catch (Exception)
{
}
UserId = null;
_heartbeatTimer.Enabled = false;
_logger.Information("Logged off due to disconnection. Attempting to reconnect...");
_reconnectTimer.Enabled = true;
}
}
}
private async Task Reconnect(ElapsedEventArgs e)
{
try
{
await ConnectAsync($"wss://hermes-ws.goblincaves.com");
Connected = true;
}
catch (Exception)
{
}
finally
{
if (Connected)
{
_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 });
}
}
} }
} }
} }

View File

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

View File

@ -3,14 +3,14 @@ using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common; using CommonSocketLibrary.Common;
using CommonSocketLibrary.Socket.Manager; using CommonSocketLibrary.Socket.Manager;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Serilog;
namespace TwitchChatTTS.Hermes.Socket.Managers namespace TwitchChatTTS.Hermes.Socket.Managers
{ {
public class HermesHandlerTypeManager : WebSocketHandlerTypeManager public class HermesHandlerTypeManager : WebSocketHandlerTypeManager
{ {
public HermesHandlerTypeManager( public HermesHandlerTypeManager(
ILogger<HermesHandlerTypeManager> factory, ILogger factory,
[FromKeyedServices("hermes")] HandlerManager<WebSocketClient, IWebSocketHandler> handlers [FromKeyedServices("hermes")] HandlerManager<WebSocketClient, IWebSocketHandler> handlers
) : base(factory, handlers) ) : base(factory, handlers)
{ {

10
Hermes/TTSVersion.cs Normal file
View File

@ -0,0 +1,10 @@
namespace TwitchChatTTS.Hermes
{
public class TTSVersion
{
public int MajorVersion { get; set; }
public int MinorVersion { get; set; }
public string Download { get; set; }
public string Changelog { get; set; }
}
}

View File

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

View File

@ -1,17 +0,0 @@
namespace TwitchChatTTS.Hermes
{
public class TTSWordFilter
{
public string? Id { get; set; }
public string? Search { get; set; }
public string? Replace { get; set; }
public string? UserId { get; set; }
public bool IsRegex { get; set; }
public TTSWordFilter() {
IsRegex = true;
}
}
}

View File

@ -1,4 +1,3 @@
[Serializable]
public class TwitchBotAuth { public class TwitchBotAuth {
public string? UserId { get; set; } public string? UserId { get; set; }
public string? AccessToken { get; set; } public string? AccessToken { get; set; }

View File

@ -1,4 +1,3 @@
[Serializable]
public class TwitchBotToken { public class TwitchBotToken {
public string? ClientId { get; set; } public string? ClientId { get; set; }
public string? ClientSecret { get; set; } public string? ClientSecret { get; set; }

View File

@ -1,4 +1,3 @@
[Serializable]
public class TwitchConnection { public class TwitchConnection {
public string? Id { get; set; } public string? Id { get; set; }
public string? Secret { get; set; } public string? Secret { get; set; }

View File

@ -1,6 +1,5 @@
namespace TwitchChatTTS.OBS.Socket.Context namespace TwitchChatTTS.OBS.Socket.Context
{ {
[Serializable]
public class HelloContext public class HelloContext
{ {
public string? Host { get; set; } public string? Host { get; set; }

View File

@ -1,6 +1,5 @@
namespace TwitchChatTTS.OBS.Socket.Data namespace TwitchChatTTS.OBS.Socket.Data
{ {
[Serializable]
public class EventMessage public class EventMessage
{ {
public string EventType { get; set; } public string EventType { get; set; }

View File

@ -1,6 +1,5 @@
namespace TwitchChatTTS.OBS.Socket.Data namespace TwitchChatTTS.OBS.Socket.Data
{ {
[Serializable]
public class HelloMessage public class HelloMessage
{ {
public string ObsWebSocketVersion { get; set; } public string ObsWebSocketVersion { get; set; }

View File

@ -1,6 +1,5 @@
namespace TwitchChatTTS.OBS.Socket.Data namespace TwitchChatTTS.OBS.Socket.Data
{ {
[Serializable]
public class IdentifiedMessage public class IdentifiedMessage
{ {
public int NegotiatedRpcVersion { get; set; } public int NegotiatedRpcVersion { get; set; }

View File

@ -1,13 +1,13 @@
namespace TwitchChatTTS.OBS.Socket.Data namespace TwitchChatTTS.OBS.Socket.Data
{ {
[Serializable]
public class IdentifyMessage public class IdentifyMessage
{ {
public int RpcVersion { get; set; } public int RpcVersion { get; set; }
public string? Authentication { get; set; } public string? Authentication { get; set; }
public int EventSubscriptions { get; set; } public int EventSubscriptions { get; set; }
public IdentifyMessage(int version, string auth, int subscriptions) { public IdentifyMessage(int version, string auth, int subscriptions)
{
RpcVersion = version; RpcVersion = version;
Authentication = auth; Authentication = auth;
EventSubscriptions = subscriptions; EventSubscriptions = subscriptions;

View File

@ -0,0 +1,25 @@
namespace TwitchChatTTS.OBS.Socket.Data
{
public class RequestBatchMessage
{
public string RequestId { get; set; }
public bool HaltOnFailure { get; set; }
public RequestBatchExecutionType ExecutionType { get; set; }
public IEnumerable<object> Requests { get; set;}
public RequestBatchMessage(string id, IEnumerable<object> requests, bool haltOnFailure = false, RequestBatchExecutionType executionType = RequestBatchExecutionType.SerialRealtime)
{
RequestId = id;
Requests = requests;
HaltOnFailure = haltOnFailure;
ExecutionType = executionType;
}
}
public enum RequestBatchExecutionType {
None = -1,
SerialRealtime = 0,
SerialFrame = 1,
Parallel = 2
}
}

View File

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

View File

@ -1,13 +1,13 @@
namespace TwitchChatTTS.OBS.Socket.Data namespace TwitchChatTTS.OBS.Socket.Data
{ {
[Serializable]
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)
{
RequestType = type; RequestType = type;
RequestId = id; RequestId = id;
RequestData = data; RequestData = data;

View File

@ -1,6 +1,5 @@
namespace TwitchChatTTS.OBS.Socket.Data namespace TwitchChatTTS.OBS.Socket.Data
{ {
[Serializable]
public class RequestResponseMessage public class RequestResponseMessage
{ {
public string RequestType { get; set; } public string RequestType { get; set; }

View File

@ -1,19 +1,20 @@
using CommonSocketLibrary.Abstract; using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common; using CommonSocketLibrary.Common;
using Microsoft.Extensions.Logging; using Serilog;
using TwitchChatTTS.OBS.Socket.Data; using TwitchChatTTS.OBS.Socket.Data;
namespace TwitchChatTTS.OBS.Socket.Handlers namespace TwitchChatTTS.OBS.Socket.Handlers
{ {
public class EventMessageHandler : IWebSocketHandler public class EventMessageHandler : IWebSocketHandler
{ {
private ILogger Logger { get; } private ILogger _logger { get; }
private IServiceProvider ServiceProvider { get; } private IServiceProvider _serviceProvider { get; }
public int OperationCode { get; set; } = 5; public int OperationCode { get; set; } = 5;
public EventMessageHandler(ILogger<EventMessageHandler> logger, IServiceProvider serviceProvider) { public EventMessageHandler(ILogger logger, IServiceProvider serviceProvider)
Logger = logger; {
ServiceProvider = serviceProvider; _logger = logger;
_serviceProvider = serviceProvider;
} }
public async Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data message) public async Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data message)
@ -21,7 +22,8 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
if (message is not EventMessage obj || obj == null) if (message is not EventMessage obj || obj == null)
return; return;
switch (obj.EventType) { switch (obj.EventType)
{
case "StreamStateChanged": case "StreamStateChanged":
case "RecordStateChanged": case "RecordStateChanged":
if (sender is not OBSSocketClient client) if (sender is not OBSSocketClient client)
@ -30,19 +32,21 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
string? raw_state = obj.EventData["outputState"].ToString(); string? raw_state = obj.EventData["outputState"].ToString();
string? state = raw_state?.Substring(21).ToLower(); string? state = raw_state?.Substring(21).ToLower();
client.Live = obj.EventData["outputActive"].ToString() == "True"; client.Live = obj.EventData["outputActive"].ToString() == "True";
Logger.LogWarning("Stream " + (state != null && state.EndsWith("ing") ? "is " : "has ") + state + "."); _logger.Warning("Stream " + (state != null && state.EndsWith("ing") ? "is " : "has ") + state + ".");
if (client.Live == false && state != null && !state.EndsWith("ing")) { if (client.Live == false && state != null && !state.EndsWith("ing"))
{
OnStreamEnd(); OnStreamEnd();
} }
break; break;
default: default:
Logger.LogDebug(obj.EventType + " EVENT: " + string.Join(" | ", obj.EventData?.Select(x => x.Key + "=" + x.Value?.ToString()) ?? new string[0])); _logger.Debug(obj.EventType + " EVENT: " + string.Join(" | ", obj.EventData?.Select(x => x.Key + "=" + x.Value?.ToString()) ?? new string[0]));
break; break;
} }
} }
private void OnStreamEnd() { private void OnStreamEnd()
{
} }
} }
} }

View File

@ -2,7 +2,7 @@ using System.Security.Cryptography;
using System.Text; using System.Text;
using CommonSocketLibrary.Abstract; using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common; using CommonSocketLibrary.Common;
using Microsoft.Extensions.Logging; using Serilog;
using TwitchChatTTS.OBS.Socket.Data; using TwitchChatTTS.OBS.Socket.Data;
using TwitchChatTTS.OBS.Socket.Context; using TwitchChatTTS.OBS.Socket.Context;
@ -10,13 +10,14 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
{ {
public class HelloHandler : IWebSocketHandler public class HelloHandler : IWebSocketHandler
{ {
private ILogger Logger { get; } private ILogger _logger { get; }
public int OperationCode { get; set; } = 0; public int OperationCode { get; set; } = 0;
private HelloContext Context { get; } private HelloContext _context { get; }
public HelloHandler(ILogger<HelloHandler> logger, HelloContext context) { public HelloHandler(ILogger logger, HelloContext context)
Logger = logger; {
Context = context; _logger = logger;
_context = context;
} }
public async Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data message) public async Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data message)
@ -24,19 +25,23 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
if (message is not HelloMessage obj || obj == null) if (message is not HelloMessage obj || obj == null)
return; return;
Logger.LogTrace("OBS websocket password: " + Context.Password); _logger.Verbose("OBS websocket password: " + _context.Password);
if (obj.Authentication == null || Context.Password == null) // TODO: send re-identify message. if (obj.Authentication == null || string.IsNullOrWhiteSpace(_context.Password))
{
await sender.Send(1, new IdentifyMessage(obj.RpcVersion, string.Empty, 1023 | 262144));
return; return;
}
var salt = obj.Authentication.Salt; var salt = obj.Authentication.Salt;
var challenge = obj.Authentication.Challenge; var challenge = obj.Authentication.Challenge;
Logger.LogTrace("Salt: " + salt); _logger.Verbose("Salt: " + salt);
Logger.LogTrace("Challenge: " + challenge); _logger.Verbose("Challenge: " + challenge);
string secret = Context.Password + salt; string secret = _context.Password + salt;
byte[] bytes = Encoding.UTF8.GetBytes(secret); byte[] bytes = Encoding.UTF8.GetBytes(secret);
string hash = null; string hash = null;
using (var sha = SHA256.Create()) { using (var sha = SHA256.Create())
{
bytes = sha.ComputeHash(bytes); bytes = sha.ComputeHash(bytes);
hash = Convert.ToBase64String(bytes); hash = Convert.ToBase64String(bytes);
@ -46,7 +51,7 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
hash = Convert.ToBase64String(bytes); hash = Convert.ToBase64String(bytes);
} }
Logger.LogTrace("Final hash: " + hash); _logger.Verbose("Final hash: " + hash);
await sender.Send(1, new IdentifyMessage(obj.RpcVersion, hash, 1023 | 262144)); await sender.Send(1, new IdentifyMessage(obj.RpcVersion, hash, 1023 | 262144));
} }
} }

View File

@ -1,6 +1,6 @@
using CommonSocketLibrary.Abstract; using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common; using CommonSocketLibrary.Common;
using Microsoft.Extensions.Logging; using Serilog;
using TwitchChatTTS.OBS.Socket.Data; using TwitchChatTTS.OBS.Socket.Data;
namespace TwitchChatTTS.OBS.Socket.Handlers namespace TwitchChatTTS.OBS.Socket.Handlers
@ -10,7 +10,8 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
private ILogger Logger { get; } private ILogger Logger { get; }
public int OperationCode { get; set; } = 2; public int OperationCode { get; set; } = 2;
public IdentifiedHandler(ILogger<IdentifiedHandler> logger) { public IdentifiedHandler(ILogger logger)
{
Logger = logger; Logger = logger;
} }
@ -20,7 +21,7 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
return; return;
sender.Connected = true; sender.Connected = true;
Logger.LogInformation("Connected to OBS via rpc version " + obj.NegotiatedRpcVersion + "."); Logger.Information("Connected to OBS via rpc version " + obj.NegotiatedRpcVersion + ".");
} }
} }
} }

View File

@ -0,0 +1,85 @@
using System.Text.Json;
using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common;
using Microsoft.Extensions.DependencyInjection;
using Serilog;
using Serilog.Context;
using TwitchChatTTS.OBS.Socket.Data;
using TwitchChatTTS.OBS.Socket.Manager;
namespace TwitchChatTTS.OBS.Socket.Handlers
{
public class RequestBatchResponseHandler : IWebSocketHandler
{
private OBSRequestBatchManager _manager { get; }
private IServiceProvider _serviceProvider { get; }
private ILogger _logger { get; }
private JsonSerializerOptions _options;
public int OperationCode { get; set; } = 9;
public RequestBatchResponseHandler(OBSRequestBatchManager manager, JsonSerializerOptions options, IServiceProvider serviceProvider, ILogger logger)
{
_manager = manager;
_serviceProvider = serviceProvider;
_logger = logger;
_options = options;
}
public async Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data data)
{
if (data is not RequestBatchResponseMessage message || message == null)
return;
using (LogContext.PushProperty("obsrid", message.RequestId))
{
var results = message.Results.ToList();
_logger.Debug($"Received request batch response of {results.Count} messages.");
var requestData = _manager.Take(message.RequestId);
if (requestData == null || !results.Any())
{
_logger.Verbose($"Received request batch response of {results.Count} messages.");
return;
}
IList<Task> tasks = new List<Task>();
int count = Math.Min(results.Count, requestData.RequestTypes.Count);
for (int i = 0; i < count; i++)
{
Type type = requestData.RequestTypes[i];
using (LogContext.PushProperty("type", type.Name))
{
try
{
var handler = GetResponseHandlerForRequestType(type);
_logger.Verbose($"Request handled by {handler.GetType().Name}.");
tasks.Add(handler.Execute(sender, results[i]));
}
catch (Exception ex)
{
_logger.Error(ex, "Failed to process an item in a request batch message.");
}
}
}
_logger.Verbose($"Waiting for processing to complete.");
await Task.WhenAll(tasks);
_logger.Debug($"Finished processing all request in this batch.");
}
}
private IWebSocketHandler? GetResponseHandlerForRequestType(Type type)
{
if (type == typeof(RequestMessage))
return _serviceProvider.GetRequiredKeyedService<IWebSocketHandler>("obs-requestresponse");
else if (type == typeof(RequestBatchMessage))
return _serviceProvider.GetRequiredKeyedService<IWebSocketHandler>("obs-requestbatcresponse");
else if (type == typeof(IdentifyMessage))
return _serviceProvider.GetRequiredKeyedService<IWebSocketHandler>("obs-identified");
return null;
}
}
}

View File

@ -1,6 +1,6 @@
using CommonSocketLibrary.Abstract; using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common; using CommonSocketLibrary.Common;
using Microsoft.Extensions.Logging; using Serilog;
using TwitchChatTTS.OBS.Socket.Data; using TwitchChatTTS.OBS.Socket.Data;
namespace TwitchChatTTS.OBS.Socket.Handlers namespace TwitchChatTTS.OBS.Socket.Handlers
@ -10,7 +10,8 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
private ILogger Logger { get; } private ILogger Logger { get; }
public int OperationCode { get; set; } = 7; public int OperationCode { get; set; } = 7;
public RequestResponseHandler(ILogger<RequestResponseHandler> logger) { public RequestResponseHandler(ILogger logger)
{
Logger = logger; Logger = logger;
} }
@ -19,14 +20,16 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
if (message is not RequestResponseMessage obj || obj == null) if (message is not RequestResponseMessage obj || obj == null)
return; return;
switch (obj.RequestType) { switch (obj.RequestType)
{
case "GetOutputStatus": case "GetOutputStatus":
if (sender is not OBSSocketClient client) if (sender is not OBSSocketClient client)
return; return;
if (obj.RequestId == "stream") { if (obj.RequestId == "stream")
{
client.Live = obj.ResponseData["outputActive"].ToString() == "True"; client.Live = obj.ResponseData["outputActive"].ToString() == "True";
Logger.LogWarning("Updated stream's live status to " + client.Live); Logger.Warning("Updated stream's live status to " + client.Live);
} }
break; break;
} }

View File

@ -0,0 +1,60 @@
using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common;
using Microsoft.Extensions.DependencyInjection;
using Serilog;
using TwitchChatTTS.OBS.Socket.Data;
namespace TwitchChatTTS.OBS.Socket.Manager
{
public class OBSRequestBatchManager
{
private IDictionary<string, OBSRequestBatchData> _requests;
private IServiceProvider _serviceProvider;
private ILogger _logger;
public OBSRequestBatchManager(IServiceProvider serviceProvider, ILogger logger)
{
_serviceProvider = serviceProvider;
_logger = logger;
}
public async Task Send(long broadcasterId, IEnumerable<WebSocketMessage> messages) {
string uid = GenerateUniqueIdentifier();
var data = new OBSRequestBatchData(broadcasterId, uid, new List<Type>());
_logger.Debug($"Sending request batch of {messages.Count()} messages.");
foreach (WebSocketMessage message in messages)
data.RequestTypes.Add(message.GetType());
var client = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("obs");
await client.Send(8, new RequestBatchMessage(uid, messages));
}
public OBSRequestBatchData? Take(string id) {
if (_requests.TryGetValue(id, out var request)) {
_requests.Remove(id);
return request;
}
return null;
}
private string GenerateUniqueIdentifier()
{
return Guid.NewGuid().ToString("X");
}
}
public class OBSRequestBatchData
{
public long BroadcasterId { get; }
public string RequestId { get; }
public IList<Type> RequestTypes { get; }
public OBSRequestBatchData(long bid, string rid, IList<Type> types) {
BroadcasterId = bid;
RequestId = rid;
RequestTypes = types;
}
}
}

View File

@ -1,4 +1,4 @@
using Microsoft.Extensions.Logging; using Serilog;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using CommonSocketLibrary.Socket.Manager; using CommonSocketLibrary.Socket.Manager;
using CommonSocketLibrary.Common; using CommonSocketLibrary.Common;
@ -7,23 +7,26 @@ namespace TwitchChatTTS.OBS.Socket.Manager
{ {
public class OBSHandlerManager : WebSocketHandlerManager public class OBSHandlerManager : WebSocketHandlerManager
{ {
public OBSHandlerManager(ILogger<OBSHandlerManager> logger, IServiceProvider provider) : base(logger) { public OBSHandlerManager(ILogger logger, IServiceProvider provider) : base(logger)
{
var basetype = typeof(IWebSocketHandler); var basetype = typeof(IWebSocketHandler);
var assembly = GetType().Assembly; var assembly = GetType().Assembly;
var types = assembly.GetTypes().Where(t => t.IsClass && basetype.IsAssignableFrom(t) && t.AssemblyQualifiedName?.Contains(".OBS.") == true); var types = assembly.GetTypes().Where(t => t.IsClass && basetype.IsAssignableFrom(t) && t.AssemblyQualifiedName?.Contains(".OBS.") == true);
foreach (var type in types) { foreach (var type in types)
{
var key = "obs-" + type.Name.Replace("Handlers", "Hand#lers") var key = "obs-" + type.Name.Replace("Handlers", "Hand#lers")
.Replace("Handler", "") .Replace("Handler", "")
.Replace("Hand#lers", "Handlers") .Replace("Hand#lers", "Handlers")
.ToLower(); .ToLower();
var handler = provider.GetKeyedService<IWebSocketHandler>(key); var handler = provider.GetKeyedService<IWebSocketHandler>(key);
if (handler == null) { if (handler == null)
logger.LogError("Failed to find obs websocket handler: " + type.AssemblyQualifiedName); {
logger.Error("Failed to find obs websocket handler: " + type.AssemblyQualifiedName);
continue; continue;
} }
Logger.LogDebug($"Linked type {type.AssemblyQualifiedName} to obs websocket handler {handler.GetType().AssemblyQualifiedName}."); Logger.Debug($"Linked type {type.AssemblyQualifiedName} to obs websocket handler {handler.GetType().AssemblyQualifiedName}.");
Add(handler); Add(handler);
} }
} }

View File

@ -2,14 +2,14 @@ using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common; using CommonSocketLibrary.Common;
using CommonSocketLibrary.Socket.Manager; using CommonSocketLibrary.Socket.Manager;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Serilog;
namespace TwitchChatTTS.OBS.Socket.Manager namespace TwitchChatTTS.OBS.Socket.Manager
{ {
public class OBSHandlerTypeManager : WebSocketHandlerTypeManager public class OBSHandlerTypeManager : WebSocketHandlerTypeManager
{ {
public OBSHandlerTypeManager( public OBSHandlerTypeManager(
ILogger<OBSHandlerTypeManager> factory, ILogger factory,
[FromKeyedServices("obs")] HandlerManager<WebSocketClient, IWebSocketHandler> handlers [FromKeyedServices("obs")] HandlerManager<WebSocketClient, IWebSocketHandler> handlers
) : base(factory, handlers) ) : base(factory, handlers)
{ {

View File

@ -1,29 +1,34 @@
using CommonSocketLibrary.Common; using CommonSocketLibrary.Common;
using CommonSocketLibrary.Abstract; using CommonSocketLibrary.Abstract;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Serilog;
using System.Text.Json; using System.Text.Json;
namespace TwitchChatTTS.OBS.Socket namespace TwitchChatTTS.OBS.Socket
{ {
public class OBSSocketClient : WebSocketClient { public class OBSSocketClient : WebSocketClient
{
private bool _live; private bool _live;
public bool? Live { public bool? Live
{
get => Connected ? _live : null; get => Connected ? _live : null;
set { set
{
if (value.HasValue) if (value.HasValue)
_live = value.Value; _live = value.Value;
} }
} }
public OBSSocketClient( public OBSSocketClient(
ILogger<OBSSocketClient> logger, ILogger logger,
[FromKeyedServices("obs")] HandlerManager<WebSocketClient, IWebSocketHandler> handlerManager, [FromKeyedServices("obs")] HandlerManager<WebSocketClient, IWebSocketHandler> handlerManager,
[FromKeyedServices("obs")] HandlerTypeManager<WebSocketClient, IWebSocketHandler> typeManager [FromKeyedServices("obs")] HandlerTypeManager<WebSocketClient, IWebSocketHandler> typeManager
) : base(logger, handlerManager, typeManager, new JsonSerializerOptions() { ) : base(logger, handlerManager, typeManager, new JsonSerializerOptions()
{
PropertyNameCaseInsensitive = false, PropertyNameCaseInsensitive = false,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase PropertyNamingPolicy = JsonNamingPolicy.CamelCase
}) { })
{
_live = false; _live = false;
} }
} }

View File

@ -1,70 +1,42 @@
using System.Collections.Concurrent;
namespace TwitchChatTTS.Seven namespace TwitchChatTTS.Seven
{ {
public class EmoteCounter { public class EmoteDatabase
public IDictionary<string, IDictionary<long, int>> Counters { get; set; } {
private readonly IDictionary<string, string> _emotes;
public IDictionary<string, string> Emotes { get => _emotes.AsReadOnly(); }
public EmoteCounter() { public EmoteDatabase()
Counters = new ConcurrentDictionary<string, IDictionary<long, int>>(); {
_emotes = new Dictionary<string, string>();
} }
public void Add(long userId, IEnumerable<string> emoteIds) { public void Add(string emoteName, string emoteId)
foreach (var emote in emoteIds) { {
if (Counters.TryGetValue(emote, out IDictionary<long, int>? subcounters)) { if (_emotes.ContainsKey(emoteName))
if (subcounters.TryGetValue(userId, out int counter)) _emotes[emoteName] = emoteId;
subcounters[userId] = counter + 1;
else else
subcounters.Add(userId, 1); _emotes.Add(emoteName, emoteId);
} else {
Counters.Add(emote, new ConcurrentDictionary<long, int>());
Counters[emote].Add(userId, 1);
} }
public void Clear()
{
_emotes.Clear();
}
public string? Get(string emoteName)
{
return _emotes.TryGetValue(emoteName, out string? emoteId) ? emoteId : null;
}
public void Remove(string emoteName)
{
if (_emotes.ContainsKey(emoteName))
_emotes.Remove(emoteName);
} }
} }
public void Clear() { public class EmoteSet
Counters.Clear(); {
}
public int Get(long userId, string emoteId) {
if (Counters.TryGetValue(emoteId, out IDictionary<long, int>? subcounters)) {
if (subcounters.TryGetValue(userId, out int counter))
return counter;
}
return -1;
}
}
public class EmoteDatabase {
private IDictionary<string, string> Emotes { get; }
public EmoteDatabase() {
Emotes = new Dictionary<string, string>();
}
public void Add(string emoteName, string emoteId) {
if (Emotes.ContainsKey(emoteName))
Emotes[emoteName] = emoteId;
else
Emotes.Add(emoteName, emoteId);
}
public void Clear() {
Emotes.Clear();
}
public string? Get(string emoteName) {
return Emotes.TryGetValue(emoteName, out string? emoteId) ? emoteId : null;
}
public void Remove(string emoteName) {
if (Emotes.ContainsKey(emoteName))
Emotes.Remove(emoteName);
}
}
public class EmoteSet {
public string Id { get; set; } public string Id { get; set; }
public string Name { get; set; } public string Name { get; set; }
public int Flags { get; set; } public int Flags { get; set; }
@ -75,7 +47,8 @@ namespace TwitchChatTTS.Seven
public int Capacity { get; set; } public int Capacity { get; set; }
} }
public class Emote { public class Emote
{
public string Id { get; set; } public string Id { get; set; }
public string Name { get; set; } public string Name { get; set; }
public int Flags { get; set; } public int Flags { get; set; }

View File

@ -1,46 +1,59 @@
using System.Text.Json; using System.Text.Json;
using TwitchChatTTS.Helpers; using TwitchChatTTS.Helpers;
using Microsoft.Extensions.Logging; using Serilog;
using TwitchChatTTS.Seven; using TwitchChatTTS.Seven;
using TwitchChatTTS;
public class SevenApiClient { public class SevenApiClient
{
public static readonly string API_URL = "https://7tv.io/v3"; public static readonly string API_URL = "https://7tv.io/v3";
public static readonly string WEBSOCKET_URL = "wss://events.7tv.io/v3"; public static readonly string WEBSOCKET_URL = "wss://events.7tv.io/v3";
private WebClientWrap Web { get; } private WebClientWrap Web { get; }
private ILogger<SevenApiClient> Logger { get; } private ILogger Logger { get; }
private long? Id { get; }
public SevenApiClient(ILogger<SevenApiClient> logger, TwitchBotToken token) { public SevenApiClient(ILogger logger)
{
Logger = logger; Logger = logger;
Id = long.TryParse(token?.BroadcasterId, out long id) ? id : -1; Web = new WebClientWrap(new JsonSerializerOptions()
{
Web = new WebClientWrap(new JsonSerializerOptions() {
PropertyNameCaseInsensitive = false, PropertyNameCaseInsensitive = false,
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
}); });
} }
public async Task<EmoteDatabase?> GetSevenEmotes() { public async Task<EmoteSet?> FetchChannelEmoteSet(string twitchId) {
if (Id == null) try
throw new NullReferenceException(nameof(Id)); {
var details = await Web.GetJson<UserDetails>($"{API_URL}/users/twitch/" + twitchId);
try { return details?.EmoteSet;
var details = await Web.GetJson<UserDetails>($"{API_URL}/users/twitch/" + Id); }
if (details == null) catch (JsonException e)
{
Logger.Error(e, "Failed to fetch emotes from 7tv due to improper JSON.");
}
catch (Exception e)
{
Logger.Error(e, "Failed to fetch emotes from 7tv.");
}
return null; return null;
}
var emotes = new EmoteDatabase(); public async Task<IEnumerable<Emote>?> FetchGlobalSevenEmotes()
if (details.EmoteSet != null) {
foreach (var emote in details.EmoteSet.Emotes) try
emotes.Add(emote.Name, emote.Id); {
Logger.LogInformation($"Loaded {details.EmoteSet?.Emotes.Count() ?? 0} emotes from 7tv."); var emoteSet = await Web.GetJson<EmoteSet>($"{API_URL}/emote-sets/6353512c802a0e34bac96dd2");
return emotes; return emoteSet?.Emotes;
} catch (JsonException e) { }
Logger.LogError(e, "Failed to fetch emotes from 7tv. 2"); catch (JsonException e)
} catch (Exception e) { {
Logger.LogError(e, "Failed to fetch emotes from 7tv."); Logger.Error(e, "Failed to fetch emotes from 7tv due to improper JSON.");
}
catch (Exception e)
{
Logger.Error(e, "Failed to fetch emotes from 7tv.");
} }
return null; return null;
} }

View File

@ -2,7 +2,7 @@ using System.Text.Json;
using CommonSocketLibrary.Abstract; using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common; using CommonSocketLibrary.Common;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Serilog;
using TwitchChatTTS.Seven.Socket.Data; using TwitchChatTTS.Seven.Socket.Data;
namespace TwitchChatTTS.Seven.Socket.Handlers namespace TwitchChatTTS.Seven.Socket.Handlers
@ -11,9 +11,11 @@ namespace TwitchChatTTS.Seven.Socket.Handlers
{ {
private ILogger Logger { get; } private ILogger Logger { get; }
private EmoteDatabase Emotes { get; } private EmoteDatabase Emotes { get; }
private object _lock = new object();
public int OperationCode { get; set; } = 0; public int OperationCode { get; set; } = 0;
public DispatchHandler(ILogger<DispatchHandler> logger, EmoteDatabase emotes) { public DispatchHandler(ILogger logger, EmoteDatabase emotes)
{
Logger = logger; Logger = logger;
Emotes = emotes; Emotes = emotes;
} }
@ -25,30 +27,79 @@ namespace TwitchChatTTS.Seven.Socket.Handlers
ApplyChanges(obj?.Body?.Pulled, cf => cf.OldValue, true); ApplyChanges(obj?.Body?.Pulled, cf => cf.OldValue, true);
ApplyChanges(obj?.Body?.Pushed, cf => cf.Value, false); ApplyChanges(obj?.Body?.Pushed, cf => cf.Value, false);
ApplyChanges(obj?.Body?.Removed, cf => cf.OldValue, true);
ApplyChanges(obj?.Body?.Updated, cf => cf.OldValue, false, cf => cf.Value);
} }
private void ApplyChanges(IEnumerable<ChangeField>? fields, Func<ChangeField, object> getter, bool removing) { private void ApplyChanges(IEnumerable<ChangeField>? fields, Func<ChangeField, object> getter, bool removing, Func<ChangeField, object>? updater = null)
if (fields == null) {
if (fields == null || !fields.Any() || removing && updater != null)
return; return;
foreach (var val in fields) { foreach (var val in fields)
{
var value = getter(val); var value = getter(val);
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,
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
});
if (o == null)
continue;
lock (_lock)
{
if (removing)
{
RemoveEmoteById(o.Id);
Logger.Information($"Removed 7tv emote: {o.Name} (id: {o.Id})");
}
else if (updater != null)
{
RemoveEmoteById(o.Id);
var update = updater(val);
var u = JsonSerializer.Deserialize<EmoteField>(update.ToString(), new JsonSerializerOptions()
{
PropertyNameCaseInsensitive = false, PropertyNameCaseInsensitive = false,
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
}); });
if (removing) { if (u != null)
Emotes.Remove(o.Name); {
Logger.LogInformation($"Removed 7tv emote: {o.Name} (id: {o.Id})"); Emotes.Add(u.Name, u.Id);
} else { Logger.Information($"Updated 7tv emote: from '{o.Name}' to '{u.Name}' (id: {u.Id})");
}
else
{
Logger.Warning("Failed to update 7tv emote.");
}
}
else
{
Emotes.Add(o.Name, o.Id); Emotes.Add(o.Name, o.Id);
Logger.LogInformation($"Added 7tv emote: {o.Name} (id: {o.Id})"); Logger.Information($"Added 7tv emote: {o.Name} (id: {o.Id})");
} }
} }
} }
} }
private void RemoveEmoteById(string id)
{
string? key = null;
foreach (var e in Emotes.Emotes)
{
if (e.Value == id)
{
key = e.Key;
break;
}
}
if (key != null)
Emotes.Remove(key);
}
}
} }

View File

@ -1,7 +1,7 @@
using CommonSocketLibrary.Abstract; using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common; using CommonSocketLibrary.Common;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Serilog;
using TwitchChatTTS.Seven.Socket.Context; using TwitchChatTTS.Seven.Socket.Context;
using TwitchChatTTS.Seven.Socket.Data; using TwitchChatTTS.Seven.Socket.Data;
@ -10,7 +10,7 @@ namespace TwitchChatTTS.Seven.Socket.Handlers
public class EndOfStreamHandler : IWebSocketHandler public class EndOfStreamHandler : IWebSocketHandler
{ {
private ILogger Logger { get; } private ILogger Logger { get; }
private Configuration Configuration { get; } private User User { get; }
private IServiceProvider ServiceProvider { get; } private IServiceProvider ServiceProvider { get; }
private string[] ErrorCodes { get; } private string[] ErrorCodes { get; }
private int[] ReconnectDelay { get; } private int[] ReconnectDelay { get; }
@ -18,9 +18,10 @@ namespace TwitchChatTTS.Seven.Socket.Handlers
public int OperationCode { get; set; } = 7; public int OperationCode { get; set; } = 7;
public EndOfStreamHandler(ILogger<EndOfStreamHandler> logger, Configuration configuration, IServiceProvider serviceProvider) { public EndOfStreamHandler(ILogger logger, User user, IServiceProvider serviceProvider)
{
Logger = logger; Logger = logger;
Configuration = configuration; User = user;
ServiceProvider = serviceProvider; ServiceProvider = serviceProvider;
ErrorCodes = [ ErrorCodes = [
@ -62,34 +63,37 @@ namespace TwitchChatTTS.Seven.Socket.Handlers
var code = obj.Code - 4000; var code = obj.Code - 4000;
if (code >= 0 && code < ErrorCodes.Length) if (code >= 0 && code < ErrorCodes.Length)
Logger.LogWarning($"Received end of stream message (reason: {ErrorCodes[code]}, code: {obj.Code}, message: {obj.Message})."); Logger.Warning($"Received end of stream message (reason: {ErrorCodes[code]}, code: {obj.Code}, message: {obj.Message}).");
else else
Logger.LogWarning($"Received end of stream message (code: {obj.Code}, message: {obj.Message})."); Logger.Warning($"Received end of stream message (code: {obj.Code}, message: {obj.Message}).");
await sender.DisconnectAsync(); await sender.DisconnectAsync();
if (code >= 0 && code < ReconnectDelay.Length && ReconnectDelay[code] < 0) { if (code >= 0 && code < ReconnectDelay.Length && ReconnectDelay[code] < 0)
Logger.LogError($"7tv client will remain disconnected due to a bad client implementation."); {
Logger.Error($"7tv client will remain disconnected due to a bad client implementation.");
return; return;
} }
if (string.IsNullOrWhiteSpace(Configuration.Seven?.UserId)) if (string.IsNullOrWhiteSpace(User.SevenEmoteSetId))
return; return;
var context = ServiceProvider.GetRequiredService<ReconnectContext>(); var context = ServiceProvider.GetRequiredService<ReconnectContext>();
await Task.Delay(ReconnectDelay[code]); await Task.Delay(ReconnectDelay[code]);
//var base_url = "@" + string.Join(",", Configuration.Seven.SevenId.Select(sub => sub.Type + "<" + string.Join(",", sub.Condition?.Select(e => e.Key + "=" + e.Value) ?? new string[0]) + ">")); var base_url = $"@emote_set.*<object_id={User.SevenEmoteSetId}>";
var base_url = $"@emote_set.*<object_id={Configuration.Seven.UserId.Trim()}>";
string url = $"{SevenApiClient.WEBSOCKET_URL}{base_url}"; string url = $"{SevenApiClient.WEBSOCKET_URL}{base_url}";
Logger.LogDebug($"7tv websocket reconnecting to {url}."); Logger.Debug($"7tv websocket reconnecting to {url}.");
await sender.ConnectAsync(url); await sender.ConnectAsync(url);
if (context.SessionId != null) { if (context.SessionId != null)
{
await sender.Send(34, new ResumeMessage() { SessionId = context.SessionId }); await sender.Send(34, new ResumeMessage() { SessionId = context.SessionId });
Logger.LogInformation("Resumed connection to 7tv websocket."); Logger.Information("Resumed connection to 7tv websocket.");
} else { }
Logger.LogDebug("7tv websocket session id not available."); else
{
Logger.Information("Resumed connection to 7tv websocket on a different session.");
} }
} }
} }

View File

@ -1,6 +1,6 @@
using CommonSocketLibrary.Abstract; using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common; using CommonSocketLibrary.Common;
using Microsoft.Extensions.Logging; using Serilog;
using TwitchChatTTS.Seven.Socket.Data; using TwitchChatTTS.Seven.Socket.Data;
namespace TwitchChatTTS.Seven.Socket.Handlers namespace TwitchChatTTS.Seven.Socket.Handlers
@ -10,7 +10,8 @@ namespace TwitchChatTTS.Seven.Socket.Handlers
private ILogger Logger { get; } private ILogger Logger { get; }
public int OperationCode { get; set; } = 6; public int OperationCode { get; set; } = 6;
public ErrorHandler(ILogger<ErrorHandler> logger) { public ErrorHandler(ILogger logger)
{
Logger = logger; Logger = logger;
} }

View File

@ -1,6 +1,6 @@
using CommonSocketLibrary.Abstract; using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common; using CommonSocketLibrary.Common;
using Microsoft.Extensions.Logging; using Serilog;
using TwitchChatTTS.Seven.Socket.Data; using TwitchChatTTS.Seven.Socket.Data;
namespace TwitchChatTTS.Seven.Socket.Handlers namespace TwitchChatTTS.Seven.Socket.Handlers
@ -10,7 +10,8 @@ namespace TwitchChatTTS.Seven.Socket.Handlers
private ILogger Logger { get; } private ILogger Logger { get; }
public int OperationCode { get; set; } = 4; public int OperationCode { get; set; } = 4;
public ReconnectHandler(ILogger<ReconnectHandler> logger) { public ReconnectHandler(ILogger logger)
{
Logger = logger; Logger = logger;
} }
@ -19,7 +20,7 @@ namespace TwitchChatTTS.Seven.Socket.Handlers
if (message is not ReconnectMessage obj || obj == null) if (message is not ReconnectMessage obj || obj == null)
return; return;
Logger.LogInformation($"7tv server wants us to reconnect (reason: {obj.Reason})."); Logger.Information($"7tv server wants us to reconnect (reason: {obj.Reason}).");
} }
} }
} }

View File

@ -1,6 +1,6 @@
using CommonSocketLibrary.Abstract; using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common; using CommonSocketLibrary.Common;
using Microsoft.Extensions.Logging; using Serilog;
using TwitchChatTTS.Seven.Socket.Data; using TwitchChatTTS.Seven.Socket.Data;
namespace TwitchChatTTS.Seven.Socket.Handlers namespace TwitchChatTTS.Seven.Socket.Handlers
@ -11,7 +11,8 @@ namespace TwitchChatTTS.Seven.Socket.Handlers
private Configuration Configuration { get; } private Configuration Configuration { get; }
public int OperationCode { get; set; } = 1; public int OperationCode { get; set; } = 1;
public SevenHelloHandler(ILogger<SevenHelloHandler> logger, Configuration configuration) { public SevenHelloHandler(ILogger logger, Configuration configuration)
{
Logger = logger; Logger = logger;
Configuration = configuration; Configuration = configuration;
} }
@ -26,7 +27,7 @@ namespace TwitchChatTTS.Seven.Socket.Handlers
seven.Connected = true; seven.Connected = true;
seven.ConnectionDetails = obj; seven.ConnectionDetails = obj;
Logger.LogInformation("Connected to 7tv websockets."); Logger.Information("Connected to 7tv websockets.");
} }
} }
} }

View File

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

View File

@ -2,14 +2,14 @@ using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common; using CommonSocketLibrary.Common;
using CommonSocketLibrary.Socket.Manager; using CommonSocketLibrary.Socket.Manager;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Serilog;
namespace TwitchChatTTS.Seven.Socket.Managers namespace TwitchChatTTS.Seven.Socket.Managers
{ {
public class SevenHandlerTypeManager : WebSocketHandlerTypeManager public class SevenHandlerTypeManager : WebSocketHandlerTypeManager
{ {
public SevenHandlerTypeManager( public SevenHandlerTypeManager(
ILogger<SevenHandlerTypeManager> factory, ILogger factory,
[FromKeyedServices("7tv")] HandlerManager<WebSocketClient, [FromKeyedServices("7tv")] HandlerManager<WebSocketClient,
IWebSocketHandler> handlers IWebSocketHandler> handlers
) : base(factory, handlers) ) : base(factory, handlers)

View File

@ -1,23 +1,26 @@
using CommonSocketLibrary.Common; using CommonSocketLibrary.Common;
using CommonSocketLibrary.Abstract; using CommonSocketLibrary.Abstract;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Serilog;
using TwitchChatTTS.Seven.Socket.Data; using TwitchChatTTS.Seven.Socket.Data;
using System.Text.Json; using System.Text.Json;
namespace TwitchChatTTS.Seven.Socket namespace TwitchChatTTS.Seven.Socket
{ {
public class SevenSocketClient : WebSocketClient { public class SevenSocketClient : WebSocketClient
{
public SevenHelloMessage? ConnectionDetails { get; set; } public SevenHelloMessage? ConnectionDetails { get; set; }
public SevenSocketClient( public SevenSocketClient(
ILogger<SevenSocketClient> logger, ILogger logger,
[FromKeyedServices("7tv")] HandlerManager<WebSocketClient, IWebSocketHandler> handlerManager, [FromKeyedServices("7tv")] HandlerManager<WebSocketClient, IWebSocketHandler> handlerManager,
[FromKeyedServices("7tv")] HandlerTypeManager<WebSocketClient, IWebSocketHandler> typeManager [FromKeyedServices("7tv")] HandlerTypeManager<WebSocketClient, IWebSocketHandler> typeManager
) : base(logger, handlerManager, typeManager, new JsonSerializerOptions() { ) : base(logger, handlerManager, typeManager, new JsonSerializerOptions()
{
PropertyNameCaseInsensitive = false, PropertyNameCaseInsensitive = false,
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
}) { })
{
ConnectionDetails = null; ConnectionDetails = null;
} }
} }

View File

@ -8,5 +8,12 @@ namespace TwitchChatTTS.Seven
public int EmoteCapacity { get; set; } public int EmoteCapacity { get; set; }
public int? EmoteSetId { get; set; } public int? EmoteSetId { get; set; }
public EmoteSet EmoteSet { get; set; } public EmoteSet EmoteSet { get; set; }
public SevenUser User { get; set; }
}
public class SevenUser
{
public string Id { get; set; }
public string Username { get; set; }
} }
} }

View File

@ -7,7 +7,6 @@ using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common; using CommonSocketLibrary.Common;
using YamlDotNet.Serialization; using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions; using YamlDotNet.Serialization.NamingConventions;
using Microsoft.Extensions.Logging;
using TwitchChatTTS.Seven.Socket; using TwitchChatTTS.Seven.Socket;
using TwitchChatTTS.OBS.Socket.Handlers; using TwitchChatTTS.OBS.Socket.Handlers;
using TwitchChatTTS.Seven.Socket.Handlers; using TwitchChatTTS.Seven.Socket.Handlers;
@ -26,6 +25,8 @@ using TwitchChatTTS.Hermes.Socket.Managers;
using TwitchChatTTS.Chat.Commands.Parameters; using TwitchChatTTS.Chat.Commands.Parameters;
using TwitchChatTTS.Chat.Commands; using TwitchChatTTS.Chat.Commands;
using System.Text.Json; using System.Text.Json;
using Serilog;
using Serilog.Events;
// dotnet publish -r linux-x64 -p:PublishSingleFile=true --self-contained true // dotnet publish -r linux-x64 -p:PublishSingleFile=true --self-contained true
// dotnet publish -r win-x64 -p:PublishSingleFile=true --self-contained true // dotnet publish -r win-x64 -p:PublishSingleFile=true --self-contained true
@ -41,18 +42,31 @@ var deserializer = new DeserializerBuilder()
var configContent = File.ReadAllText("tts.config.yml"); var configContent = File.ReadAllText("tts.config.yml");
var configuration = deserializer.Deserialize<Configuration>(configContent); var configuration = deserializer.Deserialize<Configuration>(configContent);
var redeemKeys = configuration.Twitch?.Redeems?.Keys; var redeemKeys = configuration.Twitch?.Redeems?.Keys;
if (redeemKeys != null) { if (redeemKeys != null && redeemKeys.Any())
foreach (var key in redeemKeys) { {
if (key != key.ToLower() && configuration.Twitch?.Redeems != null) foreach (var key in redeemKeys)
{
if (key != key.ToLower())
configuration.Twitch.Redeems.Add(key.ToLower(), configuration.Twitch.Redeems[key]); configuration.Twitch.Redeems.Add(key.ToLower(), configuration.Twitch.Redeems[key]);
} }
} }
s.AddSingleton<Configuration>(configuration); s.AddSingleton<Configuration>(configuration);
s.AddLogging(); var logger = new LoggerConfiguration()
#if DEBUG
.MinimumLevel.Debug()
#else
.MinimumLevel.Information()
#endif
.WriteTo.File("logs/log.log", rollingInterval: RollingInterval.Day, retainedFileCountLimit: 7)
.WriteTo.Console(restrictedToMinimumLevel: LogEventLevel.Information)
.CreateLogger();
s.AddSerilog(logger);
s.AddSingleton<User>(new User()); s.AddSingleton<User>(new User());
s.AddSingleton<JsonSerializerOptions>(new JsonSerializerOptions() { s.AddSingleton<JsonSerializerOptions>(new JsonSerializerOptions()
{
PropertyNameCaseInsensitive = false, PropertyNameCaseInsensitive = false,
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
}); });
@ -65,46 +79,38 @@ s.AddKeyedSingleton<ChatCommand, SkipCommand>("command-skip");
s.AddKeyedSingleton<ChatCommand, VoiceCommand>("command-voice"); s.AddKeyedSingleton<ChatCommand, VoiceCommand>("command-voice");
s.AddKeyedSingleton<ChatCommand, AddTTSVoiceCommand>("command-addttsvoice"); s.AddKeyedSingleton<ChatCommand, AddTTSVoiceCommand>("command-addttsvoice");
s.AddKeyedSingleton<ChatCommand, RemoveTTSVoiceCommand>("command-removettsvoice"); s.AddKeyedSingleton<ChatCommand, RemoveTTSVoiceCommand>("command-removettsvoice");
s.AddKeyedSingleton<ChatCommand, RefreshTTSDataCommand>("command-refreshttsdata");
s.AddKeyedSingleton<ChatCommand, OBSCommand>("command-obs");
s.AddKeyedSingleton<ChatCommand, TTSCommand>("command-tts");
s.AddKeyedSingleton<ChatCommand, VersionCommand>("command-version");
s.AddSingleton<ChatCommandManager>(); s.AddSingleton<ChatCommandManager>();
s.AddSingleton<TTSPlayer>(); s.AddSingleton<TTSPlayer>();
s.AddSingleton<ChatMessageHandler>(); s.AddSingleton<ChatMessageHandler>();
s.AddSingleton<HermesClient>(); s.AddSingleton<HermesApiClient>();
s.AddSingleton<TwitchBotToken>(sp => { s.AddSingleton<TwitchBotAuth>(new TwitchBotAuth());
var hermes = sp.GetRequiredService<HermesClient>();
var task = hermes.FetchTwitchBotToken();
task.Wait();
return task.Result;
});
s.AddTransient<IClient, TwitchLib.Communication.Clients.WebSocketClient>(); s.AddTransient<IClient, TwitchLib.Communication.Clients.WebSocketClient>();
s.AddTransient<ITwitchClient, TwitchClient>(); s.AddTransient<ITwitchClient, TwitchClient>();
s.AddTransient<ITwitchPubSub, TwitchPubSub>(); s.AddTransient<ITwitchPubSub, TwitchPubSub>();
s.AddSingleton<TwitchApiClient>(); s.AddSingleton<TwitchApiClient>();
s.AddSingleton<SevenApiClient>(); s.AddSingleton<SevenApiClient>();
s.AddSingleton<EmoteDatabase>(sp => { s.AddSingleton<EmoteDatabase>(new EmoteDatabase());
var api = sp.GetRequiredService<SevenApiClient>();
var task = api.GetSevenEmotes();
task.Wait();
return task.Result;
});
s.AddSingleton<EmoteCounter>(sp => {
if (!string.IsNullOrWhiteSpace(configuration.Emotes?.CounterFilePath) && File.Exists(configuration.Emotes.CounterFilePath.Trim()))
return deserializer.Deserialize<EmoteCounter>(File.ReadAllText(configuration.Emotes.CounterFilePath.Trim()));
return new EmoteCounter();
});
// OBS websocket // OBS websocket
s.AddSingleton<HelloContext>(sp => s.AddSingleton<HelloContext>(sp =>
new HelloContext() { new HelloContext()
{
Host = string.IsNullOrWhiteSpace(configuration.Obs?.Host) ? null : configuration.Obs.Host.Trim(), Host = string.IsNullOrWhiteSpace(configuration.Obs?.Host) ? null : configuration.Obs.Host.Trim(),
Port = configuration.Obs?.Port, Port = configuration.Obs?.Port,
Password = string.IsNullOrWhiteSpace(configuration.Obs?.Password) ? null : configuration.Obs.Password.Trim() Password = string.IsNullOrWhiteSpace(configuration.Obs?.Password) ? null : configuration.Obs.Password.Trim()
} }
); );
s.AddSingleton<OBSRequestBatchManager>();
s.AddKeyedSingleton<IWebSocketHandler, HelloHandler>("obs-hello"); s.AddKeyedSingleton<IWebSocketHandler, HelloHandler>("obs-hello");
s.AddKeyedSingleton<IWebSocketHandler, IdentifiedHandler>("obs-identified"); s.AddKeyedSingleton<IWebSocketHandler, IdentifiedHandler>("obs-identified");
s.AddKeyedSingleton<IWebSocketHandler, RequestResponseHandler>("obs-requestresponse"); s.AddKeyedSingleton<IWebSocketHandler, RequestResponseHandler>("obs-requestresponse");
s.AddKeyedSingleton<IWebSocketHandler, RequestBatchResponseHandler>("obs-requestbatchresponse");
s.AddKeyedSingleton<IWebSocketHandler, EventMessageHandler>("obs-eventmessage"); s.AddKeyedSingleton<IWebSocketHandler, EventMessageHandler>("obs-eventmessage");
s.AddKeyedSingleton<HandlerManager<WebSocketClient, IWebSocketHandler>, OBSHandlerManager>("obs"); s.AddKeyedSingleton<HandlerManager<WebSocketClient, IWebSocketHandler>, OBSHandlerManager>("obs");
@ -112,15 +118,18 @@ s.AddKeyedSingleton<HandlerTypeManager<WebSocketClient, IWebSocketHandler>, OBSH
s.AddKeyedSingleton<SocketClient<WebSocketMessage>, OBSSocketClient>("obs"); s.AddKeyedSingleton<SocketClient<WebSocketMessage>, OBSSocketClient>("obs");
// 7tv websocket // 7tv websocket
s.AddTransient(sp => { s.AddTransient(sp =>
var logger = sp.GetRequiredService<ILogger<ReconnectContext>>(); {
var logger = sp.GetRequiredService<ILogger>();
var client = sp.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("7tv") as SevenSocketClient; var client = sp.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("7tv") as SevenSocketClient;
if (client == null) { if (client == null)
logger.LogError("7tv client == null."); {
logger.Error("7tv client == null.");
return new ReconnectContext() { SessionId = null }; return new ReconnectContext() { SessionId = null };
} }
if (client.ConnectionDetails == null) { if (client.ConnectionDetails == null)
logger.LogError("Connection details in 7tv client == null."); {
logger.Error("Connection details in 7tv client == null.");
return new ReconnectContext() { SessionId = null }; return new ReconnectContext() { SessionId = null };
} }
return new ReconnectContext() { SessionId = client.ConnectionDetails.SessionId }; return new ReconnectContext() { SessionId = client.ConnectionDetails.SessionId };
@ -147,6 +156,5 @@ s.AddKeyedSingleton<HandlerTypeManager<WebSocketClient, IWebSocketHandler>, Herm
s.AddKeyedSingleton<SocketClient<WebSocketMessage>, HermesSocketClient>("hermes"); s.AddKeyedSingleton<SocketClient<WebSocketMessage>, HermesSocketClient>("hermes");
s.AddHostedService<TTS>(); s.AddHostedService<TTS>();
using IHost host = builder.Build(); using IHost host = builder.Build();
await host.RunAsync(); await host.RunAsync();

348
TTS.cs
View File

@ -5,78 +5,98 @@ using CommonSocketLibrary.Common;
using HermesSocketLibrary.Socket.Data; using HermesSocketLibrary.Socket.Data;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Serilog;
using NAudio.Wave.SampleProviders; using NAudio.Wave.SampleProviders;
using TwitchChatTTS.Hermes.Socket; using TwitchChatTTS.Hermes.Socket;
using TwitchChatTTS.Seven;
using TwitchLib.Client.Events; using TwitchLib.Client.Events;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
namespace TwitchChatTTS namespace TwitchChatTTS
{ {
public class TTS : IHostedService public class TTS : IHostedService
{ {
public const int MAJOR_VERSION = 3;
public const int MINOR_VERSION = 3;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly Configuration _configuration; private readonly Configuration _configuration;
private readonly TTSPlayer _player; private readonly TTSPlayer _player;
private readonly IServiceProvider _serviceProvider; private readonly IServiceProvider _serviceProvider;
public TTS(ILogger<TTS> logger, Configuration configuration, TTSPlayer player, IServiceProvider serviceProvider) { public TTS(ILogger logger, Configuration configuration, TTSPlayer player, IServiceProvider serviceProvider)
{
_logger = logger; _logger = logger;
_configuration = configuration; _configuration = configuration;
_player = player; _player = player;
_serviceProvider = serviceProvider; _serviceProvider = serviceProvider;
} }
public async Task StartAsync(CancellationToken cancellationToken) { public async Task StartAsync(CancellationToken cancellationToken)
{
Console.Title = "TTS - Twitch Chat"; Console.Title = "TTS - Twitch Chat";
var user = _serviceProvider.GetRequiredService<User>(); var user = _serviceProvider.GetRequiredService<User>();
var hermes = await InitializeHermes(); var hermes = _serviceProvider.GetRequiredService<HermesApiClient>();
var seven = _serviceProvider.GetRequiredService<SevenApiClient>();
var hermesAccount = await hermes.FetchHermesAccountDetails(); var hermesVersion = await hermes.GetTTSVersion();
user.HermesUserId = hermesAccount.Id; if (hermesVersion.MajorVersion > TTS.MAJOR_VERSION || hermesVersion.MajorVersion == TTS.MAJOR_VERSION && hermesVersion.MinorVersion > TTS.MINOR_VERSION)
user.TwitchUsername = hermesAccount.Username; {
_logger.Information($"A new update for TTS is avaiable! Version {hermesVersion.MajorVersion}.{hermesVersion.MinorVersion} is available at {hermesVersion.Download}");
var changes = hermesVersion.Changelog.Split("\n");
if (changes != null && changes.Any())
_logger.Information("Changelogs:\n - " + string.Join("\n - ", changes) + "\n\n");
await Task.Delay(15 * 1000);
}
var twitchBotToken = await hermes.FetchTwitchBotToken(); try
user.TwitchUserId = long.Parse(twitchBotToken.BroadcasterId); {
_logger.LogInformation($"Username: {user.TwitchUsername} (id: {user.TwitchUserId})"); await FetchUserData(user, hermes, seven);
}
catch (Exception ex)
{
_logger.Error(ex, "Failed to initialize properly.");
await Task.Delay(30 * 1000);
}
user.DefaultTTSVoice = await hermes.FetchTTSDefaultVoice(); var twitchapiclient = await InitializeTwitchApiClient(user.TwitchUsername, user.TwitchUserId.ToString());
_logger.LogInformation("Default Voice: " + user.DefaultTTSVoice); if (twitchapiclient == null)
{
await Task.Delay(30 * 1000);
return;
}
var wordFilters = await hermes.FetchTTSWordFilters(); var emoteSet = await seven.FetchChannelEmoteSet(user.TwitchUserId.ToString());
user.RegexFilters = wordFilters.ToList(); user.SevenEmoteSetId = emoteSet?.Id;
_logger.LogInformation($"{user.RegexFilters.Count()} TTS word filters.");
var usernameFilters = await hermes.FetchTTSUsernameFilters(); await InitializeEmotes(seven, emoteSet);
user.ChatterFilters = usernameFilters.ToDictionary(e => e.Username, e => e); await InitializeHermesWebsocket();
_logger.LogInformation($"{user.ChatterFilters.Where(f => f.Value.Tag == "blacklisted").Count()} username(s) have been blocked."); await InitializeSevenTv(emoteSet.Id);
_logger.LogInformation($"{user.ChatterFilters.Where(f => f.Value.Tag == "priority").Count()} user(s) have been prioritized.");
var twitchapiclient = await InitializeTwitchApiClient(user.TwitchUsername);
await InitializeHermesWebsocket(user);
await InitializeSevenTv();
await InitializeObs(); await InitializeObs();
try { AudioPlaybackEngine.Instance.AddOnMixerInputEnded((object? s, SampleProviderEventArgs e) =>
AudioPlaybackEngine.Instance.AddOnMixerInputEnded((object? s, SampleProviderEventArgs e) => { {
if (e.SampleProvider == _player.Playing) { if (e.SampleProvider == _player.Playing)
{
_player.Playing = null; _player.Playing = null;
} }
}); });
Task.Run(async () => { Task.Run(async () =>
while (true) { {
try { while (true)
if (cancellationToken.IsCancellationRequested) { {
_logger.LogWarning("TTS Buffer - Cancellation token was canceled."); try
{
if (cancellationToken.IsCancellationRequested)
{
_logger.Warning("TTS Buffer - Cancellation token was canceled.");
return; return;
} }
var m = _player.ReceiveBuffer(); var m = _player.ReceiveBuffer();
if (m == null) { if (m == null)
{
await Task.Delay(200); await Task.Delay(200);
continue; continue;
} }
@ -86,182 +106,240 @@ namespace TwitchChatTTS
var provider = new CachedWavProvider(sound); var provider = new CachedWavProvider(sound);
var data = AudioPlaybackEngine.Instance.ConvertSound(provider); var data = AudioPlaybackEngine.Instance.ConvertSound(provider);
var resampled = new WdlResamplingSampleProvider(data, AudioPlaybackEngine.Instance.SampleRate); var resampled = new WdlResamplingSampleProvider(data, AudioPlaybackEngine.Instance.SampleRate);
_logger.LogDebug("Fetched TTS audio data."); _logger.Debug("Fetched TTS audio data.");
m.Audio = resampled; m.Audio = resampled;
_player.Ready(m); _player.Ready(m);
} catch (COMException e) { }
_logger.LogError(e, "Failed to send request for TTS (HResult: " + e.HResult + ")."); catch (COMException e)
} catch (Exception e) { {
_logger.LogError(e, "Failed to send request for TTS."); _logger.Error(e, "Failed to send request for TTS (HResult: " + e.HResult + ").");
}
catch (Exception e)
{
_logger.Error(e, "Failed to send request for TTS.");
} }
} }
}); });
Task.Run(async () => { Task.Run(async () =>
while (true) { {
try { while (true)
if (cancellationToken.IsCancellationRequested) { {
_logger.LogWarning("TTS Queue - Cancellation token was canceled."); try
{
if (cancellationToken.IsCancellationRequested)
{
_logger.Warning("TTS Queue - Cancellation token was canceled.");
return; return;
} }
while (_player.IsEmpty() || _player.Playing != null) { while (_player.IsEmpty() || _player.Playing != null)
{
await Task.Delay(200); await Task.Delay(200);
continue; continue;
} }
var m = _player.ReceiveReady(); var m = _player.ReceiveReady();
if (m == null) { if (m == null)
{
continue; continue;
} }
if (!string.IsNullOrWhiteSpace(m.File) && File.Exists(m.File)) { if (!string.IsNullOrWhiteSpace(m.File) && File.Exists(m.File))
_logger.LogInformation("Playing message: " + m.File); {
_logger.Information("Playing message: " + m.File);
AudioPlaybackEngine.Instance.PlaySound(m.File); AudioPlaybackEngine.Instance.PlaySound(m.File);
continue; continue;
} }
_logger.LogInformation("Playing message: " + m.Message); _logger.Information("Playing message: " + m.Message);
_player.Playing = m.Audio; _player.Playing = m.Audio;
if (m.Audio != null) if (m.Audio != null)
AudioPlaybackEngine.Instance.AddMixerInput(m.Audio); AudioPlaybackEngine.Instance.AddMixerInput(m.Audio);
} catch (Exception e) { }
_logger.LogError(e, "Failed to play a TTS audio message"); catch (Exception e)
{
_logger.Error(e, "Failed to play a TTS audio message");
} }
} }
}); });
StartSavingEmoteCounter(); _logger.Information("Twitch websocket client connecting...");
_logger.LogInformation("Twitch API client connecting...");
await twitchapiclient.Connect(); await twitchapiclient.Connect();
} catch (Exception e) {
_logger.LogError(e, "Failed to initialize.");
}
Console.ReadLine();
} }
public async Task StopAsync(CancellationToken cancellationToken) public async Task StopAsync(CancellationToken cancellationToken)
{ {
if (cancellationToken.IsCancellationRequested) if (cancellationToken.IsCancellationRequested)
_logger.LogWarning("Application has stopped due to cancellation token."); _logger.Warning("Application has stopped due to cancellation token.");
else else
_logger.LogWarning("Application has stopped."); _logger.Warning("Application has stopped.");
} }
private async Task InitializeHermesWebsocket(User user) { private async Task FetchUserData(User user, HermesApiClient hermes, SevenApiClient seven)
if (_configuration.Hermes?.Token == null) { {
_logger.LogDebug("No api token given to hermes. Skipping hermes websockets."); var hermesAccount = await hermes.FetchHermesAccountDetails();
return; if (hermesAccount == null)
throw new Exception("Cannot connect to Hermes. Ensure your token is valid.");
user.HermesUserId = hermesAccount.Id;
user.HermesUsername = hermesAccount.Username;
user.TwitchUsername = hermesAccount.Username;
var twitchBotToken = await hermes.FetchTwitchBotToken();
user.TwitchUserId = long.Parse(twitchBotToken.BroadcasterId);
_logger.Information($"Username: {user.TwitchUsername} (id: {user.TwitchUserId})");
user.DefaultTTSVoice = await hermes.FetchTTSDefaultVoice();
_logger.Information("Default Voice: " + user.DefaultTTSVoice);
var wordFilters = await hermes.FetchTTSWordFilters();
user.RegexFilters = wordFilters.ToList();
_logger.Information($"{user.RegexFilters.Count()} TTS word filters.");
var usernameFilters = await hermes.FetchTTSUsernameFilters();
user.ChatterFilters = usernameFilters.ToDictionary(e => e.Username, e => e);
_logger.Information($"{user.ChatterFilters.Where(f => f.Value.Tag == "blacklisted").Count()} username(s) have been blocked.");
_logger.Information($"{user.ChatterFilters.Where(f => f.Value.Tag == "priority").Count()} user(s) have been prioritized.");
var voicesSelected = await hermes.FetchTTSChatterSelectedVoices();
user.VoicesSelected = voicesSelected.ToDictionary(s => s.ChatterId, s => s.Voice);
_logger.Information($"{user.VoicesSelected.Count} TTS voices have been selected for specific chatters.");
var voicesEnabled = await hermes.FetchTTSEnabledVoices();
if (voicesEnabled == null || !voicesEnabled.Any())
user.VoicesEnabled = new HashSet<string>(new string[] { "Brian" });
else
user.VoicesEnabled = new HashSet<string>(voicesEnabled.Select(v => v));
_logger.Information($"{user.VoicesEnabled.Count} TTS voices have been enabled.");
var defaultedChatters = voicesSelected.Where(item => item.Voice == null || !user.VoicesEnabled.Contains(item.Voice));
_logger.Information($"{defaultedChatters.Count()} chatters will have their TTS voice set to default due to having selected a disabled TTS voice.");
} }
try { private async Task InitializeHermesWebsocket()
_logger.LogInformation("Initializing hermes websocket client."); {
try
{
_logger.Information("Initializing hermes websocket client.");
var hermesClient = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("hermes") as HermesSocketClient; var hermesClient = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("hermes") as HermesSocketClient;
var url = "wss://hermes-ws.goblincaves.com"; var url = "wss://hermes-ws.goblincaves.com";
_logger.LogDebug($"Attempting to connect to {url}"); _logger.Debug($"Attempting to connect to {url}");
await hermesClient.ConnectAsync(url); await hermesClient.ConnectAsync(url);
await hermesClient.Send(1, new HermesLoginMessage() { hermesClient.Connected = true;
await hermesClient.Send(1, new HermesLoginMessage()
{
ApiKey = _configuration.Hermes.Token ApiKey = _configuration.Hermes.Token
}); });
}
while (hermesClient.UserId == null) catch (Exception)
await Task.Delay(TimeSpan.FromMilliseconds(200)); {
_logger.Warning("Connecting to hermes failed. Skipping hermes websockets.");
await hermesClient.Send(3, new RequestMessage() {
Type = "get_tts_voices",
Data = null
});
var token = _serviceProvider.GetRequiredService<TwitchBotToken>();
await hermesClient.Send(3, new RequestMessage() {
Type = "get_tts_users",
Data = new Dictionary<string, string>() { { "@broadcaster", token.BroadcasterId } }
});
} catch (Exception) {
_logger.LogWarning("Connecting to hermes failed. Skipping hermes websockets.");
} }
} }
private async Task InitializeSevenTv() { private async Task InitializeSevenTv(string emoteSetId)
if (_configuration.Seven?.UserId == null) { {
_logger.LogDebug("No user id given to 7tv. Skipping 7tv websockets."); try
return; {
} _logger.Information("Initializing 7tv websocket client.");
try {
_logger.LogInformation("Initializing 7tv websocket client.");
var sevenClient = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("7tv"); var sevenClient = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("7tv");
//var base_url = "@" + string.Join(",", Configuration.Seven.InitialSubscriptions.Select(sub => sub.Type + "<" + string.Join(",", sub.Condition?.Select(e => e.Key + "=" + e.Value) ?? new string[0]) + ">")); if (string.IsNullOrWhiteSpace(emoteSetId))
var url = $"{SevenApiClient.WEBSOCKET_URL}@emote_set.*<object_id={_configuration.Seven.UserId.Trim()}>"; {
_logger.LogDebug($"Attempting to connect to {url}"); _logger.Warning("Could not fetch 7tv emotes.");
return;
}
var url = $"{SevenApiClient.WEBSOCKET_URL}@emote_set.*<object_id={emoteSetId}>";
_logger.Debug($"Attempting to connect to {url}");
await sevenClient.ConnectAsync($"{url}"); await sevenClient.ConnectAsync($"{url}");
} catch (Exception) { }
_logger.LogWarning("Connecting to 7tv failed. Skipping 7tv websockets."); catch (Exception)
{
_logger.Warning("Connecting to 7tv failed. Skipping 7tv websockets.");
} }
} }
private async Task InitializeObs() { private async Task InitializeObs()
if (_configuration.Obs == null || string.IsNullOrWhiteSpace(_configuration.Obs.Host) || !_configuration.Obs.Port.HasValue || _configuration.Obs.Port.Value < 0) { {
_logger.LogDebug("Lacking obs connection info. Skipping obs websockets."); if (_configuration.Obs == null || string.IsNullOrWhiteSpace(_configuration.Obs.Host) || !_configuration.Obs.Port.HasValue || _configuration.Obs.Port.Value < 0)
{
_logger.Warning("Lacking OBS connection info. Skipping OBS websockets.");
return; return;
} }
try { try
_logger.LogInformation("Initializing obs websocket client."); {
var obsClient = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("obs"); var obsClient = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("obs");
var url = $"ws://{_configuration.Obs.Host.Trim()}:{_configuration.Obs.Port}"; var url = $"ws://{_configuration.Obs.Host.Trim()}:{_configuration.Obs.Port}";
_logger.LogDebug($"Attempting to connect to {url}"); _logger.Debug($"Initializing OBS websocket client. Attempting to connect to {url}");
await obsClient.ConnectAsync(url); await obsClient.ConnectAsync(url);
} catch (Exception) { }
_logger.LogWarning("Connecting to obs failed. Skipping obs websockets."); catch (Exception)
{
_logger.Warning("Connecting to obs failed. Skipping obs websockets.");
} }
} }
private async Task<HermesClient> InitializeHermes() { private async Task<TwitchApiClient?> InitializeTwitchApiClient(string username, string broadcasterId)
// Fetch id and username based on api key given. {
_logger.LogInformation("Initializing hermes client."); _logger.Debug("Initializing twitch client.");
var hermes = _serviceProvider.GetRequiredService<HermesClient>();
await hermes.FetchHermesAccountDetails();
return hermes;
}
private async Task<TwitchApiClient> InitializeTwitchApiClient(string username) {
_logger.LogInformation("Initializing twitch client.");
var twitchapiclient = _serviceProvider.GetRequiredService<TwitchApiClient>(); var twitchapiclient = _serviceProvider.GetRequiredService<TwitchApiClient>();
await twitchapiclient.Authorize(); if (!await twitchapiclient.Authorize(broadcasterId))
{
_logger.Error("Cannot connect to Twitch API.");
return null;
}
var channels = _configuration.Twitch.Channels ?? [username]; var channels = _configuration.Twitch.Channels ?? [username];
_logger.LogInformation("Twitch channels: " + string.Join(", ", channels)); _logger.Information("Twitch channels: " + string.Join(", ", channels));
twitchapiclient.InitializeClient(username, channels); twitchapiclient.InitializeClient(username, channels);
twitchapiclient.InitializePublisher(); twitchapiclient.InitializePublisher();
var handler = _serviceProvider.GetRequiredService<ChatMessageHandler>(); var handler = _serviceProvider.GetRequiredService<ChatMessageHandler>();
twitchapiclient.AddOnNewMessageReceived(async Task (object? s, OnMessageReceivedArgs e) => { twitchapiclient.AddOnNewMessageReceived(async (object? s, OnMessageReceivedArgs e) =>
{
try
{
var result = await handler.Handle(e); var result = await handler.Handle(e);
if (result.Status != MessageStatus.None || result.Emotes == null || !result.Emotes.Any())
return;
var ws = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("hermes");
await ws.Send(8, new EmoteUsageMessage()
{
MessageId = e.ChatMessage.Id,
DateTime = DateTime.UtcNow,
BroadcasterId = result.BroadcasterId,
ChatterId = result.ChatterId,
Emotes = result.Emotes
});
}
catch (Exception ex)
{
_logger.Error(ex, "Unable to send emote usage message.");
}
}); });
return twitchapiclient; return twitchapiclient;
} }
private async Task StartSavingEmoteCounter() { private async Task InitializeEmotes(SevenApiClient sevenapi, EmoteSet emoteSet)
Task.Run(async () => {
while (true) {
try {
await Task.Delay(TimeSpan.FromSeconds(300));
var serializer = new SerializerBuilder()
.WithNamingConvention(HyphenatedNamingConvention.Instance)
.Build();
var chathandler = _serviceProvider.GetRequiredService<ChatMessageHandler>();
using (TextWriter writer = File.CreateText(_configuration.Emotes.CounterFilePath.Trim()))
{ {
await writer.WriteAsync(serializer.Serialize(chathandler._emoteCounter)); var emotes = _serviceProvider.GetRequiredService<EmoteDatabase>();
var channelEmotes = emoteSet;
var globalEmotes = await sevenapi.FetchGlobalSevenEmotes();
if (channelEmotes != null && channelEmotes.Emotes.Any())
{
_logger.Information($"Loaded {channelEmotes.Emotes.Count()} 7tv channel emotes.");
foreach (var entry in channelEmotes.Emotes)
emotes.Add(entry.Name, entry.Id);
} }
} catch (Exception e) { if (globalEmotes != null && globalEmotes.Any())
_logger.LogError(e, "Failed to save the emote counter."); {
_logger.Information($"Loaded {globalEmotes.Count()} 7tv global emotes.");
foreach (var entry in globalEmotes)
emotes.Add(entry.Name, entry.Id);
} }
} }
});
}
} }
} }

View File

@ -1,6 +1,6 @@
using System.Text.Json; using System.Text.Json;
using TwitchChatTTS.Helpers; using TwitchChatTTS.Helpers;
using Microsoft.Extensions.Logging; using Serilog;
using TwitchChatTTS; using TwitchChatTTS;
using TwitchLib.Api.Core.Exceptions; using TwitchLib.Api.Core.Exceptions;
using TwitchLib.Client.Events; using TwitchLib.Client.Events;
@ -14,133 +14,164 @@ using TwitchLib.PubSub.Interfaces;
using TwitchLib.Client.Interfaces; using TwitchLib.Client.Interfaces;
using TwitchChatTTS.OBS.Socket; using TwitchChatTTS.OBS.Socket;
public class TwitchApiClient { public class TwitchApiClient
{
private readonly Configuration _configuration; private readonly Configuration _configuration;
private readonly ILogger<TwitchApiClient> _logger; private readonly ILogger _logger;
private readonly TwitchBotToken _token; private TwitchBotAuth _token;
private readonly ITwitchClient _client; private readonly ITwitchClient _client;
private readonly ITwitchPubSub _publisher; private readonly ITwitchPubSub _publisher;
private readonly WebClientWrap Web; private readonly WebClientWrap _web;
private readonly IServiceProvider _serviceProvider; private readonly IServiceProvider _serviceProvider;
private bool Initialized; private bool _initialized;
private string _broadcasterId;
public TwitchApiClient( public TwitchApiClient(
Configuration configuration, Configuration configuration,
ILogger<TwitchApiClient> logger, TwitchBotAuth token,
TwitchBotToken token,
ITwitchClient twitchClient, ITwitchClient twitchClient,
ITwitchPubSub twitchPublisher, ITwitchPubSub twitchPublisher,
IServiceProvider serviceProvider IServiceProvider serviceProvider,
) { ILogger logger
)
{
_configuration = configuration; _configuration = configuration;
_logger = logger;
_token = token; _token = token;
_client = twitchClient; _client = twitchClient;
_publisher = twitchPublisher; _publisher = twitchPublisher;
_serviceProvider = serviceProvider; _serviceProvider = serviceProvider;
Initialized = false; _logger = logger;
_initialized = false;
Web = new WebClientWrap(new JsonSerializerOptions() { _web = new WebClientWrap(new JsonSerializerOptions()
{
PropertyNameCaseInsensitive = false, PropertyNameCaseInsensitive = false,
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
}); });
if (!string.IsNullOrWhiteSpace(_configuration.Hermes?.Token)) if (!string.IsNullOrWhiteSpace(_configuration.Hermes?.Token))
Web.AddHeader("x-api-key", _configuration.Hermes.Token.Trim()); _web.AddHeader("x-api-key", _configuration.Hermes.Token.Trim());
} }
public async Task Authorize() { public async Task<bool> Authorize(string broadcasterId)
try { {
var authorize = await Web.GetJson<TwitchBotAuth>("https://hermes.goblincaves.com/api/account/reauthorize"); try
if (authorize != null && _token.BroadcasterId == authorize.BroadcasterId) { {
var authorize = await _web.GetJson<TwitchBotAuth>("https://hermes.goblincaves.com/api/account/reauthorize");
if (authorize != null && broadcasterId == authorize.BroadcasterId)
{
_token.AccessToken = authorize.AccessToken; _token.AccessToken = authorize.AccessToken;
_token.RefreshToken = authorize.RefreshToken; _token.RefreshToken = authorize.RefreshToken;
_logger.LogInformation("Updated Twitch API tokens."); _token.UserId = authorize.UserId;
} else if (authorize != null) { _token.BroadcasterId = authorize.BroadcasterId;
_logger.LogError("Twitch API Authorization failed."); _logger.Information("Updated Twitch API tokens.");
} }
} catch (HttpResponseException e) { else if (authorize != null)
{
_logger.Error("Twitch API Authorization failed: " + authorize.AccessToken + " | " + authorize.RefreshToken + " | " + authorize.UserId + " | " + authorize.BroadcasterId);
return false;
}
_broadcasterId = broadcasterId;
return true;
}
catch (HttpResponseException e)
{
if (string.IsNullOrWhiteSpace(_configuration.Hermes?.Token)) if (string.IsNullOrWhiteSpace(_configuration.Hermes?.Token))
_logger.LogError("No Hermes API key found. Enter it into the configuration file."); _logger.Error("No Hermes API key found. Enter it into the configuration file.");
else else
_logger.LogError("Invalid Hermes API key. Double check the token. HTTP Error Code: " + e.HttpResponse.StatusCode); _logger.Error("Invalid Hermes API key. Double check the token. HTTP Error Code: " + e.HttpResponse.StatusCode);
} catch (JsonException) {
} catch (Exception e) {
_logger.LogError(e, "Failed to authorize to Twitch API.");
} }
catch (JsonException)
{
}
catch (Exception e)
{
_logger.Error(e, "Failed to authorize to Twitch API.");
}
return false;
} }
public async Task Connect() { public async Task Connect()
{
_client.Connect(); _client.Connect();
await _publisher.ConnectAsync(); await _publisher.ConnectAsync();
} }
public void InitializeClient(string username, IEnumerable<string> channels) { public void InitializeClient(string username, IEnumerable<string> channels)
{
ConnectionCredentials credentials = new ConnectionCredentials(username, _token?.AccessToken); ConnectionCredentials credentials = new ConnectionCredentials(username, _token?.AccessToken);
_client.Initialize(credentials, channels.Distinct().ToList()); _client.Initialize(credentials, channels.Distinct().ToList());
if (Initialized) { if (_initialized)
_logger.LogDebug("Twitch API client has already been initialized."); {
_logger.Debug("Twitch API client has already been initialized.");
return; return;
} }
Initialized = true; _initialized = true;
_client.OnJoinedChannel += async Task (object? s, OnJoinedChannelArgs e) => { _client.OnJoinedChannel += async Task (object? s, OnJoinedChannelArgs e) =>
_logger.LogInformation("Joined channel: " + e.Channel); {
_logger.Information("Joined channel: " + e.Channel);
}; };
_client.OnConnected += async Task (object? s, OnConnectedArgs e) => { _client.OnConnected += async Task (object? s, OnConnectedArgs e) =>
_logger.LogInformation("-----------------------------------------------------------"); {
_logger.Information("-----------------------------------------------------------");
}; };
_client.OnIncorrectLogin += async Task (object? s, OnIncorrectLoginArgs e) => { _client.OnIncorrectLogin += async Task (object? s, OnIncorrectLoginArgs e) =>
_logger.LogError(e.Exception, "Incorrect Login on Twitch API client."); {
_logger.Error(e.Exception, "Incorrect Login on Twitch API client.");
_logger.LogInformation("Attempting to re-authorize."); _logger.Information("Attempting to re-authorize.");
await Authorize(); await Authorize(_broadcasterId);
}; };
_client.OnConnectionError += async Task (object? s, OnConnectionErrorArgs e) => { _client.OnConnectionError += async Task (object? s, OnConnectionErrorArgs e) =>
_logger.LogError("Connection Error: " + e.Error.Message + " (" + e.Error.GetType().Name + ")"); {
_logger.Error("Connection Error: " + e.Error.Message + " (" + e.Error.GetType().Name + ")");
_logger.LogInformation("Attempting to re-authorize."); _logger.Information("Attempting to re-authorize.");
await Authorize(); await Authorize(_broadcasterId);
}; };
_client.OnError += async Task (object? s, OnErrorEventArgs e) => { _client.OnError += async Task (object? s, OnErrorEventArgs e) =>
_logger.LogError(e.Exception, "Twitch API client error."); {
_logger.Error(e.Exception, "Twitch API client error.");
}; };
} }
public void InitializePublisher() { public void InitializePublisher()
_publisher.OnPubSubServiceConnected += async (s, e) => { {
_publisher.OnPubSubServiceConnected += async (s, e) =>
{
_publisher.ListenToChannelPoints(_token.BroadcasterId); _publisher.ListenToChannelPoints(_token.BroadcasterId);
_publisher.ListenToFollows(_token.BroadcasterId); _publisher.ListenToFollows(_token.BroadcasterId);
await _publisher.SendTopicsAsync(_token.AccessToken); await _publisher.SendTopicsAsync(_token.AccessToken);
_logger.LogInformation("Twitch PubSub has been connected."); _logger.Information("Twitch PubSub has been connected.");
}; };
_publisher.OnFollow += (s, e) => { _publisher.OnFollow += (s, e) =>
{
var client = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("obs") as OBSSocketClient; var client = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("obs") as OBSSocketClient;
if (_configuration.Twitch?.TtsWhenOffline != true && client?.Live == false) if (_configuration.Twitch?.TtsWhenOffline != true && client?.Live == false)
return; return;
_logger.LogInformation("Follow: " + e.DisplayName); _logger.Information("Follow: " + e.DisplayName);
}; };
_publisher.OnChannelPointsRewardRedeemed += (s, e) => { _publisher.OnChannelPointsRewardRedeemed += (s, e) =>
{
var client = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("obs") as OBSSocketClient; var client = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("obs") as OBSSocketClient;
if (_configuration.Twitch?.TtsWhenOffline != true && client?.Live == false) if (_configuration.Twitch?.TtsWhenOffline != true && client?.Live == false)
return; return;
_logger.LogInformation($"Channel Point Reward Redeemed: {e.RewardRedeemed.Redemption.Reward.Title} (id: {e.RewardRedeemed.Redemption.Id})"); _logger.Information($"Channel Point Reward Redeemed [redeem: {e.RewardRedeemed.Redemption.Reward.Title}][id: {e.RewardRedeemed.Redemption.Id}]");
if (_configuration.Twitch?.Redeems == null) { if (_configuration.Twitch?.Redeems == null)
_logger.LogDebug("No redeems found in the configuration.");
return; return;
}
var redeemName = e.RewardRedeemed.Redemption.Reward.Title.ToLower().Trim().Replace(" ", "-"); var redeemName = e.RewardRedeemed.Redemption.Reward.Title.ToLower().Trim().Replace(" ", "-");
if (!_configuration.Twitch.Redeems.TryGetValue(redeemName, out RedeemConfiguration? redeem)) if (!_configuration.Twitch.Redeems.TryGetValue(redeemName, out RedeemConfiguration? redeem))
@ -151,16 +182,25 @@ public class TwitchApiClient {
// Write or append to file if needed. // Write or append to file if needed.
var outputFile = string.IsNullOrWhiteSpace(redeem.OutputFilePath) ? null : redeem.OutputFilePath.Trim(); var outputFile = string.IsNullOrWhiteSpace(redeem.OutputFilePath) ? null : redeem.OutputFilePath.Trim();
if (outputFile == null) { if (outputFile == null)
_logger.LogDebug($"No output file was provided for redeem '{e.RewardRedeemed.Redemption.Reward.Title}'."); {
} else { _logger.Debug($"No output file was provided for redeem [redeem: {e.RewardRedeemed.Redemption.Reward.Title}][id: {e.RewardRedeemed.Redemption.Id}]");
}
else
{
var outputContent = string.IsNullOrWhiteSpace(redeem.OutputContent) ? null : redeem.OutputContent.Trim().Replace("%USER%", e.RewardRedeemed.Redemption.User.DisplayName).Replace("\\n", "\n"); var outputContent = string.IsNullOrWhiteSpace(redeem.OutputContent) ? null : redeem.OutputContent.Trim().Replace("%USER%", e.RewardRedeemed.Redemption.User.DisplayName).Replace("\\n", "\n");
if (outputContent == null) { if (outputContent == null)
_logger.LogWarning($"No output content was provided for redeem '{e.RewardRedeemed.Redemption.Reward.Title}'."); {
} else { _logger.Warning($"No output content was provided for redeem [redeem: {e.RewardRedeemed.Redemption.Reward.Title}][id: {e.RewardRedeemed.Redemption.Id}]");
if (redeem.OutputAppend == true) { }
else
{
if (redeem.OutputAppend == true)
{
File.AppendAllText(outputFile, outputContent + "\n"); File.AppendAllText(outputFile, outputContent + "\n");
} else { }
else
{
File.WriteAllText(outputFile, outputContent); File.WriteAllText(outputFile, outputContent);
} }
} }
@ -168,40 +208,23 @@ public class TwitchApiClient {
// Play audio file if needed. // Play audio file if needed.
var audioFile = string.IsNullOrWhiteSpace(redeem.AudioFilePath) ? null : redeem.AudioFilePath.Trim(); var audioFile = string.IsNullOrWhiteSpace(redeem.AudioFilePath) ? null : redeem.AudioFilePath.Trim();
if (audioFile == null) { if (audioFile == null)
_logger.LogDebug($"No audio file was provided for redeem '{e.RewardRedeemed.Redemption.Reward.Title}'."); {
return; _logger.Debug($"No audio file was provided for redeem [redeem: {e.RewardRedeemed.Redemption.Reward.Title}][id: {e.RewardRedeemed.Redemption.Id}]");
} }
if (!File.Exists(audioFile)) { else if (!File.Exists(audioFile))
_logger.LogWarning($"Cannot find audio file @ {audioFile} for redeem '{e.RewardRedeemed.Redemption.Reward.Title}'."); {
return; _logger.Warning($"Cannot find audio file [location: {audioFile}] for redeem [redeem: {e.RewardRedeemed.Redemption.Reward.Title}][id: {e.RewardRedeemed.Redemption.Id}]");
} }
else
{
AudioPlaybackEngine.Instance.PlaySound(audioFile); AudioPlaybackEngine.Instance.PlaySound(audioFile);
}
}; };
/*int psConnectionFailures = 0;
publisher.OnPubSubServiceError += async (s, e) => {
Console.WriteLine("PubSub ran into a service error. Attempting to connect again.");
await Task.Delay(Math.Min(3000 + (1 << psConnectionFailures), 120000));
var connect = await WebHelper.Get("https://hermes.goblincaves.com/api/account/reauthorize");
if ((int) connect.StatusCode == 200 || (int) connect.StatusCode == 201) {
psConnectionFailures = 0;
} else {
psConnectionFailures++;
} }
var twitchBotData2 = await WebHelper.GetJson<TwitchBotToken>("https://hermes.goblincaves.com/api/token/bot"); public void AddOnNewMessageReceived(AsyncEventHandler<OnMessageReceivedArgs> handler)
if (twitchBotData2 == null) { {
Console.WriteLine("The API is down. Contact the owner.");
return;
}
twitchBotData.access_token = twitchBotData2.access_token;
await pubsub.ConnectAsync();
};*/
}
public void AddOnNewMessageReceived(AsyncEventHandler<OnMessageReceivedArgs> handler) {
_client.OnMessageReceived += handler; _client.OnMessageReceived += handler;
} }
} }

View File

@ -10,11 +10,21 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="6.0.0" /> <PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Configuration" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" />
<PackageReference Include="NAudio" Version="2.2.1" /> <PackageReference Include="NAudio" Version="2.2.1" />
<PackageReference Include="NAudio.Extras" Version="2.2.1" /> <PackageReference Include="NAudio.Extras" Version="2.2.1" />
<PackageReference Include="Serilog" Version="4.0.0" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.2-dev-00338" />
<PackageReference Include="Serilog.Enrichers.Thread" Version="4.0.0" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.1-dev-10391" />
<PackageReference Include="Serilog.Formatting.Compact" Version="3.0.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="8.0.1" />
<PackageReference Include="Serilog.Sinks.Async" Version="2.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.Debug" Version="3.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.1-dev-00972" />
<PackageReference Include="Serilog.Sinks.RollingFile" Version="3.3.1-dev-00771" />
<PackageReference Include="Serilog.Sinks.Trace" Version="4.0.0" />
<PackageReference Include="System.Text.Json" Version="8.0.1" /> <PackageReference Include="System.Text.Json" Version="8.0.1" />
<PackageReference Include="System.Threading.Tasks.Dataflow" Version="8.0.0" /> <PackageReference Include="System.Threading.Tasks.Dataflow" Version="8.0.0" />
<PackageReference Include="TwitchLib.Api.Core" Version="3.10.0-preview-e47ba7f" /> <PackageReference Include="TwitchLib.Api.Core" Version="3.10.0-preview-e47ba7f" />

24
User.cs
View File

@ -1,5 +1,6 @@
using System.Text.Json.Serialization;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using TwitchChatTTS.Hermes; using HermesSocketLibrary.Requests.Messages;
namespace TwitchChatTTS namespace TwitchChatTTS
{ {
@ -7,29 +8,38 @@ namespace TwitchChatTTS
{ {
// Hermes user id // Hermes user id
public string HermesUserId { get; set; } public string HermesUserId { get; set; }
public string HermesUsername { get; set; }
public long TwitchUserId { get; set; } public long TwitchUserId { get; set; }
public string TwitchUsername { get; set; } public string TwitchUsername { get; set; }
public string SevenEmoteSetId { get; set; }
public string DefaultTTSVoice { get; set; } public string DefaultTTSVoice { get; set; }
// voice id -> voice name // voice id -> voice name
public IDictionary<string, string> VoicesAvailable { get; set; } public IDictionary<string, string> VoicesAvailable { get => _voicesAvailable; set { _voicesAvailable = value; WordFilterRegex = GenerateEnabledVoicesRegex(); } }
// chatter/twitch id -> voice name // chatter/twitch id -> voice name
public IDictionary<long, string> VoicesSelected { get; set; } public IDictionary<long, string> VoicesSelected { get; set; }
public HashSet<string> VoicesEnabled { get; set; } // voice names
public HashSet<string> VoicesEnabled { get => _voicesEnabled; set { _voicesEnabled = value; WordFilterRegex = GenerateEnabledVoicesRegex(); } }
public IDictionary<string, TTSUsernameFilter> ChatterFilters { get; set; } public IDictionary<string, TTSUsernameFilter> ChatterFilters { get; set; }
public IList<TTSWordFilter> RegexFilters { get; set; } public IList<TTSWordFilter> RegexFilters { get; set; }
[JsonIgnore]
public Regex? WordFilterRegex { get; set; }
private IDictionary<string, string> _voicesAvailable;
private HashSet<string> _voicesEnabled;
public User() { public User()
{
} }
public Regex? GenerateEnabledVoicesRegex() { private Regex? GenerateEnabledVoicesRegex()
{
if (VoicesAvailable == null || VoicesAvailable.Count() <= 0) if (VoicesAvailable == null || VoicesAvailable.Count() <= 0)
return null; return null;
var enabledVoicesString = string.Join("|", VoicesAvailable.Select(v => v.Value)); var enabledVoicesString = string.Join("|", VoicesAvailable.Where(v => VoicesEnabled == null || !VoicesEnabled.Any() || VoicesEnabled.Contains(v.Value)).Select(v => v.Value));
return new Regex($@"\b({enabledVoicesString})\:(.*?)(?=\Z|\b(?:{enabledVoicesString})\:)", RegexOptions.IgnoreCase); return new Regex($@"\b({enabledVoicesString})\:(.*?)(?=\Z|\b(?:{enabledVoicesString})\:)", RegexOptions.IgnoreCase);
} }
} }