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

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

1
.gitignore vendored
View File

@ -2,3 +2,4 @@ appsettings.json
tts.config.yml tts.config.yml
obj/ obj/
bin/ bin/
logs/

View File

@ -1,17 +1,16 @@
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using TwitchLib.Client.Events; using TwitchLib.Client.Events;
using TwitchChatTTS.OBS.Socket;
using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common;
using Serilog; using Serilog;
using Microsoft.Extensions.DependencyInjection;
using TwitchChatTTS; using TwitchChatTTS;
using TwitchChatTTS.Seven;
using TwitchChatTTS.Chat.Commands; using TwitchChatTTS.Chat.Commands;
using TwitchChatTTS.Hermes.Socket; using TwitchChatTTS.Hermes.Socket;
using HermesSocketLibrary.Socket.Data;
using TwitchChatTTS.Chat.Groups.Permissions; using TwitchChatTTS.Chat.Groups.Permissions;
using TwitchChatTTS.Chat.Groups; using TwitchChatTTS.Chat.Groups;
using TwitchChatTTS.OBS.Socket.Manager;
using TwitchChatTTS.Chat.Emotes;
using Microsoft.Extensions.DependencyInjection;
using CommonSocketLibrary.Common;
using CommonSocketLibrary.Abstract;
public class ChatMessageHandler public class ChatMessageHandler
@ -21,14 +20,14 @@ public class ChatMessageHandler
private readonly ChatCommandManager _commands; private readonly ChatCommandManager _commands;
private readonly IGroupPermissionManager _permissionManager; private readonly IGroupPermissionManager _permissionManager;
private readonly IChatterGroupManager _chatterGroupManager; private readonly IChatterGroupManager _chatterGroupManager;
private readonly EmoteDatabase _emotes; private readonly IEmoteDatabase _emotes;
private readonly OBSSocketClient? _obsClient; private readonly OBSManager _obsManager;
private readonly HermesSocketClient? _hermesClient; private readonly HermesSocketClient _hermes;
private readonly Configuration _configuration; private readonly Configuration _configuration;
private readonly ILogger _logger; private readonly ILogger _logger;
private Regex sfxRegex; private Regex _sfxRegex;
private HashSet<long> _chatters; private HashSet<long> _chatters;
public HashSet<long> Chatters { get => _chatters; set => _chatters = value; } public HashSet<long> Chatters { get => _chatters; set => _chatters = value; }
@ -40,9 +39,9 @@ public class ChatMessageHandler
ChatCommandManager commands, ChatCommandManager commands,
IGroupPermissionManager permissionManager, IGroupPermissionManager permissionManager,
IChatterGroupManager chatterGroupManager, IChatterGroupManager chatterGroupManager,
EmoteDatabase emotes, IEmoteDatabase emotes,
[FromKeyedServices("obs")] SocketClient<WebSocketMessage> obsClient, OBSManager obsManager,
[FromKeyedServices("hermes")] SocketClient<WebSocketMessage> hermesClient, [FromKeyedServices("hermes")] SocketClient<WebSocketMessage> hermes,
Configuration configuration, Configuration configuration,
ILogger logger ILogger logger
) )
@ -53,41 +52,41 @@ public class ChatMessageHandler
_permissionManager = permissionManager; _permissionManager = permissionManager;
_chatterGroupManager = chatterGroupManager; _chatterGroupManager = chatterGroupManager;
_emotes = emotes; _emotes = emotes;
_obsClient = obsClient as OBSSocketClient; _obsManager = obsManager;
_hermesClient = hermesClient as HermesSocketClient; _hermes = (hermes as HermesSocketClient)!;
_configuration = configuration; _configuration = configuration;
_logger = logger; _logger = logger;
_chatters = new HashSet<long>(); _chatters = new HashSet<long>();
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 (_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 m = e.ChatMessage; var m = e.ChatMessage;
if (!_hermes.Ready)
{
_logger.Debug($"TTS is not yet ready. Ignoring chat messages [message id: {m.Id}]");
return new MessageResult(MessageStatus.NotReady, -1, -1);
}
if (_configuration.Twitch?.TtsWhenOffline != true && !_obsManager.Streaming)
{
_logger.Debug($"OBS is not streaming. Ignoring chat messages [message id: {m.Id}]");
return new MessageResult(MessageStatus.NotReady, -1, -1);
}
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 tasks = new List<Task>();
var permissionPath = "tts.chat.messages.read";
if (!string.IsNullOrWhiteSpace(m.CustomRewardId))
permissionPath = "tts.chat.redemptions.read";
var checks = new bool[] { true, m.IsSubscriber, m.IsVip, m.IsModerator, m.IsBroadcaster }; var checks = new bool[] { true, m.IsSubscriber, m.IsVip, m.IsModerator, m.IsBroadcaster };
var defaultGroups = new string[] { "everyone", "subscribers", "vip", "moderators", "broadcaster" }; var defaultGroups = new string[] { "everyone", "subscribers", "vip", "moderators", "broadcaster" };
var customGroups = _chatterGroupManager.GetGroupNamesFor(chatterId); var customGroups = _chatterGroupManager.GetGroupNamesFor(chatterId);
var groups = defaultGroups.Where((e, i) => checks[i]).Union(customGroups); var groups = defaultGroups.Where((e, i) => checks[i]).Union(customGroups);
var permission = chatterId == _user.OwnerId ? true : _permissionManager.CheckIfAllowed(groups, permissionPath);
var blocked = permission != true;
if (!blocked || m.IsBroadcaster)
{
try try
{ {
var commandResult = await _commands.Execute(msg, m, groups); var commandResult = await _commands.Execute(msg, m, groups);
@ -98,21 +97,21 @@ public class ChatMessageHandler
{ {
_logger.Error(ex, $"Failed executing a chat command [message: {msg}][chatter: {m.Username}][chatter id: {m.UserId}][message id: {m.Id}]"); _logger.Error(ex, $"Failed executing a chat command [message: {msg}][chatter: {m.Username}][chatter id: {m.UserId}][message id: {m.Id}]");
} }
}
if (blocked) var permissionPath = "tts.chat.messages.read";
if (!string.IsNullOrWhiteSpace(m.CustomRewardId))
permissionPath = "tts.chat.redemptions.read";
var permission = chatterId == _user.OwnerId ? true : _permissionManager.CheckIfAllowed(groups, permissionPath);
if (permission != true)
{ {
_logger.Debug($"Blocked message by {m.Username}: {msg}"); _logger.Debug($"Blocked message by {m.Username}: {msg}");
return new MessageResult(MessageStatus.Blocked, -1, -1); return new MessageResult(MessageStatus.Blocked, -1, -1);
} }
if (_obsClient.Connected && !_chatters.Contains(chatterId)) if (_obsManager.Streaming && !_chatters.Contains(chatterId))
{ {
tasks.Add(_hermesClient.Send(6, new ChatterMessage() tasks.Add(_hermes.SendChatterDetails(chatterId, m.Username));
{
Id = chatterId,
Name = m.Username
}));
_chatters.Add(chatterId); _chatters.Add(chatterId);
} }
@ -149,11 +148,8 @@ 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()) if (_obsManager.Streaming && newEmotes.Any())
tasks.Add(_hermesClient.Send(7, new EmoteDetailsMessage() tasks.Add(_hermes.SendEmoteDetails(newEmotes));
{
Emotes = newEmotes
}));
msg = filteredMsg; msg = filteredMsg;
// Replace filtered words. // Replace filtered words.
@ -231,7 +227,7 @@ public class ChatMessageHandler
return; return;
var m = e.ChatMessage; var m = e.ChatMessage;
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)
@ -251,7 +247,7 @@ public class ChatMessageHandler
return; return;
} }
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++)

View File

@ -1,4 +1,4 @@
namespace TwitchChatTTS.Seven namespace TwitchChatTTS.Chat
{ {
public class ChatterDatabase public class ChatterDatabase
{ {

View File

@ -1,9 +1,7 @@
using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common;
using HermesSocketLibrary.Socket.Data;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Serilog; using Serilog;
using TwitchChatTTS.Chat.Commands.Parameters; using TwitchChatTTS.Chat.Commands.Parameters;
using TwitchChatTTS.Hermes.Socket;
using TwitchLib.Client.Models; using TwitchLib.Client.Models;
namespace TwitchChatTTS.Chat.Commands namespace TwitchChatTTS.Chat.Commands
@ -11,48 +9,41 @@ namespace TwitchChatTTS.Chat.Commands
public class AddTTSVoiceCommand : ChatCommand public class AddTTSVoiceCommand : ChatCommand
{ {
private readonly User _user; private readonly User _user;
private readonly SocketClient<WebSocketMessage> _hermesClient;
private readonly ILogger _logger; private readonly ILogger _logger;
public new bool DefaultPermissionsOverwrite { get => true; } public new bool DefaultPermissionsOverwrite { get => true; }
public AddTTSVoiceCommand( public AddTTSVoiceCommand(
User user, User user,
[FromKeyedServices("parameter-unvalidated")] ChatCommandParameter ttsVoiceParameter, [FromKeyedServices("parameter-unvalidated")] ChatCommandParameter unvalidatedParameter,
[FromKeyedServices("hermes")] SocketClient<WebSocketMessage> hermesClient,
ILogger 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.")
{ {
_user = user; _user = user;
_hermesClient = hermesClient;
_logger = logger; _logger = logger;
AddParameter(ttsVoiceParameter); AddParameter(unvalidatedParameter);
} }
public override async Task<bool> CheckDefaultPermissions(ChatMessage message, long broadcasterId) public override async Task<bool> CheckDefaultPermissions(ChatMessage message)
{ {
return false; return false;
} }
public override async Task Execute(IList<string> args, ChatMessage message, long broadcasterId) public override async Task Execute(IList<string> args, ChatMessage message, HermesSocketClient client)
{ {
if (_hermesClient == null)
return;
if (_user == null || _user.VoicesAvailable == null) if (_user == null || _user.VoicesAvailable == null)
return; return;
var voiceName = args.First(); var voiceName = args.First();
var voiceNameLower = voiceName.ToLower(); var voiceNameLower = voiceName.ToLower();
var exists = _user.VoicesAvailable.Any(v => v.Value.ToLower() == voiceNameLower); var exists = _user.VoicesAvailable.Any(v => v.Value.ToLower() == voiceNameLower);
if (exists) if (exists) {
_logger.Information("Voice already exists.");
return; return;
}
await _hermesClient.Send(3, new RequestMessage() await client.CreateTTSVoice(voiceName);
{
Type = "create_tts_voice",
Data = new Dictionary<string, object>() { { "voice", voiceName } }
});
_logger.Information($"Added a new TTS voice by {message.Username} [voice: {voiceName}][id: {message.UserId}]"); _logger.Information($"Added a new TTS voice by {message.Username} [voice: {voiceName}][id: {message.UserId}]");
} }
} }

View File

@ -1,4 +1,5 @@
using TwitchChatTTS.Chat.Commands.Parameters; using TwitchChatTTS.Chat.Commands.Parameters;
using TwitchChatTTS.Hermes.Socket;
using TwitchLib.Client.Models; using TwitchLib.Client.Models;
namespace TwitchChatTTS.Chat.Commands namespace TwitchChatTTS.Chat.Commands
@ -27,7 +28,7 @@ namespace TwitchChatTTS.Chat.Commands
} }
} }
public abstract Task<bool> CheckDefaultPermissions(ChatMessage message, long broadcasterId); public abstract Task<bool> CheckDefaultPermissions(ChatMessage message);
public abstract Task Execute(IList<string> args, ChatMessage message, long broadcasterId); public abstract Task Execute(IList<string> args, ChatMessage message, HermesSocketClient client);
} }
} }

View File

@ -1,8 +1,10 @@
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Serilog; using Serilog;
using TwitchChatTTS.Chat.Groups;
using TwitchChatTTS.Chat.Groups.Permissions; using TwitchChatTTS.Chat.Groups.Permissions;
using TwitchChatTTS.Hermes.Socket;
using TwitchLib.Client.Models; using TwitchLib.Client.Models;
namespace TwitchChatTTS.Chat.Commands namespace TwitchChatTTS.Chat.Commands
@ -10,28 +12,25 @@ namespace TwitchChatTTS.Chat.Commands
public class ChatCommandManager public class ChatCommandManager
{ {
private IDictionary<string, ChatCommand> _commands; private IDictionary<string, ChatCommand> _commands;
private readonly TwitchBotAuth _token;
private readonly User _user; private readonly User _user;
private readonly HermesSocketClient _hermes;
private readonly IGroupPermissionManager _permissionManager; private readonly IGroupPermissionManager _permissionManager;
private readonly IChatterGroupManager _chatterGroupManager;
private readonly IServiceProvider _serviceProvider; private readonly IServiceProvider _serviceProvider;
private readonly ILogger _logger; private readonly ILogger _logger;
private string CommandStartSign { get; } = "!"; private string CommandStartSign { get; } = "!";
public ChatCommandManager( public ChatCommandManager(
TwitchBotAuth token,
User user, User user,
[FromKeyedServices("hermes")] SocketClient<WebSocketMessage> socketClient,
IGroupPermissionManager permissionManager, IGroupPermissionManager permissionManager,
IChatterGroupManager chatterGroupManager,
IServiceProvider serviceProvider, IServiceProvider serviceProvider,
ILogger logger ILogger logger
) )
{ {
_token = token;
_user = user; _user = user;
_hermes = (socketClient as HermesSocketClient)!;
_permissionManager = permissionManager; _permissionManager = permissionManager;
_chatterGroupManager = chatterGroupManager;
_serviceProvider = serviceProvider; _serviceProvider = serviceProvider;
_logger = logger; _logger = logger;
@ -71,8 +70,6 @@ namespace TwitchChatTTS.Chat.Commands
public async Task<ChatCommandResult> Execute(string arg, ChatMessage message, IEnumerable<string> groups) public async Task<ChatCommandResult> Execute(string arg, ChatMessage message, IEnumerable<string> groups)
{ {
if (_token.BroadcasterId == null)
return ChatCommandResult.Unknown;
if (string.IsNullOrWhiteSpace(arg)) if (string.IsNullOrWhiteSpace(arg))
return ChatCommandResult.Unknown; return ChatCommandResult.Unknown;
@ -88,7 +85,6 @@ namespace TwitchChatTTS.Chat.Commands
.ToArray(); .ToArray();
string com = parts.First().Substring(CommandStartSign.Length).ToLower(); string com = parts.First().Substring(CommandStartSign.Length).ToLower();
string[] args = parts.Skip(1).ToArray(); string[] args = parts.Skip(1).ToArray();
long broadcasterId = long.Parse(_token.BroadcasterId);
if (!_commands.TryGetValue(com, out ChatCommand? command) || command == null) if (!_commands.TryGetValue(com, out ChatCommand? command) || command == null)
{ {
@ -107,7 +103,7 @@ namespace TwitchChatTTS.Chat.Commands
_logger.Debug($"Denied permission to use command [chatter id: {chatterId}][command: {com}]"); _logger.Debug($"Denied permission to use command [chatter id: {chatterId}][command: {com}]");
return ChatCommandResult.Permission; return ChatCommandResult.Permission;
} }
else if (executable == null && !await command.CheckDefaultPermissions(message, broadcasterId)) else if (executable == null && !await command.CheckDefaultPermissions(message))
{ {
_logger.Debug($"Chatter is missing default permission to execute command named '{com}' [args: {arg}][chatter: {message.Username}][chatter id: {message.UserId}]"); _logger.Debug($"Chatter is missing default permission to execute command named '{com}' [args: {arg}][chatter: {message.Username}][chatter id: {message.UserId}]");
return ChatCommandResult.Permission; return ChatCommandResult.Permission;
@ -132,7 +128,7 @@ namespace TwitchChatTTS.Chat.Commands
try try
{ {
await command.Execute(args, message, broadcasterId); await command.Execute(args, message, _hermes);
} }
catch (Exception e) catch (Exception e)
{ {

View File

@ -3,6 +3,7 @@ using CommonSocketLibrary.Common;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Serilog; using Serilog;
using TwitchChatTTS.Chat.Commands.Parameters; using TwitchChatTTS.Chat.Commands.Parameters;
using TwitchChatTTS.Hermes.Socket;
using TwitchChatTTS.OBS.Socket.Data; using TwitchChatTTS.OBS.Socket.Data;
using TwitchChatTTS.OBS.Socket.Manager; using TwitchChatTTS.OBS.Socket.Manager;
using TwitchLib.Client.Models; using TwitchLib.Client.Models;
@ -19,7 +20,6 @@ namespace TwitchChatTTS.Chat.Commands
[FromKeyedServices("parameter-unvalidated")] ChatCommandParameter unvalidatedParameter, [FromKeyedServices("parameter-unvalidated")] ChatCommandParameter unvalidatedParameter,
User user, User user,
OBSManager manager, OBSManager manager,
[FromKeyedServices("obs")] SocketClient<WebSocketMessage> hermesClient,
ILogger logger ILogger logger
) : base("obs", "Various obs commands.") ) : base("obs", "Various obs commands.")
{ {
@ -28,14 +28,17 @@ namespace TwitchChatTTS.Chat.Commands
_logger = logger; _logger = logger;
AddParameter(unvalidatedParameter); AddParameter(unvalidatedParameter);
AddParameter(unvalidatedParameter, optional: true);
AddParameter(unvalidatedParameter, optional: true);
AddParameter(unvalidatedParameter, optional: true);
} }
public override async Task<bool> CheckDefaultPermissions(ChatMessage message, long broadcasterId) public override async Task<bool> CheckDefaultPermissions(ChatMessage message)
{ {
return message.IsModerator || message.IsBroadcaster; 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, HermesSocketClient client)
{ {
if (_user == null || _user.VoicesAvailable == null) if (_user == null || _user.VoicesAvailable == null)
return; return;

View File

@ -0,0 +1,17 @@
namespace TwitchChatTTS.Chat.Commands.Parameters
{
public class SimpleListedParameter : ChatCommandParameter
{
private readonly string[] _values;
public SimpleListedParameter(string[] possibleValues, bool optional = false) : base("TTS Voice Name", "Name of a TTS voice", optional)
{
_values = possibleValues;
}
public override bool Validate(string value)
{
return _values.Contains(value.ToLower());
}
}
}

View File

@ -1,6 +1,9 @@
using Microsoft.Extensions.DependencyInjection;
using Serilog; using Serilog;
using TwitchChatTTS.Chat.Commands.Parameters;
using TwitchChatTTS.Chat.Groups; using TwitchChatTTS.Chat.Groups;
using TwitchChatTTS.Chat.Groups.Permissions; using TwitchChatTTS.Chat.Groups.Permissions;
using TwitchChatTTS.Hermes.Socket;
using TwitchChatTTS.OBS.Socket.Manager; using TwitchChatTTS.OBS.Socket.Manager;
using TwitchChatTTS.Twitch.Redemptions; using TwitchChatTTS.Twitch.Redemptions;
using TwitchLib.Client.Models; using TwitchLib.Client.Models;
@ -34,20 +37,28 @@ namespace TwitchChatTTS.Chat.Commands
_obsManager = obsManager; _obsManager = obsManager;
_hermesApi = hermesApi; _hermesApi = hermesApi;
_logger = logger; _logger = logger;
AddParameter(new SimpleListedParameter([
"tts_voice_enabled",
"word_filters",
"selected_voices",
"default_voice",
"redemptions",
"obs_cache",
"permissions"
]));
} }
public override async Task<bool> CheckDefaultPermissions(ChatMessage message, long broadcasterId) public override async Task<bool> CheckDefaultPermissions(ChatMessage message)
{ {
return message.IsModerator || message.IsBroadcaster; 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, HermesSocketClient client)
{ {
var value = args.FirstOrDefault(); var value = args.First().ToLower();
if (value == null)
return;
switch (value.ToLower()) switch (value)
{ {
case "tts_voice_enabled": case "tts_voice_enabled":
var voicesEnabled = await _hermesApi.FetchTTSEnabledVoices(); var voicesEnabled = await _hermesApi.FetchTTSEnabledVoices();
@ -62,12 +73,6 @@ namespace TwitchChatTTS.Chat.Commands
_user.RegexFilters = wordFilters.ToList(); _user.RegexFilters = wordFilters.ToList();
_logger.Information($"{_user.RegexFilters.Count()} TTS word filters."); _logger.Information($"{_user.RegexFilters.Count()} TTS word filters.");
break; break;
case "username_filters":
var usernameFilters = await _hermesApi.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 "selected_voices": case "selected_voices":
{ {
var voicesSelected = await _hermesApi.FetchTTSChatterSelectedVoices(); var voicesSelected = await _hermesApi.FetchTTSChatterSelectedVoices();
@ -86,16 +91,9 @@ namespace TwitchChatTTS.Chat.Commands
_logger.Information($"Redemption Manager has been refreshed with {redemptionActions.Count()} actions & {redemptions.Count()} redemptions."); _logger.Information($"Redemption Manager has been refreshed with {redemptionActions.Count()} actions & {redemptions.Count()} redemptions.");
break; break;
case "obs_cache": case "obs_cache":
{
try
{ {
_obsManager.ClearCache(); _obsManager.ClearCache();
await _obsManager.GetGroupList(async groups => await _obsManager.GetGroupSceneItemList(groups)); await _obsManager.GetGroupList(async groups => await _obsManager.GetGroupSceneItemList(groups));
}
catch (Exception e)
{
_logger.Error(e, "Failed to load OBS group info via command.");
}
break; break;
} }
case "permissions": case "permissions":

View File

@ -1,9 +1,7 @@
using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common;
using HermesSocketLibrary.Socket.Data;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Serilog; using Serilog;
using TwitchChatTTS.Chat.Commands.Parameters; using TwitchChatTTS.Chat.Commands.Parameters;
using TwitchChatTTS.Hermes.Socket;
using TwitchLib.Client.Models; using TwitchLib.Client.Models;
namespace TwitchChatTTS.Chat.Commands namespace TwitchChatTTS.Chat.Commands
@ -11,7 +9,6 @@ namespace TwitchChatTTS.Chat.Commands
public class RemoveTTSVoiceCommand : ChatCommand public class RemoveTTSVoiceCommand : ChatCommand
{ {
private readonly User _user; private readonly User _user;
private readonly SocketClient<WebSocketMessage> _hermesClient;
private ILogger _logger; private ILogger _logger;
public new bool DefaultPermissionsOverwrite { get => true; } public new bool DefaultPermissionsOverwrite { get => true; }
@ -19,39 +16,39 @@ namespace TwitchChatTTS.Chat.Commands
public RemoveTTSVoiceCommand( public RemoveTTSVoiceCommand(
[FromKeyedServices("parameter-unvalidated")] ChatCommandParameter ttsVoiceParameter, [FromKeyedServices("parameter-unvalidated")] ChatCommandParameter ttsVoiceParameter,
User user, User user,
[FromKeyedServices("hermes")] SocketClient<WebSocketMessage> hermesClient,
ILogger 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.")
{ {
_user = user; _user = user;
_hermesClient = hermesClient;
_logger = logger; _logger = logger;
AddParameter(ttsVoiceParameter); AddParameter(ttsVoiceParameter);
} }
public override async Task<bool> CheckDefaultPermissions(ChatMessage message, long broadcasterId) public override async Task<bool> CheckDefaultPermissions(ChatMessage message)
{ {
return false; return false;
} }
public override async Task Execute(IList<string> args, ChatMessage message, long broadcasterId) public override async Task Execute(IList<string> args, ChatMessage message, HermesSocketClient client)
{ {
if (_user == null || _user.VoicesAvailable == null) if (_user == null || _user.VoicesAvailable == null)
{
_logger.Debug($"Voices available are not loaded [chatter: {message.Username}][chatter id: {message.UserId}]");
return; return;
}
var voiceName = args.First().ToLower(); var voiceName = args.First().ToLower();
var exists = _user.VoicesAvailable.Any(v => v.Value.ToLower() == voiceName); var exists = _user.VoicesAvailable.Any(v => v.Value.ToLower() == voiceName);
if (!exists) if (!exists)
{
_logger.Debug($"Voice does not exist [voice: {voiceName}][chatter: {message.Username}][chatter id: {message.UserId}]");
return; return;
}
var voiceId = _user.VoicesAvailable.FirstOrDefault(v => v.Value.ToLower() == voiceName).Key; var voiceId = _user.VoicesAvailable.FirstOrDefault(v => v.Value.ToLower() == voiceName).Key;
await _hermesClient.Send(3, new RequestMessage() await client.DeleteTTSVoice(voiceId);
{ _logger.Information($"Deleted a TTS voice [voice: {voiceName}][chatter: {message.Username}][chatter id: {message.UserId}]");
Type = "delete_tts_voice",
Data = new Dictionary<string, object>() { { "voice", voiceId } }
});
_logger.Information($"Deleted a TTS voice [voice: {voiceName}][invoker: {message.Username}][id: {message.UserId}]");
} }
} }
} }

View File

@ -1,4 +1,5 @@
using Serilog; using Serilog;
using TwitchChatTTS.Hermes.Socket;
using TwitchLib.Client.Models; using TwitchLib.Client.Models;
namespace TwitchChatTTS.Chat.Commands namespace TwitchChatTTS.Chat.Commands
@ -15,12 +16,12 @@ namespace TwitchChatTTS.Chat.Commands
_logger = logger; _logger = logger;
} }
public override async Task<bool> CheckDefaultPermissions(ChatMessage message, long broadcasterId) public override async Task<bool> CheckDefaultPermissions(ChatMessage message)
{ {
return message.IsModerator || message.IsVip || message.IsBroadcaster; return message.IsModerator || message.IsVip || message.IsBroadcaster;
} }
public override async Task Execute(IList<string> args, ChatMessage message, long broadcasterId) public override async Task Execute(IList<string> args, ChatMessage message, HermesSocketClient client)
{ {
_ttsPlayer.RemoveAll(); _ttsPlayer.RemoveAll();

View File

@ -1,4 +1,5 @@
using Serilog; using Serilog;
using TwitchChatTTS.Hermes.Socket;
using TwitchLib.Client.Models; using TwitchLib.Client.Models;
namespace TwitchChatTTS.Chat.Commands namespace TwitchChatTTS.Chat.Commands
@ -15,12 +16,12 @@ namespace TwitchChatTTS.Chat.Commands
_logger = logger; _logger = logger;
} }
public override async Task<bool> CheckDefaultPermissions(ChatMessage message, long broadcasterId) public override async Task<bool> CheckDefaultPermissions(ChatMessage message)
{ {
return message.IsModerator || message.IsVip || message.IsBroadcaster; return message.IsModerator || message.IsVip || message.IsBroadcaster;
} }
public override async Task Execute(IList<string> args, ChatMessage message, long broadcasterId) public override async Task Execute(IList<string> args, ChatMessage message, HermesSocketClient client)
{ {
if (_ttsPlayer.Playing == null) if (_ttsPlayer.Playing == null)
return; return;

View File

@ -1,9 +1,7 @@
using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common;
using HermesSocketLibrary.Socket.Data;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Serilog; using Serilog;
using TwitchChatTTS.Chat.Commands.Parameters; using TwitchChatTTS.Chat.Commands.Parameters;
using TwitchChatTTS.Hermes.Socket;
using TwitchLib.Client.Models; using TwitchLib.Client.Models;
namespace TwitchChatTTS.Chat.Commands namespace TwitchChatTTS.Chat.Commands
@ -11,31 +9,27 @@ namespace TwitchChatTTS.Chat.Commands
public class TTSCommand : ChatCommand public class TTSCommand : ChatCommand
{ {
private readonly User _user; private readonly User _user;
private readonly SocketClient<WebSocketMessage> _hermesClient;
private readonly ILogger _logger; private readonly ILogger _logger;
public TTSCommand( public TTSCommand(
[FromKeyedServices("parameter-ttsvoicename")] ChatCommandParameter ttsVoiceParameter, [FromKeyedServices("parameter-ttsvoicename")] ChatCommandParameter ttsVoiceParameter,
[FromKeyedServices("parameter-unvalidated")] ChatCommandParameter unvalidatedParameter,
User user, User user,
[FromKeyedServices("hermes")] SocketClient<WebSocketMessage> hermesClient,
ILogger logger ILogger logger
) : base("tts", "Various tts commands.") ) : base("tts", "Various tts commands.")
{ {
_user = user; _user = user;
_hermesClient = hermesClient;
_logger = logger; _logger = logger;
AddParameter(ttsVoiceParameter); AddParameter(ttsVoiceParameter);
AddParameter(unvalidatedParameter); AddParameter(new SimpleListedParameter(["enable", "disable"]));
} }
public override async Task<bool> CheckDefaultPermissions(ChatMessage message, long broadcasterId) public override async Task<bool> CheckDefaultPermissions(ChatMessage message)
{ {
return message.IsModerator || message.IsBroadcaster; 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, HermesSocketClient client)
{ {
if (_user == null || _user.VoicesAvailable == null) if (_user == null || _user.VoicesAvailable == null)
return; return;
@ -44,25 +38,9 @@ namespace TwitchChatTTS.Chat.Commands
var voiceId = _user.VoicesAvailable.FirstOrDefault(v => v.Value.ToLower() == voiceName).Key; var voiceId = _user.VoicesAvailable.FirstOrDefault(v => v.Value.ToLower() == voiceName).Key;
var action = args[1].ToLower(); var action = args[1].ToLower();
switch (action) bool state = action == "enable";
{ await client.UpdateTTSVoiceState(voiceId, state);
case "enable": _logger.Information($"Changed state for TTS voice [voice: {voiceName}][state: {state}][invoker: {message.Username}][id: {message.UserId}]");
await _hermesClient.Send(3, new RequestMessage()
{
Type = "update_tts_voice_state",
Data = new Dictionary<string, object>() { { "voice", voiceId }, { "state", true } }
});
_logger.Information($"Enabled a TTS voice [voice: {voiceName}][invoker: {message.Username}][id: {message.UserId}]");
break;
case "disable":
await _hermesClient.Send(3, new RequestMessage()
{
Type = "update_tts_voice_state",
Data = new Dictionary<string, object>() { { "voice", voiceId }, { "state", false } }
});
_logger.Information($"Disabled a TTS voice [voice: {voiceName}][invoker: {message.Username}][id: {message.UserId}]");
break;
}
} }
} }
} }

View File

@ -1,26 +1,32 @@
using HermesSocketLibrary.Socket.Data;
using Serilog; using Serilog;
using TwitchChatTTS.Hermes.Socket;
using TwitchLib.Client.Models; using TwitchLib.Client.Models;
namespace TwitchChatTTS.Chat.Commands namespace TwitchChatTTS.Chat.Commands
{ {
public class VersionCommand : ChatCommand public class VersionCommand : ChatCommand
{ {
private readonly User _user;
private ILogger _logger; private ILogger _logger;
public VersionCommand(ILogger logger) public VersionCommand(User user, ILogger logger)
: base("version", "Does nothing.") : base("version", "Does nothing.")
{ {
_user = user;
_logger = logger; _logger = logger;
} }
public override async Task<bool> CheckDefaultPermissions(ChatMessage message, long broadcasterId) public override async Task<bool> CheckDefaultPermissions(ChatMessage message)
{ {
return message.IsBroadcaster; return message.IsBroadcaster;
} }
public override async Task Execute(IList<string> args, ChatMessage message, long broadcasterId) public override async Task Execute(IList<string> args, ChatMessage message, HermesSocketClient client)
{ {
_logger.Information($"Version: {TTS.MAJOR_VERSION}.{TTS.MINOR_VERSION}"); _logger.Information($"Version: {TTS.MAJOR_VERSION}.{TTS.MINOR_VERSION}");
await client.SendLoggingMessage(HermesLoggingLevel.Info, $"{_user.TwitchUsername} [twitch id: {_user.TwitchUserId}] using version {TTS.MAJOR_VERSION}.{TTS.MINOR_VERSION}.");
} }
} }
} }

View File

@ -1,9 +1,7 @@
using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common;
using HermesSocketLibrary.Socket.Data;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Serilog; using Serilog;
using TwitchChatTTS.Chat.Commands.Parameters; using TwitchChatTTS.Chat.Commands.Parameters;
using TwitchChatTTS.Hermes.Socket;
using TwitchLib.Client.Models; using TwitchLib.Client.Models;
namespace TwitchChatTTS.Chat.Commands namespace TwitchChatTTS.Chat.Commands
@ -11,29 +9,26 @@ namespace TwitchChatTTS.Chat.Commands
public class VoiceCommand : ChatCommand public class VoiceCommand : ChatCommand
{ {
private readonly User _user; private readonly User _user;
private readonly SocketClient<WebSocketMessage> _hermesClient;
private readonly ILogger _logger; private readonly ILogger _logger;
public VoiceCommand( public VoiceCommand(
[FromKeyedServices("parameter-ttsvoicename")] ChatCommandParameter ttsVoiceParameter, [FromKeyedServices("parameter-ttsvoicename")] ChatCommandParameter ttsVoiceParameter,
User user, User user,
[FromKeyedServices("hermes")] SocketClient<WebSocketMessage> hermesClient,
ILogger 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.")
{ {
_user = user; _user = user;
_hermesClient = hermesClient;
_logger = logger; _logger = logger;
AddParameter(ttsVoiceParameter); AddParameter(ttsVoiceParameter);
} }
public override async Task<bool> CheckDefaultPermissions(ChatMessage message, long broadcasterId) public override async Task<bool> CheckDefaultPermissions(ChatMessage message)
{ {
return message.IsModerator || message.IsBroadcaster || message.IsSubscriber || message.Bits >= 100; 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, HermesSocketClient client)
{ {
if (_user == null || _user.VoicesSelected == null || _user.VoicesEnabled == null) if (_user == null || _user.VoicesSelected == null || _user.VoicesEnabled == null)
return; return;
@ -43,14 +38,21 @@ namespace TwitchChatTTS.Chat.Commands
var voice = _user.VoicesAvailable.First(v => v.Value.ToLower() == voiceName); var voice = _user.VoicesAvailable.First(v => v.Value.ToLower() == voiceName);
var enabled = _user.VoicesEnabled.Contains(voice.Value); var enabled = _user.VoicesEnabled.Contains(voice.Value);
if (enabled) if (!enabled)
{ {
await _hermesClient.Send(3, new RequestMessage() _logger.Information($"Voice is disabled. Cannot switch to that voice [voice: {voice.Value}][username: {message.Username}]");
return;
}
if (_user.VoicesSelected.ContainsKey(chatterId))
{ {
Type = _user.VoicesSelected.ContainsKey(chatterId) ? "update_tts_user" : "create_tts_user", await client.UpdateTTSUser(chatterId, voice.Key);
Data = new Dictionary<string, object>() { { "chatter", chatterId }, { "voice", voice.Key } } _logger.Debug($"Sent request to create chat TTS voice [voice: {voice.Value}][username: {message.Username}][reason: command]");
}); }
_logger.Debug($"Sent request to update chat TTS voice [voice: {voice.Value}][username: {message.Username}]."); else
{
await client.CreateTTSUser(chatterId, voice.Key);
_logger.Debug($"Sent request to update chat TTS voice [voice: {voice.Value}][username: {message.Username}][reason: command]");
} }
} }
} }

View File

@ -1,6 +1,6 @@
namespace TwitchChatTTS.Seven namespace TwitchChatTTS.Chat.Emotes
{ {
public class EmoteDatabase public class EmoteDatabase : IEmoteDatabase
{ {
private readonly IDictionary<string, string> _emotes; private readonly IDictionary<string, string> _emotes;
public IDictionary<string, string> Emotes { get => _emotes.AsReadOnly(); } public IDictionary<string, string> Emotes { get => _emotes.AsReadOnly(); }

View File

@ -0,0 +1,10 @@
namespace TwitchChatTTS.Chat.Emotes
{
public interface IEmoteDatabase
{
void Add(string emoteName, string emoteId);
void Clear();
string? Get(string emoteName);
void Remove(string emoteName);
}
}

View File

@ -15,7 +15,7 @@ namespace TwitchChatTTS
public class TwitchConfiguration { public class TwitchConfiguration {
public IEnumerable<string>? Channels; public IEnumerable<string>? Channels;
public bool? TtsWhenOffline; public bool TtsWhenOffline;
} }
public class OBSConfiguration { public class OBSConfiguration {

View File

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

View File

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

View File

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

View File

@ -2,17 +2,21 @@ using System.Collections.Concurrent;
using System.Text.Json; using System.Text.Json;
using CommonSocketLibrary.Abstract; using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common; using CommonSocketLibrary.Common;
using HermesSocketLibrary.Requests.Callbacks;
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 Serilog; using Serilog;
using TwitchChatTTS.Seven; using TwitchChatTTS.Chat.Emotes;
using TwitchChatTTS.Twitch.Redemptions;
namespace TwitchChatTTS.Hermes.Socket.Handlers namespace TwitchChatTTS.Hermes.Socket.Handlers
{ {
public class RequestAckHandler : IWebSocketHandler public class RequestAckHandler : IWebSocketHandler
{ {
private User _user; private User _user;
//private readonly RedemptionManager _redemptionManager;
private readonly ICallbackManager<HermesRequestData> _callbackManager;
private readonly IServiceProvider _serviceProvider; private readonly IServiceProvider _serviceProvider;
private readonly JsonSerializerOptions _options; private readonly JsonSerializerOptions _options;
private readonly ILogger _logger; private readonly ILogger _logger;
@ -21,9 +25,19 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
public int OperationCode { get; } = 4; public int OperationCode { get; } = 4;
public RequestAckHandler(User user, IServiceProvider serviceProvider, JsonSerializerOptions options, ILogger logger)
public RequestAckHandler(
User user,
//RedemptionManager redemptionManager,
ICallbackManager<HermesRequestData> callbackManager,
IServiceProvider serviceProvider,
JsonSerializerOptions options,
ILogger logger
)
{ {
_user = user; _user = user;
//_redemptionManager = redemptionManager;
_callbackManager = callbackManager;
_serviceProvider = serviceProvider; _serviceProvider = serviceProvider;
_options = options; _options = options;
_logger = logger; _logger = logger;
@ -34,10 +48,22 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
if (data is not RequestAckMessage message || message == null) if (data is not RequestAckMessage message || message == null)
return; return;
if (message.Request == null) if (message.Request == null)
{
_logger.Warning("Received a Hermes request message without a proper request.");
return; return;
if (_user == null) }
return;
HermesRequestData? hermesRequestData = null;
if (!string.IsNullOrEmpty(message.Request.RequestId))
{
hermesRequestData = _callbackManager.Take(message.Request.RequestId);
if (hermesRequestData == null)
_logger.Warning($"Could not find callback for request [request id: {message.Request.RequestId}][type: {message.Request.Type}]");
else if (hermesRequestData.Data == null)
hermesRequestData.Data = new Dictionary<string, object>();
}
_logger.Debug($"Received a Hermes request message [type: {message.Request.Type}][data: {string.Join(',', message.Request.Data?.Select(entry => entry.Key + '=' + entry.Value) ?? Array.Empty<string>())}]");
if (message.Request.Type == "get_tts_voices") if (message.Request.Type == "get_tts_voices")
{ {
_logger.Verbose("Updating all available voices for TTS."); _logger.Verbose("Updating all available voices for TTS.");
@ -54,16 +80,16 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
else if (message.Request.Type == "create_tts_user") else if (message.Request.Type == "create_tts_user")
{ {
_logger.Verbose("Adding new tts voice for user."); _logger.Verbose("Adding new tts voice for user.");
if (!long.TryParse(message.Request.Data["user"].ToString(), out long chatterId)) if (!long.TryParse(message.Request.Data["chatter"].ToString(), out long chatterId))
{ {
_logger.Warning($"Failed to parse chatter id [chatter id: {message.Request.Data["chatter"]}]"); _logger.Warning($"Failed to parse chatter id [chatter id: {message.Request.Data["chatter"]}]");
return; return;
} }
string userId = message.Request.Data["user"].ToString(); string userId = message.Request.Data["user"].ToString();
string voice = message.Request.Data["voice"].ToString(); string voiceId = message.Request.Data["voice"].ToString();
_user.VoicesSelected.Add(chatterId, voice); _user.VoicesSelected.Add(chatterId, voiceId);
_logger.Information($"Added new TTS voice [voice: {voice}] for user [user id: {userId}]"); _logger.Information($"Added new TTS voice [voice: {voiceId}] for user [user id: {userId}]");
} }
else if (message.Request.Type == "update_tts_user") else if (message.Request.Type == "update_tts_user")
{ {
@ -74,10 +100,10 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
return; return;
} }
string userId = message.Request.Data["user"].ToString(); string userId = message.Request.Data["user"].ToString();
string voice = message.Request.Data["voice"].ToString(); string voiceId = message.Request.Data["voice"].ToString();
_user.VoicesSelected[chatterId] = voice; _user.VoicesSelected[chatterId] = voiceId;
_logger.Information($"Updated TTS voice [voice: {voice}] for user [user id: {userId}]"); _logger.Information($"Updated TTS voice [voice: {voiceId}] for user [user id: {userId}]");
} }
else if (message.Request.Type == "create_tts_voice") else if (message.Request.Type == "create_tts_voice")
{ {
@ -99,7 +125,7 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
{ {
_logger.Verbose("Deleting tts voice."); _logger.Verbose("Deleting tts voice.");
var voice = message.Request.Data["voice"].ToString(); var voice = message.Request.Data["voice"].ToString();
if (!_user.VoicesAvailable.TryGetValue(voice, out string voiceName) || voiceName == null) if (!_user.VoicesAvailable.TryGetValue(voice, out string? voiceName) || voiceName == null)
return; return;
lock (_voicesAvailableLock) lock (_voicesAvailableLock)
@ -116,7 +142,7 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
string voiceId = message.Request.Data["idd"].ToString(); string voiceId = message.Request.Data["idd"].ToString();
string voice = message.Request.Data["voice"].ToString(); string voice = message.Request.Data["voice"].ToString();
if (!_user.VoicesAvailable.TryGetValue(voiceId, out string voiceName) || voiceName == null) if (!_user.VoicesAvailable.TryGetValue(voiceId, out string? voiceName) || voiceName == null)
return; return;
_user.VoicesAvailable[voiceId] = voice; _user.VoicesAvailable[voiceId] = voice;
@ -153,8 +179,9 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
if (emotes == null) if (emotes == null)
return; return;
var emoteDb = _serviceProvider.GetRequiredService<EmoteDatabase>(); var emoteDb = _serviceProvider.GetRequiredService<IEmoteDatabase>();
var count = 0; var count = 0;
var duplicateNames = 0;
foreach (var emote in emotes) foreach (var emote in emotes)
{ {
if (emoteDb.Get(emote.Name) == null) if (emoteDb.Get(emote.Name) == null)
@ -162,8 +189,12 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
emoteDb.Add(emote.Name, emote.Id); emoteDb.Add(emote.Name, emote.Id);
count++; count++;
} }
else
duplicateNames++;
} }
_logger.Information($"Fetched {count} emotes from various sources."); _logger.Information($"Fetched {count} emotes from various sources.");
if (duplicateNames > 0)
_logger.Warning($"Found {duplicateNames} emotes with duplicate names.");
} }
else if (message.Request.Type == "update_tts_voice_state") else if (message.Request.Type == "update_tts_voice_state")
{ {
@ -171,7 +202,7 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
string voiceId = message.Request.Data["voice"].ToString(); string voiceId = message.Request.Data["voice"].ToString();
bool state = message.Request.Data["state"].ToString().ToLower() == "true"; bool state = message.Request.Data["state"].ToString().ToLower() == "true";
if (!_user.VoicesAvailable.TryGetValue(voiceId, out string voiceName) || voiceName == null) if (!_user.VoicesAvailable.TryGetValue(voiceId, out string? voiceName) || voiceName == null)
{ {
_logger.Warning($"Failed to find voice by id [id: {voiceId}]"); _logger.Warning($"Failed to find voice by id [id: {voiceId}]");
return; return;
@ -183,6 +214,73 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
_user.VoicesEnabled.Remove(voiceId); _user.VoicesEnabled.Remove(voiceId);
_logger.Information($"Updated voice state [voice: {voiceName}][new state: {(state ? "enabled" : "disabled")}]"); _logger.Information($"Updated voice state [voice: {voiceName}][new state: {(state ? "enabled" : "disabled")}]");
} }
else if (message.Request.Type == "get_redemptions")
{
_logger.Verbose("Fetching all the redemptions.");
IEnumerable<Redemption>? redemptions = JsonSerializer.Deserialize<IEnumerable<Redemption>>(message.Data!.ToString()!, _options);
if (redemptions != null)
{
_logger.Information($"Redemptions [count: {redemptions.Count()}] loaded.");
if (hermesRequestData != null)
hermesRequestData.Data!.Add("redemptions", redemptions);
}
else
_logger.Information(message.Data.GetType().ToString());
}
else if (message.Request.Type == "get_redeemable_actions")
{
_logger.Verbose("Fetching all the redeemable actions.");
IEnumerable<RedeemableAction>? actions = JsonSerializer.Deserialize<IEnumerable<RedeemableAction>>(message.Data!.ToString()!, _options);
if (actions == null)
{
_logger.Warning("Failed to read the redeemable actions for redemptions.");
return;
}
if (hermesRequestData?.Data == null || !(hermesRequestData.Data["redemptions"] is IEnumerable<Redemption> redemptions))
{
_logger.Warning("Failed to read the redemptions while updating redemption actions.");
return;
}
_logger.Information($"Redeemable actions [count: {actions.Count()}] loaded.");
var redemptionManager = _serviceProvider.GetRequiredService<RedemptionManager>();
redemptionManager.Initialize(redemptions, actions.ToDictionary(a => a.Name, a => a));
}
else if (message.Request.Type == "get_default_tts_voice")
{
string? defaultVoice = message.Data?.ToString();
if (defaultVoice != null)
{
_user.DefaultTTSVoice = defaultVoice;
_logger.Information($"Default TTS voice was changed to '{defaultVoice}'.");
}
}
else if (message.Request.Type == "update_default_tts_voice")
{
if (message.Request.Data?.TryGetValue("voice", out object? voice) == true && voice is string v)
{
_user.DefaultTTSVoice = v;
_logger.Information($"Default TTS voice was changed to '{v}'.");
}
else
_logger.Warning("Failed to update default TTS voice via request.");
}
else
{
_logger.Warning($"Found unknown request type when acknowledging [type: {message.Request.Type}]");
}
if (hermesRequestData != null)
{
_logger.Debug($"Callback was found for request [request id: {message.Request.RequestId}][type: {message.Request.Type}]");
hermesRequestData.Callback?.Invoke(hermesRequestData.Data);
} }
} }
} }
public class HermesRequestData
{
public Action<IDictionary<string, object>?>? Callback { get; set; }
public IDictionary<string, object>? Data { get; set; }
}
}

View File

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

View File

@ -1,9 +0,0 @@
namespace TwitchChatTTS.OBS.Socket.Context
{
public class HelloContext
{
public string? Host { get; set; }
public short? Port { get; set; }
public string? Password { get; set; }
}
}

View File

@ -12,5 +12,9 @@ namespace TwitchChatTTS.OBS.Socket.Data
RequestId = id; RequestId = id;
RequestData = data; RequestData = data;
} }
public RequestMessage(string type, Dictionary<string, object> data) : this(type, string.Empty, data) { }
public RequestMessage(string type) : this(type, string.Empty, new()) { }
} }
} }

View File

@ -2,16 +2,19 @@ using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common; using CommonSocketLibrary.Common;
using Serilog; using Serilog;
using TwitchChatTTS.OBS.Socket.Data; using TwitchChatTTS.OBS.Socket.Data;
using TwitchChatTTS.OBS.Socket.Manager;
namespace TwitchChatTTS.OBS.Socket.Handlers namespace TwitchChatTTS.OBS.Socket.Handlers
{ {
public class EventMessageHandler : IWebSocketHandler public class EventMessageHandler : IWebSocketHandler
{ {
private readonly OBSManager _manager;
private readonly ILogger _logger; private readonly ILogger _logger;
public int OperationCode { get; } = 5; public int OperationCode { get; } = 5;
public EventMessageHandler(ILogger logger) public EventMessageHandler(OBSManager manager, ILogger logger)
{ {
_manager = manager;
_logger = logger; _logger = logger;
} }
@ -23,28 +26,23 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
switch (message.EventType) switch (message.EventType)
{ {
case "StreamStateChanged": case "StreamStateChanged":
case "RecordStateChanged":
if (sender is not OBSSocketClient client) if (sender is not OBSSocketClient client)
return; return;
string? raw_state = message.EventData["outputState"].ToString(); string? raw_state = message.EventData["outputState"].ToString();
string? state = raw_state?.Substring(21).ToLower(); string? state = raw_state?.Substring(21).ToLower();
client.Live = message.EventData["outputActive"].ToString() == "True"; _manager.Streaming = message.EventData["outputActive"].ToString().ToLower() == "true";
_logger.Warning("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 (_manager.Streaming == false && state != null && !state.EndsWith("ing"))
{ {
OnStreamEnd(); // Stream ended
} }
break; break;
default: default:
_logger.Debug(message.EventType + " EVENT: " + string.Join(" | ", message.EventData?.Select(x => x.Key + "=" + x.Value?.ToString()) ?? new string[0])); _logger.Debug(message.EventType + " EVENT: " + string.Join(" | ", message.EventData?.Select(x => x.Key + "=" + x.Value?.ToString()) ?? Array.Empty<string>()));
break; break;
} }
} }
private void OnStreamEnd()
{
}
} }
} }

View File

@ -4,19 +4,18 @@ using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common; using CommonSocketLibrary.Common;
using Serilog; using Serilog;
using TwitchChatTTS.OBS.Socket.Data; using TwitchChatTTS.OBS.Socket.Data;
using TwitchChatTTS.OBS.Socket.Context;
namespace TwitchChatTTS.OBS.Socket.Handlers namespace TwitchChatTTS.OBS.Socket.Handlers
{ {
public class HelloHandler : IWebSocketHandler public class HelloHandler : IWebSocketHandler
{ {
private readonly HelloContext _context; private readonly Configuration _configuration;
private readonly ILogger _logger; private readonly ILogger _logger;
public int OperationCode { get; } = 0; public int OperationCode { get; } = 0;
public HelloHandler(HelloContext context, ILogger logger) public HelloHandler(Configuration configuration, ILogger logger)
{ {
_context = context; _configuration = configuration;
_logger = logger; _logger = logger;
} }
@ -25,8 +24,9 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
if (data is not HelloMessage message || message == null) if (data is not HelloMessage message || message == null)
return; return;
_logger.Verbose("OBS websocket password: " + _context.Password); string? password = string.IsNullOrWhiteSpace(_configuration.Obs?.Password) ? null : _configuration.Obs.Password.Trim();
if (message.Authentication == null || string.IsNullOrWhiteSpace(_context.Password)) _logger.Verbose("OBS websocket password: " + password);
if (message.Authentication == null || string.IsNullOrWhiteSpace(password))
{ {
await sender.Send(1, new IdentifyMessage(message.RpcVersion, string.Empty, 1023 | 262144)); await sender.Send(1, new IdentifyMessage(message.RpcVersion, string.Empty, 1023 | 262144));
return; return;
@ -37,7 +37,7 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
_logger.Verbose("Salt: " + salt); _logger.Verbose("Salt: " + salt);
_logger.Verbose("Challenge: " + challenge); _logger.Verbose("Challenge: " + challenge);
string secret = _context.Password + salt; string secret = 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())

View File

@ -23,7 +23,7 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
if (data is not IdentifiedMessage message || message == null) if (data is not IdentifiedMessage message || message == null)
return; return;
sender.Connected = true; _manager.Connected = true;
_logger.Information("Connected to OBS via rpc version " + message.NegotiatedRpcVersion + "."); _logger.Information("Connected to OBS via rpc version " + message.NegotiatedRpcVersion + ".");
try try
@ -34,6 +34,8 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
{ {
_logger.Error(e, "Failed to load OBS group info upon OBS identification."); _logger.Error(e, "Failed to load OBS group info upon OBS identification.");
} }
await _manager.UpdateStreamingState();
} }
} }
} }

View File

@ -42,10 +42,7 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
switch (request.RequestType) switch (request.RequestType)
{ {
case "GetOutputStatus": case "GetOutputStatus":
if (sender is not OBSSocketClient client) _logger.Debug($"Fetched stream's live status [live: {_manager.Streaming}][obs request id: {message.RequestId}]");
return;
_logger.Debug($"Fetched stream's live status [live: {client.Live}][obs request id: {message.RequestId}]");
break; break;
case "GetSceneItemId": case "GetSceneItemId":
{ {
@ -227,6 +224,24 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
_logger.Debug($"Received response from OBS for sleeping [sleep: {sleepMillis}][obs request id: {message.RequestId}]"); _logger.Debug($"Received response from OBS for sleeping [sleep: {sleepMillis}][obs request id: {message.RequestId}]");
break; break;
} }
case "GetStreamStatus":
{
if (message.ResponseData == null)
{
_logger.Warning($"OBS Response is null [obs request id: {message.RequestId}]");
return;
}
if (!message.ResponseData.TryGetValue("outputActive", out object? outputActive) || outputActive == null)
{
_logger.Warning($"Failed to fetch the scene item visibility [obs request id: {message.RequestId}]");
return;
}
_manager.Streaming = outputActive?.ToString()!.ToLower() == "true";
requestData.ResponseValues = message.ResponseData;
_logger.Information($"OBS is currently {(_manager.Streaming ? "" : "not ")}streaming.");
break;
}
default: default:
_logger.Warning($"OBS Request Response not being processed [type: {request.RequestType}][{string.Join(Environment.NewLine, message.ResponseData?.Select(kvp => kvp.Key + " = " + kvp.Value?.ToString()) ?? [])}]"); _logger.Warning($"OBS Request Response not being processed [type: {request.RequestType}][{string.Join(Environment.NewLine, message.ResponseData?.Select(kvp => kvp.Key + " = " + kvp.Value?.ToString()) ?? [])}]");
break; break;

View File

@ -12,11 +12,19 @@ namespace TwitchChatTTS.OBS.Socket.Manager
{ {
private readonly IDictionary<string, RequestData> _requests; private readonly IDictionary<string, RequestData> _requests;
private readonly IDictionary<string, long> _sourceIds; private readonly IDictionary<string, long> _sourceIds;
private string? URL;
private readonly Configuration _configuration;
private readonly IServiceProvider _serviceProvider; private readonly IServiceProvider _serviceProvider;
private readonly ILogger _logger; private readonly ILogger _logger;
public OBSManager(IServiceProvider serviceProvider, ILogger logger) public bool Connected { get; set; }
public bool Streaming { get; set; }
public OBSManager(Configuration configuration, IServiceProvider serviceProvider, ILogger logger)
{ {
_configuration = configuration;
_serviceProvider = serviceProvider; _serviceProvider = serviceProvider;
_logger = logger; _logger = logger;
@ -24,6 +32,27 @@ namespace TwitchChatTTS.OBS.Socket.Manager
_sourceIds = new Dictionary<string, long>(); _sourceIds = new Dictionary<string, long>();
} }
public void Initialize()
{
_logger.Information($"Initializing OBS websocket client.");
var client = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("obs");
client.OnConnected += (sender, e) =>
{
Connected = true;
_logger.Information("OBS websocket client connected.");
};
client.OnDisconnected += (sender, e) =>
{
Connected = false;
_logger.Information("OBS websocket client disconnected.");
};
if (!string.IsNullOrWhiteSpace(_configuration.Obs?.Host) && _configuration.Obs?.Port != null)
URL = $"ws://{_configuration.Obs.Host?.Trim()}:{_configuration.Obs.Port}";
}
public void AddSourceId(string sourceName, long sourceId) public void AddSourceId(string sourceName, long sourceId)
{ {
@ -39,8 +68,35 @@ namespace TwitchChatTTS.OBS.Socket.Manager
_sourceIds.Clear(); _sourceIds.Clear();
} }
public async Task Connect()
{
if (string.IsNullOrWhiteSpace(URL))
{
_logger.Warning("Lacking connection info for OBS websockets. Not connecting to OBS.");
return;
}
var client = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("obs");
_logger.Debug($"OBS websocket client attempting to connect to {URL}");
try
{
await client.ConnectAsync(URL);
}
catch (Exception)
{
_logger.Warning("Connecting to obs failed. Skipping obs websockets.");
}
}
public async Task Send(IEnumerable<RequestMessage> messages) public async Task Send(IEnumerable<RequestMessage> messages)
{ {
if (!Connected)
{
_logger.Warning("OBS websocket client is not connected. Not sending a message.");
return;
}
string uid = GenerateUniqueIdentifier(); string uid = GenerateUniqueIdentifier();
var list = messages.ToList(); var list = messages.ToList();
_logger.Debug($"Sending OBS request batch of {list.Count} messages [obs request batch id: {uid}]."); _logger.Debug($"Sending OBS request batch of {list.Count} messages [obs request batch id: {uid}].");
@ -60,6 +116,12 @@ namespace TwitchChatTTS.OBS.Socket.Manager
public async Task Send(RequestMessage message, Action<Dictionary<string, object>>? callback = null) public async Task Send(RequestMessage message, Action<Dictionary<string, object>>? callback = null)
{ {
if (!Connected)
{
_logger.Warning("OBS websocket client is not connected. Not sending a message.");
return;
}
string uid = GenerateUniqueIdentifier(); string uid = GenerateUniqueIdentifier();
_logger.Debug($"Sending an OBS request [type: {message.RequestType}][obs request id: {uid}]"); _logger.Debug($"Sending an OBS request [type: {message.RequestType}][obs request id: {uid}]");
@ -85,21 +147,26 @@ namespace TwitchChatTTS.OBS.Socket.Manager
return null; return null;
} }
public async Task UpdateStreamingState()
{
await Send(new RequestMessage("GetStreamStatus"));
}
public async Task UpdateTransformation(string sceneName, string sceneItemName, Action<OBSTransformationData> action) public async Task UpdateTransformation(string sceneName, string sceneItemName, Action<OBSTransformationData> action)
{ {
if (action == null) if (action == null)
return; return;
await GetSceneItemById(sceneName, sceneItemName, async (sceneItemId) => await GetSceneItemByName(sceneName, sceneItemName, async (sceneItemId) =>
{ {
var m2 = new RequestMessage("GetSceneItemTransform", string.Empty, new Dictionary<string, object>() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId } }); var m2 = new RequestMessage("GetSceneItemTransform", new Dictionary<string, object>() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId } });
await Send(m2, async (d) => await Send(m2, async (d) =>
{ {
if (d == null || !d.TryGetValue("sceneItemTransform", out object? transformData) || transformData == null) if (d == null || !d.TryGetValue("sceneItemTransform", out object? transformData) || transformData == null)
return; return;
_logger.Verbose($"Current transformation data [scene: {sceneName}][sceneItemName: {sceneItemName}][sceneItemId: {sceneItemId}][transform: {transformData}][obs request id: {m2.RequestId}]"); _logger.Verbose($"Current transformation data [scene: {sceneName}][sceneItemName: {sceneItemName}][sceneItemId: {sceneItemId}][transform: {transformData}][obs request id: {m2.RequestId}]");
var transform = JsonSerializer.Deserialize<OBSTransformationData>(transformData.ToString(), new JsonSerializerOptions() var transform = JsonSerializer.Deserialize<OBSTransformationData>(transformData.ToString()!, new JsonSerializerOptions()
{ {
PropertyNameCaseInsensitive = false, PropertyNameCaseInsensitive = false,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase PropertyNamingPolicy = JsonNamingPolicy.CamelCase
@ -126,20 +193,6 @@ namespace TwitchChatTTS.OBS.Socket.Manager
transform.PositionY = transform.PositionY + h / 2; transform.PositionY = transform.PositionY + h / 2;
} }
// if (hasBounds)
// {
// // Take care of bounds, for most cases.
// // 'Crop to Bounding Box' might be unsupported.
// w = transform.BoundsWidth;
// h = transform.BoundsHeight;
// a = transform.BoundsAlignment;
// }
// else if (transform.CropBottom + transform.CropLeft + transform.CropRight + transform.CropTop > 0)
// {
// w -= transform.CropLeft + transform.CropRight;
// h -= transform.CropTop + transform.CropBottom;
// }
action?.Invoke(transform); action?.Invoke(transform);
var m3 = new RequestMessage("SetSceneItemTransform", string.Empty, new Dictionary<string, object>() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId }, { "sceneItemTransform", transform } }); var m3 = new RequestMessage("SetSceneItemTransform", string.Empty, new Dictionary<string, object>() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId }, { "sceneItemTransform", transform } });
@ -151,7 +204,7 @@ namespace TwitchChatTTS.OBS.Socket.Manager
public async Task ToggleSceneItemVisibility(string sceneName, string sceneItemName) public async Task ToggleSceneItemVisibility(string sceneName, string sceneItemName)
{ {
await GetSceneItemById(sceneName, sceneItemName, async (sceneItemId) => await GetSceneItemByName(sceneName, sceneItemName, async (sceneItemId) =>
{ {
var m1 = new RequestMessage("GetSceneItemEnabled", string.Empty, new Dictionary<string, object>() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId } }); var m1 = new RequestMessage("GetSceneItemEnabled", string.Empty, new Dictionary<string, object>() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId } });
await Send(m1, async (d) => await Send(m1, async (d) =>
@ -167,7 +220,7 @@ namespace TwitchChatTTS.OBS.Socket.Manager
public async Task UpdateSceneItemVisibility(string sceneName, string sceneItemName, bool isVisible) public async Task UpdateSceneItemVisibility(string sceneName, string sceneItemName, bool isVisible)
{ {
await GetSceneItemById(sceneName, sceneItemName, async (sceneItemId) => await GetSceneItemByName(sceneName, sceneItemName, async (sceneItemId) =>
{ {
var m = new RequestMessage("SetSceneItemEnabled", string.Empty, new Dictionary<string, object>() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId }, { "sceneItemEnabled", isVisible } }); var m = new RequestMessage("SetSceneItemEnabled", string.Empty, new Dictionary<string, object>() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId }, { "sceneItemEnabled", isVisible } });
await Send(m); await Send(m);
@ -176,7 +229,7 @@ namespace TwitchChatTTS.OBS.Socket.Manager
public async Task UpdateSceneItemIndex(string sceneName, string sceneItemName, int index) public async Task UpdateSceneItemIndex(string sceneName, string sceneItemName, int index)
{ {
await GetSceneItemById(sceneName, sceneItemName, async (sceneItemId) => await GetSceneItemByName(sceneName, sceneItemName, async (sceneItemId) =>
{ {
var m = new RequestMessage("SetSceneItemIndex", string.Empty, new Dictionary<string, object>() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId }, { "sceneItemIndex", index } }); var m = new RequestMessage("SetSceneItemIndex", string.Empty, new Dictionary<string, object>() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId }, { "sceneItemIndex", index } });
await Send(m); await Send(m);
@ -220,7 +273,7 @@ namespace TwitchChatTTS.OBS.Socket.Manager
_logger.Debug($"Fetched the list of OBS scene items in all groups [groups: {string.Join(", ", groupNames)}]"); _logger.Debug($"Fetched the list of OBS scene items in all groups [groups: {string.Join(", ", groupNames)}]");
} }
private async Task GetSceneItemById(string sceneName, string sceneItemName, Action<long> action) private async Task GetSceneItemByName(string sceneName, string sceneItemName, Action<long> action)
{ {
if (_sourceIds.TryGetValue(sceneItemName, out long sourceId)) if (_sourceIds.TryGetValue(sceneItemName, out long sourceId))
{ {
@ -245,18 +298,6 @@ namespace TwitchChatTTS.OBS.Socket.Manager
{ {
return Guid.NewGuid().ToString("N"); return Guid.NewGuid().ToString("N");
} }
private void LogExceptions(Action action, string description)
{
try
{
action.Invoke();
}
catch (Exception e)
{
_logger.Error(e, description);
}
}
} }
public class RequestData public class RequestData

View File

@ -8,17 +8,6 @@ namespace TwitchChatTTS.OBS.Socket
{ {
public class OBSSocketClient : WebSocketClient public class OBSSocketClient : WebSocketClient
{ {
private bool _live;
public bool? Live
{
get => Connected ? _live : null;
set
{
if (value.HasValue)
_live = value.Value;
}
}
public OBSSocketClient( public OBSSocketClient(
ILogger logger, ILogger logger,
[FromKeyedServices("obs")] HandlerManager<WebSocketClient, IWebSocketHandler> handlerManager, [FromKeyedServices("obs")] HandlerManager<WebSocketClient, IWebSocketHandler> handlerManager,
@ -29,7 +18,6 @@ namespace TwitchChatTTS.OBS.Socket
PropertyNamingPolicy = JsonNamingPolicy.CamelCase PropertyNamingPolicy = JsonNamingPolicy.CamelCase
}) })
{ {
_live = false;
} }
} }
} }

View File

@ -2,6 +2,7 @@ using System.Text.Json;
using TwitchChatTTS.Helpers; using TwitchChatTTS.Helpers;
using Serilog; using Serilog;
using TwitchChatTTS.Seven; using TwitchChatTTS.Seven;
using TwitchChatTTS.Chat.Emotes;
public class SevenApiClient public class SevenApiClient
{ {

57
Seven/SevenManager.cs Normal file
View File

@ -0,0 +1,57 @@
using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common;
using Microsoft.Extensions.DependencyInjection;
using Serilog;
namespace TwitchChatTTS.Seven.Socket
{
public class SevenManager
{
private readonly User _user;
private readonly IServiceProvider _serviceProvider;
private readonly ILogger _logger;
private string URL;
public bool Connected { get; set; }
public bool Streaming { get; set; }
public SevenManager(User user, IServiceProvider serviceProvider, ILogger logger)
{
_user = user;
_serviceProvider = serviceProvider;
_logger = logger;
}
public void Initialize() {
_logger.Information("Initializing 7tv websocket client.");
var client = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("7tv");
client.OnConnected += (sender, e) => {
Connected = true;
_logger.Information("7tv websocket client connected.");
};
client.OnDisconnected += (sender, e) => {
Connected = false;
_logger.Information("7tv websocket client disconnected.");
};
if (!string.IsNullOrEmpty(_user.SevenEmoteSetId))
URL = $"{SevenApiClient.WEBSOCKET_URL}@emote_set.*<object_id={_user.SevenEmoteSetId}>";
}
public async Task Connect()
{
if (string.IsNullOrWhiteSpace(_user.SevenEmoteSetId))
{
_logger.Warning("Cannot find 7tv data for your channel. Not connecting to 7tv websockets.");
return;
}
var client = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("7tv");
_logger.Debug($"7tv client attempting to connect to {URL}");
await client.ConnectAsync($"{URL}");
}
}
}

View File

@ -1,7 +0,0 @@
namespace TwitchChatTTS.Seven.Socket.Data
{
public class IdentifyMessage
{
}
}

View File

@ -2,6 +2,7 @@ using System.Text.Json;
using CommonSocketLibrary.Abstract; using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common; using CommonSocketLibrary.Common;
using Serilog; using Serilog;
using TwitchChatTTS.Chat.Emotes;
using TwitchChatTTS.Seven.Socket.Data; using TwitchChatTTS.Seven.Socket.Data;
namespace TwitchChatTTS.Seven.Socket.Handlers namespace TwitchChatTTS.Seven.Socket.Handlers
@ -9,14 +10,14 @@ namespace TwitchChatTTS.Seven.Socket.Handlers
public class DispatchHandler : IWebSocketHandler public class DispatchHandler : IWebSocketHandler
{ {
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly EmoteDatabase _emotes; private readonly IEmoteDatabase _emotes;
private readonly object _lock = new object(); private readonly object _lock = new object();
public int OperationCode { get; } = 0; public int OperationCode { get; } = 0;
public DispatchHandler(ILogger logger, EmoteDatabase emotes) public DispatchHandler(IEmoteDatabase emotes, ILogger logger)
{ {
_logger = logger;
_emotes = emotes; _emotes = emotes;
_logger = logger;
} }
public async Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data data) public async Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data data)
@ -53,12 +54,20 @@ namespace TwitchChatTTS.Seven.Socket.Handlers
{ {
if (removing) if (removing)
{ {
RemoveEmoteById(o.Id); if (_emotes.Get(o.Name) != o.Id) {
_logger.Warning("Mismatched emote found while removing a 7tv emote.");
continue;
}
_emotes.Remove(o.Name);
_logger.Information($"Removed 7tv emote [name: {o.Name}][id: {o.Id}]"); _logger.Information($"Removed 7tv emote [name: {o.Name}][id: {o.Id}]");
} }
else if (updater != null) else if (updater != null)
{ {
RemoveEmoteById(o.Id); if (_emotes.Get(o.Name) != o.Id) {
_logger.Warning("Mismatched emote found while updating a 7tv emote.");
continue;
}
_emotes.Remove(o.Name);
var update = updater(val); var update = updater(val);
var u = JsonSerializer.Deserialize<EmoteField>(update.ToString(), new JsonSerializerOptions() var u = JsonSerializer.Deserialize<EmoteField>(update.ToString(), new JsonSerializerOptions()
@ -85,20 +94,5 @@ namespace TwitchChatTTS.Seven.Socket.Handlers
} }
} }
} }
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

@ -41,9 +41,9 @@ namespace TwitchChatTTS.Seven.Socket.Handlers
]; ];
_reconnectDelay = [ _reconnectDelay = [
1000, 1000,
0, -1,
0, -1,
0, -1,
0, 0,
3000, 3000,
1000, 1000,
@ -77,7 +77,7 @@ namespace TwitchChatTTS.Seven.Socket.Handlers
if (string.IsNullOrWhiteSpace(_user.SevenEmoteSetId)) if (string.IsNullOrWhiteSpace(_user.SevenEmoteSetId))
{ {
_logger.Warning("Connected to 7tv websocket previously, but no emote set id was set."); _logger.Warning("Could not find the 7tv emote set id. Not reconnecting.");
return; return;
} }
@ -85,11 +85,9 @@ namespace TwitchChatTTS.Seven.Socket.Handlers
if (_reconnectDelay[code] > 0) if (_reconnectDelay[code] > 0)
await Task.Delay(_reconnectDelay[code]); await Task.Delay(_reconnectDelay[code]);
var base_url = $"@emote_set.*<object_id={_user.SevenEmoteSetId}>"; var manager = _serviceProvider.GetRequiredService<SevenManager>();
string url = $"{SevenApiClient.WEBSOCKET_URL}{base_url}"; await manager.Connect();
_logger.Debug($"7tv websocket reconnecting to {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 });

View File

@ -23,9 +23,8 @@ namespace TwitchChatTTS.Seven.Socket.Handlers
if (sender is not SevenSocketClient seven || seven == null) if (sender is not SevenSocketClient seven || seven == null)
return; return;
seven.Connected = true;
seven.ConnectionDetails = message; seven.ConnectionDetails = message;
_logger.Information("Connected to 7tv websockets."); _logger.Debug("Received hello handshake ack.");
} }
} }
} }

View File

@ -1,3 +1,5 @@
using TwitchChatTTS.Chat.Emotes;
namespace TwitchChatTTS.Seven namespace TwitchChatTTS.Seven
{ {
public class UserDetails public class UserDetails

View File

@ -11,8 +11,6 @@ using TwitchChatTTS.Seven.Socket;
using TwitchChatTTS.OBS.Socket.Handlers; using TwitchChatTTS.OBS.Socket.Handlers;
using TwitchChatTTS.Seven.Socket.Handlers; using TwitchChatTTS.Seven.Socket.Handlers;
using TwitchChatTTS.Seven.Socket.Context; using TwitchChatTTS.Seven.Socket.Context;
using TwitchChatTTS.Seven;
using TwitchChatTTS.OBS.Socket.Context;
using TwitchLib.Client.Interfaces; using TwitchLib.Client.Interfaces;
using TwitchLib.Client; using TwitchLib.Client;
using TwitchLib.PubSub.Interfaces; using TwitchLib.PubSub.Interfaces;
@ -31,6 +29,8 @@ using Serilog.Sinks.SystemConsole.Themes;
using TwitchChatTTS.Twitch.Redemptions; using TwitchChatTTS.Twitch.Redemptions;
using TwitchChatTTS.Chat.Groups.Permissions; using TwitchChatTTS.Chat.Groups.Permissions;
using TwitchChatTTS.Chat.Groups; using TwitchChatTTS.Chat.Groups;
using TwitchChatTTS.Chat.Emotes;
using HermesSocketLibrary.Requests.Callbacks;
// 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
@ -61,6 +61,7 @@ var logger = new LoggerConfiguration()
s.AddSerilog(logger); s.AddSerilog(logger);
s.AddSingleton<User>(new User()); s.AddSingleton<User>(new User());
s.AddSingleton<ICallbackManager<HermesRequestData>, CallbackManager<HermesRequestData>>();
s.AddSingleton<JsonSerializerOptions>(new JsonSerializerOptions() s.AddSingleton<JsonSerializerOptions>(new JsonSerializerOptions()
{ {
@ -71,6 +72,7 @@ s.AddSingleton<JsonSerializerOptions>(new JsonSerializerOptions()
// Command parameters // Command parameters
s.AddKeyedSingleton<ChatCommandParameter, TTSVoiceNameParameter>("parameter-ttsvoicename"); s.AddKeyedSingleton<ChatCommandParameter, TTSVoiceNameParameter>("parameter-ttsvoicename");
s.AddKeyedSingleton<ChatCommandParameter, UnvalidatedParameter>("parameter-unvalidated"); s.AddKeyedSingleton<ChatCommandParameter, UnvalidatedParameter>("parameter-unvalidated");
s.AddKeyedSingleton<ChatCommandParameter, SimpleListedParameter>("parameter-simplelisted");
s.AddKeyedSingleton<ChatCommand, SkipAllCommand>("command-skipall"); s.AddKeyedSingleton<ChatCommand, SkipAllCommand>("command-skipall");
s.AddKeyedSingleton<ChatCommand, SkipCommand>("command-skip"); s.AddKeyedSingleton<ChatCommand, SkipCommand>("command-skip");
s.AddKeyedSingleton<ChatCommand, VoiceCommand>("command-voice"); s.AddKeyedSingleton<ChatCommand, VoiceCommand>("command-voice");
@ -88,24 +90,16 @@ s.AddSingleton<TTSPlayer>();
s.AddSingleton<ChatMessageHandler>(); s.AddSingleton<ChatMessageHandler>();
s.AddSingleton<RedemptionManager>(); s.AddSingleton<RedemptionManager>();
s.AddSingleton<HermesApiClient>(); s.AddSingleton<HermesApiClient>();
s.AddSingleton<TwitchBotAuth>(new TwitchBotAuth()); s.AddSingleton<TwitchBotAuth>();
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>(new EmoteDatabase()); s.AddSingleton<IEmoteDatabase, EmoteDatabase>();
// OBS websocket // OBS websocket
s.AddSingleton<HelloContext>(sp =>
new HelloContext()
{
Host = string.IsNullOrWhiteSpace(configuration.Obs?.Host) ? null : configuration.Obs.Host.Trim(),
Port = configuration.Obs?.Port,
Password = string.IsNullOrWhiteSpace(configuration.Obs?.Password) ? null : configuration.Obs.Password.Trim()
}
);
s.AddSingleton<OBSManager>(); s.AddSingleton<OBSManager>();
s.AddKeyedSingleton<IWebSocketHandler, HelloHandler>("obs-hello"); s.AddKeyedSingleton<IWebSocketHandler, HelloHandler>("obs-hello");
s.AddKeyedSingleton<IWebSocketHandler, IdentifiedHandler>("obs-identified"); s.AddKeyedSingleton<IWebSocketHandler, IdentifiedHandler>("obs-identified");
@ -123,15 +117,9 @@ s.AddTransient(sp =>
var logger = sp.GetRequiredService<ILogger>(); 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.Error("7tv client == null.");
return new ReconnectContext() { SessionId = null }; return new ReconnectContext() { SessionId = null };
}
if (client.ConnectionDetails == null) if (client.ConnectionDetails == 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 };
}); });
s.AddKeyedSingleton<IWebSocketHandler, SevenHelloHandler>("7tv-sevenhello"); s.AddKeyedSingleton<IWebSocketHandler, SevenHelloHandler>("7tv-sevenhello");
@ -141,6 +129,7 @@ s.AddKeyedSingleton<IWebSocketHandler, ReconnectHandler>("7tv-reconnect");
s.AddKeyedSingleton<IWebSocketHandler, ErrorHandler>("7tv-error"); s.AddKeyedSingleton<IWebSocketHandler, ErrorHandler>("7tv-error");
s.AddKeyedSingleton<IWebSocketHandler, EndOfStreamHandler>("7tv-endofstream"); s.AddKeyedSingleton<IWebSocketHandler, EndOfStreamHandler>("7tv-endofstream");
s.AddSingleton<SevenManager>();
s.AddKeyedSingleton<HandlerManager<WebSocketClient, IWebSocketHandler>, SevenHandlerManager>("7tv"); s.AddKeyedSingleton<HandlerManager<WebSocketClient, IWebSocketHandler>, SevenHandlerManager>("7tv");
s.AddKeyedSingleton<HandlerTypeManager<WebSocketClient, IWebSocketHandler>, SevenHandlerTypeManager>("7tv"); s.AddKeyedSingleton<HandlerTypeManager<WebSocketClient, IWebSocketHandler>, SevenHandlerTypeManager>("7tv");
s.AddKeyedSingleton<SocketClient<WebSocketMessage>, SevenSocketClient>("7tv"); s.AddKeyedSingleton<SocketClient<WebSocketMessage>, SevenSocketClient>("7tv");

134
TTS.cs
View File

@ -1,30 +1,34 @@
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Web; using System.Web;
using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common;
using HermesSocketLibrary.Socket.Data;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Serilog; using Serilog;
using NAudio.Wave.SampleProviders; using NAudio.Wave.SampleProviders;
using TwitchChatTTS.Seven;
using TwitchLib.Client.Events; using TwitchLib.Client.Events;
using TwitchChatTTS.Twitch.Redemptions; using TwitchChatTTS.Twitch.Redemptions;
using org.mariuszgromada.math.mxparser; using org.mariuszgromada.math.mxparser;
using TwitchChatTTS.Hermes.Socket; using TwitchChatTTS.Hermes.Socket;
using TwitchChatTTS.Chat.Groups.Permissions; using TwitchChatTTS.Chat.Groups.Permissions;
using TwitchChatTTS.Chat.Groups; using TwitchChatTTS.Chat.Groups;
using TwitchChatTTS.OBS.Socket.Manager;
using TwitchChatTTS.Seven.Socket;
using TwitchChatTTS.Chat.Emotes;
using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common;
namespace TwitchChatTTS namespace TwitchChatTTS
{ {
public class TTS : IHostedService public class TTS : IHostedService
{ {
public const int MAJOR_VERSION = 3; public const int MAJOR_VERSION = 3;
public const int MINOR_VERSION = 8; public const int MINOR_VERSION = 9;
private readonly User _user; private readonly User _user;
private readonly HermesApiClient _hermesApiClient; private readonly HermesApiClient _hermesApiClient;
private readonly SevenApiClient _sevenApiClient; private readonly SevenApiClient _sevenApiClient;
private readonly OBSManager _obsManager;
private readonly SevenManager _sevenManager;
private readonly HermesSocketClient _hermes;
private readonly RedemptionManager _redemptionManager; private readonly RedemptionManager _redemptionManager;
private readonly IChatterGroupManager _chatterGroupManager; private readonly IChatterGroupManager _chatterGroupManager;
private readonly IGroupPermissionManager _permissionManager; private readonly IGroupPermissionManager _permissionManager;
@ -37,6 +41,9 @@ namespace TwitchChatTTS
User user, User user,
HermesApiClient hermesApiClient, HermesApiClient hermesApiClient,
SevenApiClient sevenApiClient, SevenApiClient sevenApiClient,
OBSManager obsManager,
SevenManager sevenManager,
[FromKeyedServices("hermes")] SocketClient<WebSocketMessage> hermes,
RedemptionManager redemptionManager, RedemptionManager redemptionManager,
IChatterGroupManager chatterGroupManager, IChatterGroupManager chatterGroupManager,
IGroupPermissionManager permissionManager, IGroupPermissionManager permissionManager,
@ -49,6 +56,9 @@ namespace TwitchChatTTS
_user = user; _user = user;
_hermesApiClient = hermesApiClient; _hermesApiClient = hermesApiClient;
_sevenApiClient = sevenApiClient; _sevenApiClient = sevenApiClient;
_obsManager = obsManager;
_sevenManager = sevenManager;
_hermes = (hermes as HermesSocketClient)!;
_redemptionManager = redemptionManager; _redemptionManager = redemptionManager;
_chatterGroupManager = chatterGroupManager; _chatterGroupManager = chatterGroupManager;
_permissionManager = permissionManager; _permissionManager = permissionManager;
@ -63,7 +73,7 @@ namespace TwitchChatTTS
Console.Title = "TTS - Twitch Chat"; Console.Title = "TTS - Twitch Chat";
License.iConfirmCommercialUse("abcdef"); License.iConfirmCommercialUse("abcdef");
if (string.IsNullOrWhiteSpace(_configuration.Hermes.Token)) if (string.IsNullOrWhiteSpace(_configuration.Hermes?.Token))
{ {
_logger.Error("Hermes API token not set in the configuration file."); _logger.Error("Hermes API token not set in the configuration file.");
return; return;
@ -83,13 +93,14 @@ namespace TwitchChatTTS
await Task.Delay(15 * 1000); await Task.Delay(15 * 1000);
} }
await InitializeHermesWebsocket();
try try
{ {
await FetchUserData(_user, _hermesApiClient, _sevenApiClient); await FetchUserData(_user, _hermesApiClient);
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.Error(ex, "Failed to initialize properly."); _logger.Error(ex, "Failed to initialize properly. Restart app please.");
await Task.Delay(30 * 1000); await Task.Delay(30 * 1000);
} }
@ -101,13 +112,21 @@ namespace TwitchChatTTS
} }
var emoteSet = await _sevenApiClient.FetchChannelEmoteSet(_user.TwitchUserId.ToString()); var emoteSet = await _sevenApiClient.FetchChannelEmoteSet(_user.TwitchUserId.ToString());
if (emoteSet != null)
_user.SevenEmoteSetId = emoteSet.Id; _user.SevenEmoteSetId = emoteSet.Id;
await InitializeEmotes(_sevenApiClient, emoteSet); await InitializeEmotes(_sevenApiClient, emoteSet);
await InitializeHermesWebsocket();
await InitializeSevenTv(); await InitializeSevenTv();
await InitializeObs(); await InitializeObs();
// _logger.Information("Sending a request to server...");
// await _hermesManager.Send(3, new RequestMessage() {
// Type = "get_redeemable_actions",
// Data = new Dictionary<string, object>()
// });
// _logger.Warning("OS VERSION: " + Environment.OSVersion + " | " + Environment.OSVersion.Platform);
// return;
AudioPlaybackEngine.Instance.AddOnMixerInputEnded((object? s, SampleProviderEventArgs e) => AudioPlaybackEngine.Instance.AddOnMixerInputEnded((object? s, SampleProviderEventArgs e) =>
{ {
if (e.SampleProvider == _player.Playing) if (e.SampleProvider == _player.Playing)
@ -212,12 +231,9 @@ namespace TwitchChatTTS
_logger.Warning("Application has stopped."); _logger.Warning("Application has stopped.");
} }
private async Task FetchUserData(User user, HermesApiClient hermes, SevenApiClient seven) private async Task FetchUserData(User user, HermesApiClient hermes)
{ {
var hermesAccount = await hermes.FetchHermesAccountDetails(); var hermesAccount = await hermes.FetchHermesAccountDetails();
if (hermesAccount == null)
throw new Exception("Cannot connect to Hermes. Ensure your token is valid.");
user.HermesUserId = hermesAccount.Id; user.HermesUserId = hermesAccount.Id;
user.HermesUsername = hermesAccount.Username; user.HermesUsername = hermesAccount.Username;
user.TwitchUsername = hermesAccount.Username; user.TwitchUsername = hermesAccount.Username;
@ -226,25 +242,20 @@ namespace TwitchChatTTS
user.TwitchUserId = long.Parse(twitchBotToken.BroadcasterId); user.TwitchUserId = long.Parse(twitchBotToken.BroadcasterId);
_logger.Information($"Username: {user.TwitchUsername} [id: {user.TwitchUserId}]"); _logger.Information($"Username: {user.TwitchUsername} [id: {user.TwitchUserId}]");
user.DefaultTTSVoice = await hermes.FetchTTSDefaultVoice(); // user.DefaultTTSVoice = await hermes.FetchTTSDefaultVoice();
_logger.Information("TTS Default Voice: " + user.DefaultTTSVoice); // _logger.Information("TTS Default Voice: " + user.DefaultTTSVoice);
var wordFilters = await hermes.FetchTTSWordFilters(); // var wordFilters = await hermes.FetchTTSWordFilters();
user.RegexFilters = wordFilters.ToList(); // user.RegexFilters = wordFilters.ToList();
_logger.Information($"{user.RegexFilters.Count()} TTS word filters."); // _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(); var voicesSelected = await hermes.FetchTTSChatterSelectedVoices();
user.VoicesSelected = voicesSelected.ToDictionary(s => s.ChatterId, s => s.Voice); user.VoicesSelected = voicesSelected.ToDictionary(s => s.ChatterId, s => s.Voice);
_logger.Information($"{user.VoicesSelected.Count} TTS voices have been selected for specific chatters."); _logger.Information($"{user.VoicesSelected.Count} chatters have selected a specific TTS voice, among {user.VoicesSelected.Values.Distinct().Count()} distinct TTS voices.");
var voicesEnabled = await hermes.FetchTTSEnabledVoices(); var voicesEnabled = await hermes.FetchTTSEnabledVoices();
if (voicesEnabled == null || !voicesEnabled.Any()) if (voicesEnabled == null || !voicesEnabled.Any())
user.VoicesEnabled = new HashSet<string>(["Brian"]); user.VoicesEnabled = new HashSet<string>([user.DefaultTTSVoice]);
else else
user.VoicesEnabled = new HashSet<string>(voicesEnabled.Select(v => v)); user.VoicesEnabled = new HashSet<string>(voicesEnabled.Select(v => v));
_logger.Information($"{user.VoicesEnabled.Count} TTS voices have been enabled."); _logger.Information($"{user.VoicesEnabled.Count} TTS voices have been enabled.");
@ -253,13 +264,10 @@ namespace TwitchChatTTS
if (defaultedChatters.Any()) if (defaultedChatters.Any())
_logger.Information($"{defaultedChatters.Count()} chatter(s) will have their TTS voice set to default due to having selected a disabled TTS voice."); _logger.Information($"{defaultedChatters.Count()} chatter(s) will have their TTS voice set to default due to having selected a disabled TTS voice.");
var redemptionActions = await hermes.FetchRedeemableActions(); // var redemptionActions = await hermes.FetchRedeemableActions();
var redemptions = await hermes.FetchRedemptions(); // var redemptions = await hermes.FetchRedemptions();
_redemptionManager.Initialize(redemptions, redemptionActions.ToDictionary(a => a.Name, a => a)); // _redemptionManager.Initialize(redemptions, redemptionActions.ToDictionary(a => a.Name, a => a));
_logger.Information($"Redemption Manager has been initialized with {redemptionActions.Count()} actions & {redemptions.Count()} redemptions."); // _logger.Information($"Redemption Manager has been initialized with {redemptionActions.Count()} actions & {redemptions.Count()} redemptions.");
_chatterGroupManager.Clear();
_permissionManager.Clear();
var groups = await hermes.FetchGroups(); var groups = await hermes.FetchGroups();
var groupsById = groups.ToDictionary(g => g.Id, g => g); var groupsById = groups.ToDictionary(g => g.Id, g => g);
@ -296,22 +304,12 @@ namespace TwitchChatTTS
{ {
try try
{ {
_logger.Information("Initializing hermes websocket client."); _hermes.Initialize();
var hermesClient = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("hermes"); await _hermes.Connect();
var url = $"wss://{HermesSocketClient.BASE_URL}";
_logger.Debug($"Attempting to connect to {url}");
await hermesClient.ConnectAsync(url);
hermesClient.Connected = true;
await hermesClient.Send(1, new HermesLoginMessage()
{
ApiKey = _configuration.Hermes!.Token!,
MajorVersion = TTS.MAJOR_VERSION,
MinorVersion = TTS.MINOR_VERSION,
});
} }
catch (Exception) catch (Exception e)
{ {
_logger.Warning("Connecting to hermes failed. Skipping hermes websockets."); _logger.Error(e, "Connecting to hermes failed. Skipping hermes websockets.");
} }
} }
@ -319,37 +317,21 @@ namespace TwitchChatTTS
{ {
try try
{ {
_logger.Information("Initializing 7tv websocket client."); _sevenManager.Initialize();
var sevenClient = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("7tv"); await _sevenManager.Connect();
if (string.IsNullOrWhiteSpace(_user.SevenEmoteSetId))
{
_logger.Warning("Could not fetch 7tv emotes.");
return;
} }
var url = $"{SevenApiClient.WEBSOCKET_URL}@emote_set.*<object_id={_user.SevenEmoteSetId}>"; catch (Exception e)
_logger.Debug($"Attempting to connect to {url}");
await sevenClient.ConnectAsync($"{url}");
}
catch (Exception)
{ {
_logger.Warning("Connecting to 7tv failed. Skipping 7tv websockets."); _logger.Error(e, "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.Warning("Lacking OBS connection info. Skipping OBS websockets.");
return;
}
try try
{ {
var obsClient = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("obs"); _obsManager.Initialize();
var url = $"ws://{_configuration.Obs.Host.Trim()}:{_configuration.Obs.Port}"; await _obsManager.Connect();
_logger.Debug($"Initializing OBS websocket client. Attempting to connect to {url}");
await obsClient.ConnectAsync(url);
} }
catch (Exception) catch (Exception)
{ {
@ -367,7 +349,7 @@ namespace TwitchChatTTS
return null; return null;
} }
var channels = _configuration.Twitch.Channels ?? [username]; var channels = _configuration.Twitch?.Channels ?? [username];
_logger.Information("Twitch channels: " + string.Join(", ", channels)); _logger.Information("Twitch channels: " + string.Join(", ", channels));
twitchapiclient.InitializeClient(username, channels); twitchapiclient.InitializeClient(username, channels);
twitchapiclient.InitializePublisher(); twitchapiclient.InitializePublisher();
@ -381,15 +363,7 @@ namespace TwitchChatTTS
if (result.Status != MessageStatus.None || result.Emotes == null || !result.Emotes.Any()) if (result.Status != MessageStatus.None || result.Emotes == null || !result.Emotes.Any())
return; return;
var ws = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("hermes"); await _hermes.SendEmoteUsage(e.ChatMessage.Id, result.ChatterId, result.Emotes);
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) catch (Exception ex)
{ {
@ -400,9 +374,9 @@ namespace TwitchChatTTS
return twitchapiclient; return twitchapiclient;
} }
private async Task InitializeEmotes(SevenApiClient sevenapi, EmoteSet channelEmotes) private async Task InitializeEmotes(SevenApiClient sevenapi, EmoteSet? channelEmotes)
{ {
var emotes = _serviceProvider.GetRequiredService<EmoteDatabase>(); var emotes = _serviceProvider.GetRequiredService<IEmoteDatabase>();
var globalEmotes = await sevenapi.FetchGlobalSevenEmotes(); var globalEmotes = await sevenapi.FetchGlobalSevenEmotes();
if (channelEmotes != null && channelEmotes.Emotes.Any()) if (channelEmotes != null && channelEmotes.Emotes.Any())

View File

@ -1,9 +0,0 @@
namespace TwitchChatTTS.Twitch.Redemptions
{
public class RedeemableAction
{
public string Name { get; set; }
public string Type { get; set; }
public IDictionary<string, string> Data { get; set; }
}
}

View File

@ -1,11 +0,0 @@
namespace TwitchChatTTS.Twitch.Redemptions
{
public class Redemption
{
public string Id { get; set; }
public string RedemptionId { get; set; }
public string ActionName { get; set; }
public int Order { get; set; }
public bool State { get; set; }
}
}

View File

@ -1,9 +1,11 @@
using System.Reflection; using System.Reflection;
using CommonSocketLibrary.Abstract; using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common; using CommonSocketLibrary.Common;
using HermesSocketLibrary.Requests.Messages;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using org.mariuszgromada.math.mxparser; using org.mariuszgromada.math.mxparser;
using Serilog; using Serilog;
using TwitchChatTTS.Hermes.Socket;
using TwitchChatTTS.OBS.Socket.Data; using TwitchChatTTS.OBS.Socket.Data;
using TwitchChatTTS.OBS.Socket.Manager; using TwitchChatTTS.OBS.Socket.Manager;
@ -14,7 +16,7 @@ namespace TwitchChatTTS.Twitch.Redemptions
private readonly IDictionary<string, IList<RedeemableAction>> _store; private readonly IDictionary<string, IList<RedeemableAction>> _store;
private readonly User _user; private readonly User _user;
private readonly OBSManager _obsManager; private readonly OBSManager _obsManager;
private readonly SocketClient<WebSocketMessage> _hermesClient; private readonly HermesSocketClient _hermes;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly Random _random; private readonly Random _random;
private bool _isReady; private bool _isReady;
@ -23,13 +25,13 @@ namespace TwitchChatTTS.Twitch.Redemptions
public RedemptionManager( public RedemptionManager(
User user, User user,
OBSManager obsManager, OBSManager obsManager,
[FromKeyedServices("hermes")] SocketClient<WebSocketMessage> hermesClient, [FromKeyedServices("hermes")] SocketClient<WebSocketMessage> hermes,
ILogger logger) ILogger logger)
{ {
_store = new Dictionary<string, IList<RedeemableAction>>(); _store = new Dictionary<string, IList<RedeemableAction>>();
_user = user; _user = user;
_obsManager = obsManager; _obsManager = obsManager;
_hermesClient = hermesClient; _hermes = (hermes as HermesSocketClient)!;
_logger = logger; _logger = logger;
_random = new Random(); _random = new Random();
_isReady = false; _isReady = false;
@ -46,6 +48,14 @@ namespace TwitchChatTTS.Twitch.Redemptions
public async Task Execute(RedeemableAction action, string senderDisplayName, long senderId) public async Task Execute(RedeemableAction action, string senderDisplayName, long senderId)
{ {
_logger.Debug($"Executing an action for a redemption [action: {action.Name}][action type: {action.Type}][chatter: {senderDisplayName}][chatter id: {senderId}]");
if (action.Data == null)
{
_logger.Warning($"No data was provided for an action, caused by redemption [action: {action.Name}][action type: {action.Type}][chatter: {senderDisplayName}][chatter id: {senderId}]");
return;
}
try try
{ {
switch (action.Type) switch (action.Type)
@ -53,12 +63,12 @@ namespace TwitchChatTTS.Twitch.Redemptions
case "WRITE_TO_FILE": case "WRITE_TO_FILE":
Directory.CreateDirectory(Path.GetDirectoryName(action.Data["file_path"])); Directory.CreateDirectory(Path.GetDirectoryName(action.Data["file_path"]));
await File.WriteAllTextAsync(action.Data["file_path"], ReplaceContentText(action.Data["file_content"], senderDisplayName)); await File.WriteAllTextAsync(action.Data["file_path"], ReplaceContentText(action.Data["file_content"], senderDisplayName));
_logger.Debug($"Overwritten text to file [file: {action.Data["file_path"]}]"); _logger.Debug($"Overwritten text to file [file: {action.Data["file_path"]}][chatter: {senderDisplayName}][chatter id: {senderId}]");
break; break;
case "APPEND_TO_FILE": case "APPEND_TO_FILE":
Directory.CreateDirectory(Path.GetDirectoryName(action.Data["file_path"])); Directory.CreateDirectory(Path.GetDirectoryName(action.Data["file_path"]));
await File.AppendAllTextAsync(action.Data["file_path"], ReplaceContentText(action.Data["file_content"], senderDisplayName)); await File.AppendAllTextAsync(action.Data["file_path"], ReplaceContentText(action.Data["file_content"], senderDisplayName));
_logger.Debug($"Appended text to file [file: {action.Data["file_path"]}]"); _logger.Debug($"Appended text to file [file: {action.Data["file_path"]}][chatter: {senderDisplayName}][chatter id: {senderId}]");
break; break;
case "OBS_TRANSFORM": case "OBS_TRANSFORM":
var type = typeof(OBSTransformationData); var type = typeof(OBSTransformationData);
@ -74,29 +84,30 @@ namespace TwitchChatTTS.Twitch.Redemptions
PropertyInfo? prop = type.GetProperty(propertyName, BindingFlags.Public | BindingFlags.Instance); PropertyInfo? prop = type.GetProperty(propertyName, BindingFlags.Public | BindingFlags.Instance);
if (prop == null) if (prop == null)
{ {
_logger.Warning($"Failed to find property for OBS transformation [scene: {action.Data["scene_name"]}][source: {action.Data["scene_item_name"]}][property: {propertyName}]"); _logger.Warning($"Failed to find property for OBS transformation [scene: {action.Data["scene_name"]}][source: {action.Data["scene_item_name"]}][property: {propertyName}][chatter: {senderDisplayName}][chatter id: {senderId}]");
continue; continue;
} }
var currentValue = prop.GetValue(d); var currentValue = prop.GetValue(d);
if (currentValue == null) if (currentValue == null)
{ {
_logger.Warning($"Found a null value from OBS transformation [scene: {action.Data["scene_name"]}][source: {action.Data["scene_item_name"]}][property: {propertyName}]"); _logger.Warning($"Found a null value from OBS transformation [scene: {action.Data["scene_name"]}][source: {action.Data["scene_item_name"]}][property: {propertyName}][chatter: {senderDisplayName}][chatter id: {senderId}]");
continue;
} }
Expression expression = new Expression(expressionString); Expression expression = new Expression(expressionString);
expression.addConstants(new Constant("x", (double?)currentValue ?? 0.0d)); expression.addConstants(new Constant("x", (double?)currentValue ?? 0.0d));
if (!expression.checkSyntax()) if (!expression.checkSyntax())
{ {
_logger.Warning($"Could not parse math expression for OBS transformation [scene: {action.Data["scene_name"]}][source: {action.Data["scene_item_name"]}][expression: {expressionString}][property: {propertyName}]"); _logger.Warning($"Could not parse math expression for OBS transformation [scene: {action.Data["scene_name"]}][source: {action.Data["scene_item_name"]}][expression: {expressionString}][property: {propertyName}][chatter: {senderDisplayName}][chatter id: {senderId}]");
continue; continue;
} }
var newValue = expression.calculate(); var newValue = expression.calculate();
prop.SetValue(d, newValue); prop.SetValue(d, newValue);
_logger.Debug($"OBS transformation [scene: {action.Data["scene_name"]}][source: {action.Data["scene_item_name"]}][property: {propertyName}][old value: {currentValue}][new value: {newValue}][expression: {expressionString}]"); _logger.Debug($"OBS transformation [scene: {action.Data["scene_name"]}][source: {action.Data["scene_item_name"]}][property: {propertyName}][old value: {currentValue}][new value: {newValue}][expression: {expressionString}][chatter: {senderDisplayName}][chatter id: {senderId}]");
} }
_logger.Debug($"Finished applying the OBS transformation property changes [scene: {action.Data["scene_name"]}][source: {action.Data["scene_item_name"]}]"); _logger.Debug($"Finished applying the OBS transformation property changes [scene: {action.Data["scene_name"]}][source: {action.Data["scene_item_name"]}][chatter: {senderDisplayName}][chatter id: {senderId}]");
}); });
break; break;
case "TOGGLE_OBS_VISIBILITY": case "TOGGLE_OBS_VISIBILITY":
@ -113,63 +124,78 @@ namespace TwitchChatTTS.Twitch.Redemptions
await Task.Delay(int.Parse(action.Data["sleep"])); await Task.Delay(int.Parse(action.Data["sleep"]));
break; break;
case "SPECIFIC_TTS_VOICE": case "SPECIFIC_TTS_VOICE":
var voiceId = _user.VoicesAvailable.Keys.First(id => _user.VoicesAvailable[id].ToLower() == action.Data["tts_voice"].ToLower()); case "RANDOM_TTS_VOICE":
if (voiceId == null) string voiceId = string.Empty;
bool specific = action.Type == "SPECIFIC_TTS_VOICE";
var voicesEnabled = _user.VoicesEnabled.ToList();
if (specific)
voiceId = _user.VoicesAvailable.Keys.First(id => _user.VoicesAvailable[id].ToLower() == action.Data["tts_voice"].ToLower());
else
{ {
_logger.Warning($"Voice specified is not valid [voice: {action.Data["tts_voice"]}]"); if (!voicesEnabled.Any())
{
_logger.Warning($"There are no TTS voices enabled [voice pool size: {voicesEnabled.Count}][chatter: {senderDisplayName}][chatter id: {senderId}]");
return;
}
if (voicesEnabled.Count <= 1)
{
_logger.Warning($"There are not enough TTS voices enabled to randomize [voice pool size: {voicesEnabled.Count}][chatter: {senderDisplayName}][chatter id: {senderId}]");
return;
}
string? selectedId = null;
if (!_user.VoicesSelected.ContainsKey(senderId))
selectedId = _user.VoicesAvailable.Keys.First(id => _user.VoicesAvailable[id] == _user.DefaultTTSVoice);
else
selectedId = _user.VoicesSelected[senderId];
do
{
var randomVoice = voicesEnabled[_random.Next(voicesEnabled.Count)];
voiceId = _user.VoicesAvailable.Keys.First(id => _user.VoicesAvailable[id] == randomVoice);
} while (voiceId == selectedId);
}
if (string.IsNullOrEmpty(voiceId))
{
_logger.Warning($"Voice is not valid [voice: {action.Data["tts_voice"]}][voice pool size: {voicesEnabled.Count}][source: redemption][chatter: {senderDisplayName}][chatter id: {senderId}]");
return; return;
} }
var voiceName = _user.VoicesAvailable[voiceId]; var voiceName = _user.VoicesAvailable[voiceId];
if (!_user.VoicesEnabled.Contains(voiceName)) if (!_user.VoicesEnabled.Contains(voiceName))
{ {
_logger.Warning($"Voice specified is not enabled [voice: {action.Data["tts_voice"]}][voice id: {voiceId}]"); _logger.Warning($"Voice is not enabled [voice: {action.Data["tts_voice"]}][voice pool size: {voicesEnabled.Count}][voice id: {voiceId}][source: redemption][chatter: {senderDisplayName}][chatter id: {senderId}]");
return; return;
} }
await _hermesClient.Send(3, new HermesSocketLibrary.Socket.Data.RequestMessage()
if (_user.VoicesSelected.ContainsKey(senderId))
{ {
Type = _user.VoicesSelected.ContainsKey(senderId) ? "update_tts_user" : "create_tts_user", await _hermes.UpdateTTSUser(senderId, voiceId);
Data = new Dictionary<string, object>() { { "chatter", senderId }, { "voice", voiceId } } _logger.Debug($"Sent request to create chat TTS voice [voice: {voiceName}][chatter id: {senderId}][source: redemption][chatter: {senderDisplayName}][chatter id: {senderId}]");
});
_logger.Debug($"Changed the TTS voice of a chatter [voice: {action.Data["tts_voice"]}][display name: {senderDisplayName}][chatter id: {senderId}]");
break;
case "RANDOM_TTS_VOICE":
var voicesEnabled = _user.VoicesEnabled.ToList();
if (!voicesEnabled.Any())
{
_logger.Warning($"There are no TTS voices enabled [voice pool size: {voicesEnabled.Count}]");
return;
} }
if (voicesEnabled.Count <= 1) else
{ {
_logger.Warning($"There are not enough TTS voices enabled to randomize [voice pool size: {voicesEnabled.Count}]"); await _hermes.CreateTTSUser(senderId, voiceId);
return; _logger.Debug($"Sent request to update chat TTS voice [voice: {voiceName}][chatter id: {senderId}][source: redemption][chatter: {senderDisplayName}][chatter id: {senderId}]");
} }
var randomVoice = voicesEnabled[_random.Next(voicesEnabled.Count)];
var randomVoiceId = _user.VoicesAvailable.Keys.First(id => _user.VoicesAvailable[id] == randomVoice);
await _hermesClient.Send(3, new HermesSocketLibrary.Socket.Data.RequestMessage()
{
Type = _user.VoicesSelected.ContainsKey(senderId) ? "update_tts_user" : "create_tts_user",
Data = new Dictionary<string, object>() { { "chatter", senderId }, { "voice", randomVoiceId } }
});
_logger.Debug($"Randomly changed the TTS voice of a chatter [voice: {randomVoice}][display name: {senderDisplayName}][chatter id: {senderId}]");
break; break;
case "AUDIO_FILE": case "AUDIO_FILE":
if (!File.Exists(action.Data["file_path"])) if (!File.Exists(action.Data["file_path"]))
{ {
_logger.Warning($"Cannot find audio file for Twitch channel point redeem [file: {action.Data["file_path"]}]"); _logger.Warning($"Cannot find audio file for Twitch channel point redeem [file: {action.Data["file_path"]}][chatter: {senderDisplayName}][chatter id: {senderId}]");
return; return;
} }
AudioPlaybackEngine.Instance.PlaySound(action.Data["file_path"]); AudioPlaybackEngine.Instance.PlaySound(action.Data["file_path"]);
_logger.Debug($"Played an audio file for channel point redeem [file: {action.Data["file_path"]}]"); _logger.Debug($"Played an audio file for channel point redeem [file: {action.Data["file_path"]}][chatter: {senderDisplayName}][chatter id: {senderId}]");
break; break;
default: default:
_logger.Warning($"Unknown redeemable action has occured. Update needed? [type: {action.Type}]"); _logger.Warning($"Unknown redeemable action has occured. Update needed? [type: {action.Type}][chatter: {senderDisplayName}][chatter id: {senderId}]");
break; break;
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.Error(ex, "Failed to execute a redemption action."); _logger.Error(ex, $"Failed to execute a redemption action [action: {action.Name}][action type: {action.Type}][chatter: {senderDisplayName}][chatter id: {senderId}]");
} }
} }
@ -187,9 +213,15 @@ namespace TwitchChatTTS.Twitch.Redemptions
{ {
_store.Clear(); _store.Clear();
var ordered = redemptions.OrderBy(r => r.Order); var ordered = redemptions.Where(r => r != null).OrderBy(r => r.Order);
foreach (var redemption in ordered) foreach (var redemption in ordered)
{ {
if (redemption.ActionName == null)
{
_logger.Warning("Null value found for the action name of a redemption.");
continue;
}
try try
{ {
if (actions.TryGetValue(redemption.ActionName, out var action) && action != null) if (actions.TryGetValue(redemption.ActionName, out var action) && action != null)

View File

@ -6,47 +6,43 @@ using TwitchLib.Api.Core.Exceptions;
using TwitchLib.Client.Events; using TwitchLib.Client.Events;
using TwitchLib.Client.Models; using TwitchLib.Client.Models;
using TwitchLib.Communication.Events; using TwitchLib.Communication.Events;
using Microsoft.Extensions.DependencyInjection;
using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common;
using TwitchLib.PubSub.Interfaces; using TwitchLib.PubSub.Interfaces;
using TwitchLib.Client.Interfaces; using TwitchLib.Client.Interfaces;
using TwitchChatTTS.OBS.Socket;
using TwitchChatTTS.Twitch.Redemptions; using TwitchChatTTS.Twitch.Redemptions;
public class TwitchApiClient public class TwitchApiClient
{ {
private readonly RedemptionManager _redemptionManager; private readonly RedemptionManager _redemptionManager;
private readonly HermesApiClient _hermesApiClient; private readonly HermesApiClient _hermesApiClient;
private readonly Configuration _configuration;
private readonly TwitchBotAuth _token;
private readonly ITwitchClient _client; private readonly ITwitchClient _client;
private readonly ITwitchPubSub _publisher; private readonly ITwitchPubSub _publisher;
private readonly WebClientWrap _web; private readonly User _user;
private readonly IServiceProvider _serviceProvider; private readonly Configuration _configuration;
private readonly TwitchBotAuth _token;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly WebClientWrap _web;
private bool _initialized; private bool _initialized;
private string _broadcasterId; private string _broadcasterId;
public TwitchApiClient( public TwitchApiClient(
RedemptionManager redemptionManager,
HermesApiClient hermesApiClient,
Configuration configuration,
TwitchBotAuth token,
ITwitchClient twitchClient, ITwitchClient twitchClient,
ITwitchPubSub twitchPublisher, ITwitchPubSub twitchPublisher,
IServiceProvider serviceProvider, RedemptionManager redemptionManager,
HermesApiClient hermesApiClient,
User user,
Configuration configuration,
TwitchBotAuth token,
ILogger logger ILogger logger
) )
{ {
_redemptionManager = redemptionManager; _redemptionManager = redemptionManager;
_hermesApiClient = hermesApiClient; _hermesApiClient = hermesApiClient;
_configuration = configuration;
_token = token;
_client = twitchClient; _client = twitchClient;
_publisher = twitchPublisher; _publisher = twitchPublisher;
_serviceProvider = serviceProvider; _user = user;
_configuration = configuration;
_token = token;
_logger = logger; _logger = logger;
_initialized = false; _initialized = false;
_broadcasterId = string.Empty; _broadcasterId = string.Empty;
@ -88,7 +84,7 @@ public class TwitchApiClient
} }
catch (HttpResponseException e) catch (HttpResponseException e)
{ {
if (string.IsNullOrWhiteSpace(_configuration.Hermes?.Token)) if (string.IsNullOrWhiteSpace(_configuration.Hermes!.Token))
_logger.Error("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.Error("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);
@ -112,7 +108,7 @@ public class TwitchApiClient
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)
@ -130,7 +126,7 @@ public class TwitchApiClient
_client.OnConnected += async Task (object? s, OnConnectedArgs e) => _client.OnConnected += async Task (object? s, OnConnectedArgs e) =>
{ {
_logger.Information("-----------------------------------------------------------"); _logger.Information("Twitch API client connected.");
}; };
_client.OnIncorrectLogin += async Task (object? s, OnIncorrectLoginArgs e) => _client.OnIncorrectLogin += async Task (object? s, OnIncorrectLoginArgs e) =>
@ -139,9 +135,10 @@ public class TwitchApiClient
_logger.Information("Attempting to re-authorize."); _logger.Information("Attempting to re-authorize.");
await Authorize(_broadcasterId); await Authorize(_broadcasterId);
await _client.DisconnectAsync(); _client.SetConnectionCredentials(new ConnectionCredentials(_user.TwitchUsername, _token!.AccessToken));
await Task.Delay(TimeSpan.FromSeconds(1));
await _client.ConnectAsync(); await Task.Delay(TimeSpan.FromSeconds(3));
await _client.ReconnectAsync();
}; };
_client.OnConnectionError += async Task (object? s, OnConnectionErrorArgs e) => _client.OnConnectionError += async Task (object? s, OnConnectionErrorArgs e) =>
@ -156,6 +153,8 @@ public class TwitchApiClient
{ {
_logger.Error(e.Exception, "Twitch API client error."); _logger.Error(e.Exception, "Twitch API client error.");
}; };
_client.OnDisconnected += async Task (s, e) => _logger.Warning("Twitch API client disconnected.");
} }
public void InitializePublisher() public void InitializePublisher()
@ -171,21 +170,15 @@ public class TwitchApiClient
_publisher.OnFollow += (s, e) => _publisher.OnFollow += (s, e) =>
{ {
var client = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("obs") as OBSSocketClient;
if (_configuration.Twitch?.TtsWhenOffline != true && client?.Live == false)
return;
_logger.Information($"New Follower [name: {e.DisplayName}][username: {e.Username}]"); _logger.Information($"New Follower [name: {e.DisplayName}][username: {e.Username}]");
}; };
_publisher.OnChannelPointsRewardRedeemed += async (s, e) => _publisher.OnChannelPointsRewardRedeemed += async (s, e) =>
{ {
var client = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("obs") as OBSSocketClient;
if (_configuration.Twitch?.TtsWhenOffline != true && client?.Live == false)
return;
_logger.Information($"Channel Point Reward Redeemed [redeem: {e.RewardRedeemed.Redemption.Reward.Title}][redeem id: {e.RewardRedeemed.Redemption.Reward.Id}][transaction: {e.RewardRedeemed.Redemption.Id}]"); _logger.Information($"Channel Point Reward Redeemed [redeem: {e.RewardRedeemed.Redemption.Reward.Title}][redeem id: {e.RewardRedeemed.Redemption.Reward.Id}][transaction: {e.RewardRedeemed.Redemption.Id}]");
try
{
var actions = _redemptionManager.Get(e.RewardRedeemed.Redemption.Reward.Id); var actions = _redemptionManager.Get(e.RewardRedeemed.Redemption.Reward.Id);
if (!actions.Any()) if (!actions.Any())
{ {
@ -203,6 +196,11 @@ public class TwitchApiClient
{ {
_logger.Error(ex, $"Failed to execute redeeemable action [action: {action.Name}][action type: {action.Type}][redeem: {e.RewardRedeemed.Redemption.Reward.Title}][redeem id: {e.RewardRedeemed.Redemption.Reward.Id}][transaction: {e.RewardRedeemed.Redemption.Id}]"); _logger.Error(ex, $"Failed to execute redeeemable action [action: {action.Name}][action type: {action.Type}][redeem: {e.RewardRedeemed.Redemption.Reward.Title}][redeem id: {e.RewardRedeemed.Redemption.Reward.Id}][transaction: {e.RewardRedeemed.Redemption.Id}]");
} }
}
catch (Exception ex)
{
_logger.Error(ex, $"Failed to fetch the redeemable actions for a redemption [redeem: {e.RewardRedeemed.Redemption.Reward.Title}][redeem id: {e.RewardRedeemed.Redemption.Reward.Id}][transaction: {e.RewardRedeemed.Redemption.Id}]");
}
}; };
_publisher.OnPubSubServiceClosed += async (s, e) => _publisher.OnPubSubServiceClosed += async (s, e) =>

View File

@ -25,7 +25,7 @@
<PackageReference Include="Serilog.Sinks.File" Version="5.0.1-dev-00972" /> <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.RollingFile" Version="3.3.1-dev-00771" />
<PackageReference Include="Serilog.Sinks.Trace" Version="4.0.0" /> <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.4" />
<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" />
<PackageReference Include="TwitchLib.Api.Core.Enums" Version="3.10.0-preview-e47ba7f" /> <PackageReference Include="TwitchLib.Api.Core.Enums" Version="3.10.0-preview-e47ba7f" />
@ -35,8 +35,8 @@
<PackageReference Include="TwitchLib.Client" Version="4.0.0-preview-fd131763416cb9f1a31705ca609566d7e7e7fac8" /> <PackageReference Include="TwitchLib.Client" Version="4.0.0-preview-fd131763416cb9f1a31705ca609566d7e7e7fac8" />
<PackageReference Include="TwitchLib.Client.Enums" Version="4.0.0-preview-fd131763416cb9f1a31705ca609566d7e7e7fac8" /> <PackageReference Include="TwitchLib.Client.Enums" Version="4.0.0-preview-fd131763416cb9f1a31705ca609566d7e7e7fac8" />
<PackageReference Include="TwitchLib.Client.Models" Version="4.0.0-preview-fd131763416cb9f1a31705ca609566d7e7e7fac8" /> <PackageReference Include="TwitchLib.Client.Models" Version="4.0.0-preview-fd131763416cb9f1a31705ca609566d7e7e7fac8" />
<PackageReference Include="TwitchLib.Communication" Version="2.0.0" /> <PackageReference Include="TwitchLib.Communication" Version="2.0.1" />
<PackageReference Include="TwitchLib.EventSub.Core" Version="2.5.1" /> <PackageReference Include="TwitchLib.EventSub.Core" Version="2.5.3-preview-e1a92de" />
<PackageReference Include="TwitchLib.PubSub" Version="4.0.0-preview-f833b1ab1ebef37618dba3fbb1e0a661ff183af5" /> <PackageReference Include="TwitchLib.PubSub" Version="4.0.0-preview-f833b1ab1ebef37618dba3fbb1e0a661ff183af5" />
<PackageReference Include="NAudio.Core" Version="2.2.1" /> <PackageReference Include="NAudio.Core" Version="2.2.1" />
<PackageReference Include="TwitchLib.Api" Version="3.10.0-preview-e47ba7f" /> <PackageReference Include="TwitchLib.Api" Version="3.10.0-preview-e47ba7f" />