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
obj/
bin/
logs/

View File

@ -1,17 +1,16 @@
using System.Text.RegularExpressions;
using TwitchLib.Client.Events;
using TwitchChatTTS.OBS.Socket;
using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common;
using Serilog;
using Microsoft.Extensions.DependencyInjection;
using TwitchChatTTS;
using TwitchChatTTS.Seven;
using TwitchChatTTS.Chat.Commands;
using TwitchChatTTS.Hermes.Socket;
using HermesSocketLibrary.Socket.Data;
using TwitchChatTTS.Chat.Groups.Permissions;
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
@ -21,14 +20,14 @@ public class ChatMessageHandler
private readonly ChatCommandManager _commands;
private readonly IGroupPermissionManager _permissionManager;
private readonly IChatterGroupManager _chatterGroupManager;
private readonly EmoteDatabase _emotes;
private readonly OBSSocketClient? _obsClient;
private readonly HermesSocketClient? _hermesClient;
private readonly IEmoteDatabase _emotes;
private readonly OBSManager _obsManager;
private readonly HermesSocketClient _hermes;
private readonly Configuration _configuration;
private readonly ILogger _logger;
private Regex sfxRegex;
private Regex _sfxRegex;
private HashSet<long> _chatters;
public HashSet<long> Chatters { get => _chatters; set => _chatters = value; }
@ -40,9 +39,9 @@ public class ChatMessageHandler
ChatCommandManager commands,
IGroupPermissionManager permissionManager,
IChatterGroupManager chatterGroupManager,
EmoteDatabase emotes,
[FromKeyedServices("obs")] SocketClient<WebSocketMessage> obsClient,
[FromKeyedServices("hermes")] SocketClient<WebSocketMessage> hermesClient,
IEmoteDatabase emotes,
OBSManager obsManager,
[FromKeyedServices("hermes")] SocketClient<WebSocketMessage> hermes,
Configuration configuration,
ILogger logger
)
@ -53,66 +52,66 @@ public class ChatMessageHandler
_permissionManager = permissionManager;
_chatterGroupManager = chatterGroupManager;
_emotes = emotes;
_obsClient = obsClient as OBSSocketClient;
_hermesClient = hermesClient as HermesSocketClient;
_obsManager = obsManager;
_hermes = (hermes as HermesSocketClient)!;
_configuration = configuration;
_logger = logger;
_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)
{
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;
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 chatterId = long.Parse(m.UserId);
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 defaultGroups = new string[] { "everyone", "subscribers", "vip", "moderators", "broadcaster" };
var customGroups = _chatterGroupManager.GetGroupNamesFor(chatterId);
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);
if (commandResult != ChatCommandResult.Unknown)
return new MessageResult(MessageStatus.Command, -1, -1);
}
catch (Exception ex)
{
_logger.Error(ex, $"Failed executing a chat command [message: {msg}][chatter: {m.Username}][chatter id: {m.UserId}][message id: {m.Id}]");
}
var commandResult = await _commands.Execute(msg, m, groups);
if (commandResult != ChatCommandResult.Unknown)
return new MessageResult(MessageStatus.Command, -1, -1);
}
catch (Exception ex)
{
_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}");
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()
{
Id = chatterId,
Name = m.Username
}));
tasks.Add(_hermes.SendChatterDetails(chatterId, m.Username));
_chatters.Add(chatterId);
}
@ -149,11 +148,8 @@ public class ChatMessageHandler
if (wordCounter[w] <= 4 && (emoteId == null || totalEmoteUsed <= 5))
filteredMsg += w + " ";
}
if (_obsClient.Connected && newEmotes.Any())
tasks.Add(_hermesClient.Send(7, new EmoteDetailsMessage()
{
Emotes = newEmotes
}));
if (_obsManager.Streaming && newEmotes.Any())
tasks.Add(_hermes.SendEmoteDetails(newEmotes));
msg = filteredMsg;
// Replace filtered words.
@ -231,7 +227,7 @@ public class ChatMessageHandler
return;
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));
if (parts.Length == 1)
@ -251,7 +247,7 @@ public class ChatMessageHandler
return;
}
var sfxMatches = sfxRegex.Matches(message);
var sfxMatches = _sfxRegex.Matches(message);
var sfxStart = sfxMatches.FirstOrDefault()?.Index ?? message.Length;
for (var i = 0; i < sfxMatches.Count; i++)

View File

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

View File

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

View File

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

View File

@ -3,6 +3,7 @@ using CommonSocketLibrary.Common;
using Microsoft.Extensions.DependencyInjection;
using Serilog;
using TwitchChatTTS.Chat.Commands.Parameters;
using TwitchChatTTS.Hermes.Socket;
using TwitchChatTTS.OBS.Socket.Data;
using TwitchChatTTS.OBS.Socket.Manager;
using TwitchLib.Client.Models;
@ -19,7 +20,6 @@ namespace TwitchChatTTS.Chat.Commands
[FromKeyedServices("parameter-unvalidated")] ChatCommandParameter unvalidatedParameter,
User user,
OBSManager manager,
[FromKeyedServices("obs")] SocketClient<WebSocketMessage> hermesClient,
ILogger logger
) : base("obs", "Various obs commands.")
{
@ -28,14 +28,17 @@ namespace TwitchChatTTS.Chat.Commands
_logger = logger;
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;
}
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)
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 TwitchChatTTS.Chat.Commands.Parameters;
using TwitchChatTTS.Chat.Groups;
using TwitchChatTTS.Chat.Groups.Permissions;
using TwitchChatTTS.Hermes.Socket;
using TwitchChatTTS.OBS.Socket.Manager;
using TwitchChatTTS.Twitch.Redemptions;
using TwitchLib.Client.Models;
@ -34,20 +37,28 @@ namespace TwitchChatTTS.Chat.Commands
_obsManager = obsManager;
_hermesApi = hermesApi;
_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;
}
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();
if (value == null)
return;
var value = args.First().ToLower();
switch (value.ToLower())
switch (value)
{
case "tts_voice_enabled":
var voicesEnabled = await _hermesApi.FetchTTSEnabledVoices();
@ -62,12 +73,6 @@ namespace TwitchChatTTS.Chat.Commands
_user.RegexFilters = wordFilters.ToList();
_logger.Information($"{_user.RegexFilters.Count()} TTS word filters.");
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":
{
var voicesSelected = await _hermesApi.FetchTTSChatterSelectedVoices();
@ -87,15 +92,8 @@ namespace TwitchChatTTS.Chat.Commands
break;
case "obs_cache":
{
try
{
_obsManager.ClearCache();
await _obsManager.GetGroupList(async groups => await _obsManager.GetGroupSceneItemList(groups));
}
catch (Exception e)
{
_logger.Error(e, "Failed to load OBS group info via command.");
}
_obsManager.ClearCache();
await _obsManager.GetGroupList(async groups => await _obsManager.GetGroupSceneItemList(groups));
break;
}
case "permissions":

View File

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

View File

@ -1,4 +1,5 @@
using Serilog;
using TwitchChatTTS.Hermes.Socket;
using TwitchLib.Client.Models;
namespace TwitchChatTTS.Chat.Commands
@ -15,12 +16,12 @@ namespace TwitchChatTTS.Chat.Commands
_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;
}
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();

View File

@ -1,4 +1,5 @@
using Serilog;
using TwitchChatTTS.Hermes.Socket;
using TwitchLib.Client.Models;
namespace TwitchChatTTS.Chat.Commands
@ -15,12 +16,12 @@ namespace TwitchChatTTS.Chat.Commands
_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;
}
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)
return;

View File

@ -1,9 +1,7 @@
using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common;
using HermesSocketLibrary.Socket.Data;
using Microsoft.Extensions.DependencyInjection;
using Serilog;
using TwitchChatTTS.Chat.Commands.Parameters;
using TwitchChatTTS.Hermes.Socket;
using TwitchLib.Client.Models;
namespace TwitchChatTTS.Chat.Commands
@ -11,31 +9,27 @@ namespace TwitchChatTTS.Chat.Commands
public class TTSCommand : ChatCommand
{
private readonly User _user;
private readonly SocketClient<WebSocketMessage> _hermesClient;
private readonly ILogger _logger;
public TTSCommand(
[FromKeyedServices("parameter-ttsvoicename")] ChatCommandParameter ttsVoiceParameter,
[FromKeyedServices("parameter-unvalidated")] ChatCommandParameter unvalidatedParameter,
User user,
[FromKeyedServices("hermes")] SocketClient<WebSocketMessage> hermesClient,
ILogger logger
) : base("tts", "Various tts commands.")
{
_user = user;
_hermesClient = hermesClient;
_logger = logger;
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;
}
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)
return;
@ -44,25 +38,9 @@ namespace TwitchChatTTS.Chat.Commands
var voiceId = _user.VoicesAvailable.FirstOrDefault(v => v.Value.ToLower() == voiceName).Key;
var action = args[1].ToLower();
switch (action)
{
case "enable":
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;
}
bool state = action == "enable";
await client.UpdateTTSVoiceState(voiceId, state);
_logger.Information($"Changed state for TTS voice [voice: {voiceName}][state: {state}][invoker: {message.Username}][id: {message.UserId}]");
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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;
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 Serilog;
using TwitchChatTTS.OBS.Socket.Data;
using TwitchChatTTS.OBS.Socket.Manager;
namespace TwitchChatTTS.OBS.Socket.Handlers
{
public class EventMessageHandler : IWebSocketHandler
{
private readonly OBSManager _manager;
private readonly ILogger _logger;
public int OperationCode { get; } = 5;
public EventMessageHandler(ILogger logger)
public EventMessageHandler(OBSManager manager, ILogger logger)
{
_manager = manager;
_logger = logger;
}
@ -23,28 +26,23 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
switch (message.EventType)
{
case "StreamStateChanged":
case "RecordStateChanged":
if (sender is not OBSSocketClient client)
return;
string? raw_state = message.EventData["outputState"].ToString();
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 + ".");
if (client.Live == false && state != null && !state.EndsWith("ing"))
if (_manager.Streaming == false && state != null && !state.EndsWith("ing"))
{
OnStreamEnd();
// Stream ended
}
break;
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;
}
}
private void OnStreamEnd()
{
}
}
}

View File

@ -4,19 +4,18 @@ using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common;
using Serilog;
using TwitchChatTTS.OBS.Socket.Data;
using TwitchChatTTS.OBS.Socket.Context;
namespace TwitchChatTTS.OBS.Socket.Handlers
{
public class HelloHandler : IWebSocketHandler
{
private readonly HelloContext _context;
private readonly Configuration _configuration;
private readonly ILogger _logger;
public int OperationCode { get; } = 0;
public HelloHandler(HelloContext context, ILogger logger)
public HelloHandler(Configuration configuration, ILogger logger)
{
_context = context;
_configuration = configuration;
_logger = logger;
}
@ -25,8 +24,9 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
if (data is not HelloMessage message || message == null)
return;
_logger.Verbose("OBS websocket password: " + _context.Password);
if (message.Authentication == null || string.IsNullOrWhiteSpace(_context.Password))
string? password = string.IsNullOrWhiteSpace(_configuration.Obs?.Password) ? null : _configuration.Obs.Password.Trim();
_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));
return;
@ -37,7 +37,7 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
_logger.Verbose("Salt: " + salt);
_logger.Verbose("Challenge: " + challenge);
string secret = _context.Password + salt;
string secret = password + salt;
byte[] bytes = Encoding.UTF8.GetBytes(secret);
string hash = null;
using (var sha = SHA256.Create())

View File

@ -23,7 +23,7 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
if (data is not IdentifiedMessage message || message == null)
return;
sender.Connected = true;
_manager.Connected = true;
_logger.Information("Connected to OBS via rpc version " + message.NegotiatedRpcVersion + ".");
try
@ -34,6 +34,8 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
{
_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)
{
case "GetOutputStatus":
if (sender is not OBSSocketClient client)
return;
_logger.Debug($"Fetched stream's live status [live: {client.Live}][obs request id: {message.RequestId}]");
_logger.Debug($"Fetched stream's live status [live: {_manager.Streaming}][obs request id: {message.RequestId}]");
break;
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}]");
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:
_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;

View File

@ -12,11 +12,19 @@ namespace TwitchChatTTS.OBS.Socket.Manager
{
private readonly IDictionary<string, RequestData> _requests;
private readonly IDictionary<string, long> _sourceIds;
private string? URL;
private readonly Configuration _configuration;
private readonly IServiceProvider _serviceProvider;
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;
_logger = logger;
@ -24,6 +32,27 @@ namespace TwitchChatTTS.OBS.Socket.Manager
_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)
{
@ -39,8 +68,35 @@ namespace TwitchChatTTS.OBS.Socket.Manager
_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)
{
if (!Connected)
{
_logger.Warning("OBS websocket client is not connected. Not sending a message.");
return;
}
string uid = GenerateUniqueIdentifier();
var list = messages.ToList();
_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)
{
if (!Connected)
{
_logger.Warning("OBS websocket client is not connected. Not sending a message.");
return;
}
string uid = GenerateUniqueIdentifier();
_logger.Debug($"Sending an OBS request [type: {message.RequestType}][obs request id: {uid}]");
@ -85,21 +147,26 @@ namespace TwitchChatTTS.OBS.Socket.Manager
return null;
}
public async Task UpdateStreamingState()
{
await Send(new RequestMessage("GetStreamStatus"));
}
public async Task UpdateTransformation(string sceneName, string sceneItemName, Action<OBSTransformationData> action)
{
if (action == null)
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) =>
{
if (d == null || !d.TryGetValue("sceneItemTransform", out object? transformData) || transformData == null)
return;
_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,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
@ -126,20 +193,6 @@ namespace TwitchChatTTS.OBS.Socket.Manager
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);
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)
{
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 } });
await Send(m1, async (d) =>
@ -167,7 +220,7 @@ namespace TwitchChatTTS.OBS.Socket.Manager
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 } });
await Send(m);
@ -176,7 +229,7 @@ namespace TwitchChatTTS.OBS.Socket.Manager
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 } });
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)}]");
}
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))
{
@ -245,18 +298,6 @@ namespace TwitchChatTTS.OBS.Socket.Manager
{
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

View File

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

View File

@ -2,6 +2,7 @@ using System.Text.Json;
using TwitchChatTTS.Helpers;
using Serilog;
using TwitchChatTTS.Seven;
using TwitchChatTTS.Chat.Emotes;
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.Common;
using Serilog;
using TwitchChatTTS.Chat.Emotes;
using TwitchChatTTS.Seven.Socket.Data;
namespace TwitchChatTTS.Seven.Socket.Handlers
@ -9,14 +10,14 @@ namespace TwitchChatTTS.Seven.Socket.Handlers
public class DispatchHandler : IWebSocketHandler
{
private readonly ILogger _logger;
private readonly EmoteDatabase _emotes;
private readonly IEmoteDatabase _emotes;
private readonly object _lock = new object();
public int OperationCode { get; } = 0;
public DispatchHandler(ILogger logger, EmoteDatabase emotes)
public DispatchHandler(IEmoteDatabase emotes, ILogger logger)
{
_logger = logger;
_emotes = emotes;
_logger = logger;
}
public async Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data data)
@ -53,12 +54,20 @@ namespace TwitchChatTTS.Seven.Socket.Handlers
{
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}]");
}
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 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 = [
1000,
0,
0,
0,
-1,
-1,
-1,
0,
3000,
1000,
@ -77,7 +77,7 @@ namespace TwitchChatTTS.Seven.Socket.Handlers
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;
}
@ -85,11 +85,9 @@ namespace TwitchChatTTS.Seven.Socket.Handlers
if (_reconnectDelay[code] > 0)
await Task.Delay(_reconnectDelay[code]);
var base_url = $"@emote_set.*<object_id={_user.SevenEmoteSetId}>";
string url = $"{SevenApiClient.WEBSOCKET_URL}{base_url}";
_logger.Debug($"7tv websocket reconnecting to {url}.");
var manager = _serviceProvider.GetRequiredService<SevenManager>();
await manager.Connect();
await sender.ConnectAsync(url);
if (context.SessionId != null)
{
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)
return;
seven.Connected = true;
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
{
public class UserDetails

View File

@ -11,8 +11,6 @@ using TwitchChatTTS.Seven.Socket;
using TwitchChatTTS.OBS.Socket.Handlers;
using TwitchChatTTS.Seven.Socket.Handlers;
using TwitchChatTTS.Seven.Socket.Context;
using TwitchChatTTS.Seven;
using TwitchChatTTS.OBS.Socket.Context;
using TwitchLib.Client.Interfaces;
using TwitchLib.Client;
using TwitchLib.PubSub.Interfaces;
@ -31,6 +29,8 @@ using Serilog.Sinks.SystemConsole.Themes;
using TwitchChatTTS.Twitch.Redemptions;
using TwitchChatTTS.Chat.Groups.Permissions;
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 win-x64 -p:PublishSingleFile=true --self-contained true
@ -61,6 +61,7 @@ var logger = new LoggerConfiguration()
s.AddSerilog(logger);
s.AddSingleton<User>(new User());
s.AddSingleton<ICallbackManager<HermesRequestData>, CallbackManager<HermesRequestData>>();
s.AddSingleton<JsonSerializerOptions>(new JsonSerializerOptions()
{
@ -71,6 +72,7 @@ s.AddSingleton<JsonSerializerOptions>(new JsonSerializerOptions()
// Command parameters
s.AddKeyedSingleton<ChatCommandParameter, TTSVoiceNameParameter>("parameter-ttsvoicename");
s.AddKeyedSingleton<ChatCommandParameter, UnvalidatedParameter>("parameter-unvalidated");
s.AddKeyedSingleton<ChatCommandParameter, SimpleListedParameter>("parameter-simplelisted");
s.AddKeyedSingleton<ChatCommand, SkipAllCommand>("command-skipall");
s.AddKeyedSingleton<ChatCommand, SkipCommand>("command-skip");
s.AddKeyedSingleton<ChatCommand, VoiceCommand>("command-voice");
@ -88,24 +90,16 @@ s.AddSingleton<TTSPlayer>();
s.AddSingleton<ChatMessageHandler>();
s.AddSingleton<RedemptionManager>();
s.AddSingleton<HermesApiClient>();
s.AddSingleton<TwitchBotAuth>(new TwitchBotAuth());
s.AddSingleton<TwitchBotAuth>();
s.AddTransient<IClient, TwitchLib.Communication.Clients.WebSocketClient>();
s.AddTransient<ITwitchClient, TwitchClient>();
s.AddTransient<ITwitchPubSub, TwitchPubSub>();
s.AddSingleton<TwitchApiClient>();
s.AddSingleton<SevenApiClient>();
s.AddSingleton<EmoteDatabase>(new EmoteDatabase());
s.AddSingleton<IEmoteDatabase, EmoteDatabase>();
// 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.AddKeyedSingleton<IWebSocketHandler, HelloHandler>("obs-hello");
s.AddKeyedSingleton<IWebSocketHandler, IdentifiedHandler>("obs-identified");
@ -123,15 +117,9 @@ s.AddTransient(sp =>
var logger = sp.GetRequiredService<ILogger>();
var client = sp.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("7tv") as SevenSocketClient;
if (client == null)
{
logger.Error("7tv client == null.");
return new ReconnectContext() { SessionId = null };
}
if (client.ConnectionDetails == null)
{
logger.Error("Connection details in 7tv client == null.");
return new ReconnectContext() { SessionId = null };
}
return new ReconnectContext() { SessionId = client.ConnectionDetails.SessionId };
});
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, EndOfStreamHandler>("7tv-endofstream");
s.AddSingleton<SevenManager>();
s.AddKeyedSingleton<HandlerManager<WebSocketClient, IWebSocketHandler>, SevenHandlerManager>("7tv");
s.AddKeyedSingleton<HandlerTypeManager<WebSocketClient, IWebSocketHandler>, SevenHandlerTypeManager>("7tv");
s.AddKeyedSingleton<SocketClient<WebSocketMessage>, SevenSocketClient>("7tv");

136
TTS.cs
View File

@ -1,30 +1,34 @@
using System.Runtime.InteropServices;
using System.Web;
using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common;
using HermesSocketLibrary.Socket.Data;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Serilog;
using NAudio.Wave.SampleProviders;
using TwitchChatTTS.Seven;
using TwitchLib.Client.Events;
using TwitchChatTTS.Twitch.Redemptions;
using org.mariuszgromada.math.mxparser;
using TwitchChatTTS.Hermes.Socket;
using TwitchChatTTS.Chat.Groups.Permissions;
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
{
public class TTS : IHostedService
{
public const int MAJOR_VERSION = 3;
public const int MINOR_VERSION = 8;
public const int MINOR_VERSION = 9;
private readonly User _user;
private readonly HermesApiClient _hermesApiClient;
private readonly SevenApiClient _sevenApiClient;
private readonly OBSManager _obsManager;
private readonly SevenManager _sevenManager;
private readonly HermesSocketClient _hermes;
private readonly RedemptionManager _redemptionManager;
private readonly IChatterGroupManager _chatterGroupManager;
private readonly IGroupPermissionManager _permissionManager;
@ -37,6 +41,9 @@ namespace TwitchChatTTS
User user,
HermesApiClient hermesApiClient,
SevenApiClient sevenApiClient,
OBSManager obsManager,
SevenManager sevenManager,
[FromKeyedServices("hermes")] SocketClient<WebSocketMessage> hermes,
RedemptionManager redemptionManager,
IChatterGroupManager chatterGroupManager,
IGroupPermissionManager permissionManager,
@ -49,6 +56,9 @@ namespace TwitchChatTTS
_user = user;
_hermesApiClient = hermesApiClient;
_sevenApiClient = sevenApiClient;
_obsManager = obsManager;
_sevenManager = sevenManager;
_hermes = (hermes as HermesSocketClient)!;
_redemptionManager = redemptionManager;
_chatterGroupManager = chatterGroupManager;
_permissionManager = permissionManager;
@ -63,7 +73,7 @@ namespace TwitchChatTTS
Console.Title = "TTS - Twitch Chat";
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.");
return;
@ -83,13 +93,14 @@ namespace TwitchChatTTS
await Task.Delay(15 * 1000);
}
await InitializeHermesWebsocket();
try
{
await FetchUserData(_user, _hermesApiClient, _sevenApiClient);
await FetchUserData(_user, _hermesApiClient);
}
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);
}
@ -101,13 +112,21 @@ namespace TwitchChatTTS
}
var emoteSet = await _sevenApiClient.FetchChannelEmoteSet(_user.TwitchUserId.ToString());
_user.SevenEmoteSetId = emoteSet.Id;
if (emoteSet != null)
_user.SevenEmoteSetId = emoteSet.Id;
await InitializeEmotes(_sevenApiClient, emoteSet);
await InitializeHermesWebsocket();
await InitializeSevenTv();
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) =>
{
if (e.SampleProvider == _player.Playing)
@ -212,12 +231,9 @@ namespace TwitchChatTTS
_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();
if (hermesAccount == null)
throw new Exception("Cannot connect to Hermes. Ensure your token is valid.");
user.HermesUserId = hermesAccount.Id;
user.HermesUsername = hermesAccount.Username;
user.TwitchUsername = hermesAccount.Username;
@ -226,25 +242,20 @@ namespace TwitchChatTTS
user.TwitchUserId = long.Parse(twitchBotToken.BroadcasterId);
_logger.Information($"Username: {user.TwitchUsername} [id: {user.TwitchUserId}]");
user.DefaultTTSVoice = await hermes.FetchTTSDefaultVoice();
_logger.Information("TTS Default Voice: " + user.DefaultTTSVoice);
// user.DefaultTTSVoice = await hermes.FetchTTSDefaultVoice();
// _logger.Information("TTS Default Voice: " + user.DefaultTTSVoice);
var wordFilters = await hermes.FetchTTSWordFilters();
user.RegexFilters = wordFilters.ToList();
_logger.Information($"{user.RegexFilters.Count()} TTS word filters.");
var usernameFilters = await hermes.FetchTTSUsernameFilters();
user.ChatterFilters = usernameFilters.ToDictionary(e => e.Username, e => e);
_logger.Information($"{user.ChatterFilters.Where(f => f.Value.Tag == "blacklisted").Count()} username(s) have been blocked.");
_logger.Information($"{user.ChatterFilters.Where(f => f.Value.Tag == "priority").Count()} user(s) have been prioritized.");
// var wordFilters = await hermes.FetchTTSWordFilters();
// user.RegexFilters = wordFilters.ToList();
// _logger.Information($"{user.RegexFilters.Count()} TTS word filters.");
var voicesSelected = await hermes.FetchTTSChatterSelectedVoices();
user.VoicesSelected = voicesSelected.ToDictionary(s => s.ChatterId, s => s.Voice);
_logger.Information($"{user.VoicesSelected.Count} TTS voices have been selected for specific chatters.");
_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();
if (voicesEnabled == null || !voicesEnabled.Any())
user.VoicesEnabled = new HashSet<string>(["Brian"]);
user.VoicesEnabled = new HashSet<string>([user.DefaultTTSVoice]);
else
user.VoicesEnabled = new HashSet<string>(voicesEnabled.Select(v => v));
_logger.Information($"{user.VoicesEnabled.Count} TTS voices have been enabled.");
@ -253,13 +264,10 @@ namespace TwitchChatTTS
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.");
var redemptionActions = await hermes.FetchRedeemableActions();
var redemptions = await hermes.FetchRedemptions();
_redemptionManager.Initialize(redemptions, redemptionActions.ToDictionary(a => a.Name, a => a));
_logger.Information($"Redemption Manager has been initialized with {redemptionActions.Count()} actions & {redemptions.Count()} redemptions.");
_chatterGroupManager.Clear();
_permissionManager.Clear();
// var redemptionActions = await hermes.FetchRedeemableActions();
// var redemptions = await hermes.FetchRedemptions();
// _redemptionManager.Initialize(redemptions, redemptionActions.ToDictionary(a => a.Name, a => a));
// _logger.Information($"Redemption Manager has been initialized with {redemptionActions.Count()} actions & {redemptions.Count()} redemptions.");
var groups = await hermes.FetchGroups();
var groupsById = groups.ToDictionary(g => g.Id, g => g);
@ -296,22 +304,12 @@ namespace TwitchChatTTS
{
try
{
_logger.Information("Initializing hermes websocket client.");
var hermesClient = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("hermes");
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,
});
_hermes.Initialize();
await _hermes.Connect();
}
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
{
_logger.Information("Initializing 7tv websocket client.");
var sevenClient = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("7tv");
if (string.IsNullOrWhiteSpace(_user.SevenEmoteSetId))
{
_logger.Warning("Could not fetch 7tv emotes.");
return;
}
var url = $"{SevenApiClient.WEBSOCKET_URL}@emote_set.*<object_id={_user.SevenEmoteSetId}>";
_logger.Debug($"Attempting to connect to {url}");
await sevenClient.ConnectAsync($"{url}");
_sevenManager.Initialize();
await _sevenManager.Connect();
}
catch (Exception)
catch (Exception e)
{
_logger.Warning("Connecting to 7tv failed. Skipping 7tv websockets.");
_logger.Error(e, "Connecting to 7tv failed. Skipping 7tv websockets.");
}
}
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
{
var obsClient = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("obs");
var url = $"ws://{_configuration.Obs.Host.Trim()}:{_configuration.Obs.Port}";
_logger.Debug($"Initializing OBS websocket client. Attempting to connect to {url}");
await obsClient.ConnectAsync(url);
_obsManager.Initialize();
await _obsManager.Connect();
}
catch (Exception)
{
@ -367,7 +349,7 @@ namespace TwitchChatTTS
return null;
}
var channels = _configuration.Twitch.Channels ?? [username];
var channels = _configuration.Twitch?.Channels ?? [username];
_logger.Information("Twitch channels: " + string.Join(", ", channels));
twitchapiclient.InitializeClient(username, channels);
twitchapiclient.InitializePublisher();
@ -381,15 +363,7 @@ namespace TwitchChatTTS
if (result.Status != MessageStatus.None || result.Emotes == null || !result.Emotes.Any())
return;
var ws = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("hermes");
await ws.Send(8, new EmoteUsageMessage()
{
MessageId = e.ChatMessage.Id,
DateTime = DateTime.UtcNow,
BroadcasterId = result.BroadcasterId,
ChatterId = result.ChatterId,
Emotes = result.Emotes
});
await _hermes.SendEmoteUsage(e.ChatMessage.Id, result.ChatterId, result.Emotes);
}
catch (Exception ex)
{
@ -400,9 +374,9 @@ namespace TwitchChatTTS
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();
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 CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common;
using HermesSocketLibrary.Requests.Messages;
using Microsoft.Extensions.DependencyInjection;
using org.mariuszgromada.math.mxparser;
using Serilog;
using TwitchChatTTS.Hermes.Socket;
using TwitchChatTTS.OBS.Socket.Data;
using TwitchChatTTS.OBS.Socket.Manager;
@ -14,7 +16,7 @@ namespace TwitchChatTTS.Twitch.Redemptions
private readonly IDictionary<string, IList<RedeemableAction>> _store;
private readonly User _user;
private readonly OBSManager _obsManager;
private readonly SocketClient<WebSocketMessage> _hermesClient;
private readonly HermesSocketClient _hermes;
private readonly ILogger _logger;
private readonly Random _random;
private bool _isReady;
@ -23,13 +25,13 @@ namespace TwitchChatTTS.Twitch.Redemptions
public RedemptionManager(
User user,
OBSManager obsManager,
[FromKeyedServices("hermes")] SocketClient<WebSocketMessage> hermesClient,
[FromKeyedServices("hermes")] SocketClient<WebSocketMessage> hermes,
ILogger logger)
{
_store = new Dictionary<string, IList<RedeemableAction>>();
_user = user;
_obsManager = obsManager;
_hermesClient = hermesClient;
_hermes = (hermes as HermesSocketClient)!;
_logger = logger;
_random = new Random();
_isReady = false;
@ -46,6 +48,14 @@ namespace TwitchChatTTS.Twitch.Redemptions
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
{
switch (action.Type)
@ -53,12 +63,12 @@ namespace TwitchChatTTS.Twitch.Redemptions
case "WRITE_TO_FILE":
Directory.CreateDirectory(Path.GetDirectoryName(action.Data["file_path"]));
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;
case "APPEND_TO_FILE":
Directory.CreateDirectory(Path.GetDirectoryName(action.Data["file_path"]));
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;
case "OBS_TRANSFORM":
var type = typeof(OBSTransformationData);
@ -74,29 +84,30 @@ namespace TwitchChatTTS.Twitch.Redemptions
PropertyInfo? prop = type.GetProperty(propertyName, BindingFlags.Public | BindingFlags.Instance);
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;
}
var currentValue = prop.GetValue(d);
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.addConstants(new Constant("x", (double?)currentValue ?? 0.0d));
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;
}
var newValue = expression.calculate();
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;
case "TOGGLE_OBS_VISIBILITY":
@ -113,63 +124,78 @@ namespace TwitchChatTTS.Twitch.Redemptions
await Task.Delay(int.Parse(action.Data["sleep"]));
break;
case "SPECIFIC_TTS_VOICE":
var voiceId = _user.VoicesAvailable.Keys.First(id => _user.VoicesAvailable[id].ToLower() == action.Data["tts_voice"].ToLower());
if (voiceId == null)
case "RANDOM_TTS_VOICE":
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;
}
var voiceName = _user.VoicesAvailable[voiceId];
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;
}
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",
Data = new Dictionary<string, object>() { { "chatter", senderId }, { "voice", voiceId } }
});
_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;
await _hermes.UpdateTTSUser(senderId, voiceId);
_logger.Debug($"Sent request to create chat TTS voice [voice: {voiceName}][chatter id: {senderId}][source: redemption][chatter: {senderDisplayName}][chatter id: {senderId}]");
}
if (voicesEnabled.Count <= 1)
else
{
_logger.Warning($"There are not enough TTS voices enabled to randomize [voice pool size: {voicesEnabled.Count}]");
return;
await _hermes.CreateTTSUser(senderId, voiceId);
_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;
case "AUDIO_FILE":
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;
}
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;
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;
}
}
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();
var ordered = redemptions.OrderBy(r => r.Order);
var ordered = redemptions.Where(r => r != null).OrderBy(r => r.Order);
foreach (var redemption in ordered)
{
if (redemption.ActionName == null)
{
_logger.Warning("Null value found for the action name of a redemption.");
continue;
}
try
{
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.Models;
using TwitchLib.Communication.Events;
using Microsoft.Extensions.DependencyInjection;
using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common;
using TwitchLib.PubSub.Interfaces;
using TwitchLib.Client.Interfaces;
using TwitchChatTTS.OBS.Socket;
using TwitchChatTTS.Twitch.Redemptions;
public class TwitchApiClient
{
private readonly RedemptionManager _redemptionManager;
private readonly HermesApiClient _hermesApiClient;
private readonly Configuration _configuration;
private readonly TwitchBotAuth _token;
private readonly ITwitchClient _client;
private readonly ITwitchPubSub _publisher;
private readonly WebClientWrap _web;
private readonly IServiceProvider _serviceProvider;
private readonly User _user;
private readonly Configuration _configuration;
private readonly TwitchBotAuth _token;
private readonly ILogger _logger;
private readonly WebClientWrap _web;
private bool _initialized;
private string _broadcasterId;
public TwitchApiClient(
RedemptionManager redemptionManager,
HermesApiClient hermesApiClient,
Configuration configuration,
TwitchBotAuth token,
ITwitchClient twitchClient,
ITwitchPubSub twitchPublisher,
IServiceProvider serviceProvider,
RedemptionManager redemptionManager,
HermesApiClient hermesApiClient,
User user,
Configuration configuration,
TwitchBotAuth token,
ILogger logger
)
{
_redemptionManager = redemptionManager;
_hermesApiClient = hermesApiClient;
_configuration = configuration;
_token = token;
_client = twitchClient;
_publisher = twitchPublisher;
_serviceProvider = serviceProvider;
_user = user;
_configuration = configuration;
_token = token;
_logger = logger;
_initialized = false;
_broadcasterId = string.Empty;
@ -88,7 +84,7 @@ public class TwitchApiClient
}
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.");
else
_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)
{
ConnectionCredentials credentials = new ConnectionCredentials(username, _token?.AccessToken);
ConnectionCredentials credentials = new ConnectionCredentials(username, _token!.AccessToken);
_client.Initialize(credentials, channels.Distinct().ToList());
if (_initialized)
@ -130,7 +126,7 @@ public class TwitchApiClient
_client.OnConnected += async Task (object? s, OnConnectedArgs e) =>
{
_logger.Information("-----------------------------------------------------------");
_logger.Information("Twitch API client connected.");
};
_client.OnIncorrectLogin += async Task (object? s, OnIncorrectLoginArgs e) =>
@ -139,9 +135,10 @@ public class TwitchApiClient
_logger.Information("Attempting to re-authorize.");
await Authorize(_broadcasterId);
await _client.DisconnectAsync();
await Task.Delay(TimeSpan.FromSeconds(1));
await _client.ConnectAsync();
_client.SetConnectionCredentials(new ConnectionCredentials(_user.TwitchUsername, _token!.AccessToken));
await Task.Delay(TimeSpan.FromSeconds(3));
await _client.ReconnectAsync();
};
_client.OnConnectionError += async Task (object? s, OnConnectionErrorArgs e) =>
@ -156,6 +153,8 @@ public class TwitchApiClient
{
_logger.Error(e.Exception, "Twitch API client error.");
};
_client.OnDisconnected += async Task (s, e) => _logger.Warning("Twitch API client disconnected.");
}
public void InitializePublisher()
@ -171,38 +170,37 @@ public class TwitchApiClient
_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}]");
};
_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}]");
var actions = _redemptionManager.Get(e.RewardRedeemed.Redemption.Reward.Id);
if (!actions.Any())
try
{
_logger.Debug($"No redemable actions for this redeem was found [redeem: {e.RewardRedeemed.Redemption.Reward.Title}][redeem id: {e.RewardRedeemed.Redemption.Reward.Id}][transaction: {e.RewardRedeemed.Redemption.Id}]");
return;
}
_logger.Debug($"Found {actions.Count} actions for this Twitch channel point redemption [redeem: {e.RewardRedeemed.Redemption.Reward.Title}][redeem id: {e.RewardRedeemed.Redemption.Reward.Id}][transaction: {e.RewardRedeemed.Redemption.Id}]");
var actions = _redemptionManager.Get(e.RewardRedeemed.Redemption.Reward.Id);
if (!actions.Any())
{
_logger.Debug($"No redemable actions for this redeem was found [redeem: {e.RewardRedeemed.Redemption.Reward.Title}][redeem id: {e.RewardRedeemed.Redemption.Reward.Id}][transaction: {e.RewardRedeemed.Redemption.Id}]");
return;
}
_logger.Debug($"Found {actions.Count} actions for this Twitch channel point redemption [redeem: {e.RewardRedeemed.Redemption.Reward.Title}][redeem id: {e.RewardRedeemed.Redemption.Reward.Id}][transaction: {e.RewardRedeemed.Redemption.Id}]");
foreach (var action in actions)
try
{
await _redemptionManager.Execute(action, e.RewardRedeemed.Redemption.User.DisplayName, long.Parse(e.RewardRedeemed.Redemption.User.Id));
}
catch (Exception ex)
{
_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}]");
}
foreach (var action in actions)
try
{
await _redemptionManager.Execute(action, e.RewardRedeemed.Redemption.User.DisplayName, long.Parse(e.RewardRedeemed.Redemption.User.Id));
}
catch (Exception ex)
{
_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) =>

View File

@ -25,7 +25,7 @@
<PackageReference Include="Serilog.Sinks.File" Version="5.0.1-dev-00972" />
<PackageReference Include="Serilog.Sinks.RollingFile" Version="3.3.1-dev-00771" />
<PackageReference Include="Serilog.Sinks.Trace" Version="4.0.0" />
<PackageReference Include="System.Text.Json" Version="8.0.1" />
<PackageReference Include="System.Text.Json" Version="8.0.4" />
<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.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.Enums" 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.EventSub.Core" Version="2.5.1" />
<PackageReference Include="TwitchLib.Communication" Version="2.0.1" />
<PackageReference Include="TwitchLib.EventSub.Core" Version="2.5.3-preview-e1a92de" />
<PackageReference Include="TwitchLib.PubSub" Version="4.0.0-preview-f833b1ab1ebef37618dba3fbb1e0a661ff183af5" />
<PackageReference Include="NAudio.Core" Version="2.2.1" />
<PackageReference Include="TwitchLib.Api" Version="3.10.0-preview-e47ba7f" />