Added groups & permissions. Fixed TTS user creation. Better connection handling. Fixed 7tv reconnection.
This commit is contained in:
parent
9fb966474f
commit
e6b3819356
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,3 +2,4 @@ appsettings.json
|
||||
tts.config.yml
|
||||
obj/
|
||||
bin/
|
||||
logs/
|
@ -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,41 +52,41 @@ 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
|
||||
{
|
||||
var commandResult = await _commands.Execute(msg, m, groups);
|
||||
@ -98,21 +97,21 @@ public class ChatMessageHandler
|
||||
{
|
||||
_logger.Error(ex, $"Failed executing a chat command [message: {msg}][chatter: {m.Username}][chatter id: {m.UserId}][message id: {m.Id}]");
|
||||
}
|
||||
}
|
||||
|
||||
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++)
|
||||
|
@ -1,4 +1,4 @@
|
||||
namespace TwitchChatTTS.Seven
|
||||
namespace TwitchChatTTS.Chat
|
||||
{
|
||||
public class ChatterDatabase
|
||||
{
|
@ -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}]");
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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)
|
||||
{
|
||||
|
@ -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;
|
||||
|
17
Chat/Commands/Parameters/SimpleListedParameter.cs
Normal file
17
Chat/Commands/Parameters/SimpleListedParameter.cs
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
@ -86,16 +91,9 @@ namespace TwitchChatTTS.Chat.Commands
|
||||
_logger.Information($"Redemption Manager has been refreshed with {redemptionActions.Count()} actions & {redemptions.Count()} redemptions.");
|
||||
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.");
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "permissions":
|
||||
|
@ -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}]");
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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}]");
|
||||
}
|
||||
}
|
||||
}
|
@ -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}.");
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
_logger.Information($"Voice is disabled. Cannot switch to that voice [voice: {voice.Value}][username: {message.Username}]");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_user.VoicesSelected.ContainsKey(chatterId))
|
||||
{
|
||||
Type = _user.VoicesSelected.ContainsKey(chatterId) ? "update_tts_user" : "create_tts_user",
|
||||
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}].");
|
||||
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]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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(); }
|
10
Chat/Emotes/IEmoteDatabase.cs
Normal file
10
Chat/Emotes/IEmoteDatabase.cs
Normal 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);
|
||||
}
|
||||
}
|
@ -15,7 +15,7 @@ namespace TwitchChatTTS
|
||||
|
||||
public class TwitchConfiguration {
|
||||
public IEnumerable<string>? Channels;
|
||||
public bool? TtsWhenOffline;
|
||||
public bool TtsWhenOffline;
|
||||
}
|
||||
|
||||
public class OBSConfiguration {
|
||||
|
@ -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");
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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; }
|
||||
}
|
||||
}
|
@ -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()
|
||||
{
|
||||
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;
|
||||
@ -83,33 +299,42 @@ namespace TwitchChatTTS.Hermes.Socket
|
||||
}
|
||||
|
||||
private async Task Reconnect(ElapsedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
await ConnectAsync($"wss://{HermesSocketClient.BASE_URL}");
|
||||
Connected = true;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (Connected)
|
||||
{
|
||||
_logger.Information("Reconnected.");
|
||||
_reconnectTimer.Enabled = false;
|
||||
_heartbeatTimer.Enabled = true;
|
||||
LastHeartbeatReceived = DateTime.UtcNow;
|
||||
|
||||
if (_configuration.Hermes?.Token != null)
|
||||
await Send(1, new HermesLoginMessage()
|
||||
try
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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; }
|
||||
}
|
||||
}
|
@ -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()) { }
|
||||
}
|
||||
}
|
@ -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()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -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())
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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
57
Seven/SevenManager.cs
Normal 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}");
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
namespace TwitchChatTTS.Seven.Socket.Data
|
||||
{
|
||||
public class IdentifyMessage
|
||||
{
|
||||
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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 });
|
||||
|
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
using TwitchChatTTS.Chat.Emotes;
|
||||
|
||||
namespace TwitchChatTTS.Seven
|
||||
{
|
||||
public class UserDetails
|
||||
|
25
Startup.cs
25
Startup.cs
@ -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");
|
||||
|
134
TTS.cs
134
TTS.cs
@ -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());
|
||||
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;
|
||||
_sevenManager.Initialize();
|
||||
await _sevenManager.Connect();
|
||||
}
|
||||
var url = $"{SevenApiClient.WEBSOCKET_URL}@emote_set.*<object_id={_user.SevenEmoteSetId}>";
|
||||
_logger.Debug($"Attempting to connect to {url}");
|
||||
await sevenClient.ConnectAsync($"{url}");
|
||||
}
|
||||
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())
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
@ -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; }
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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,21 +170,15 @@ 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}]");
|
||||
|
||||
try
|
||||
{
|
||||
var actions = _redemptionManager.Get(e.RewardRedeemed.Redemption.Reward.Id);
|
||||
if (!actions.Any())
|
||||
{
|
||||
@ -203,6 +196,11 @@ public class TwitchApiClient
|
||||
{
|
||||
_logger.Error(ex, $"Failed to execute redeeemable action [action: {action.Name}][action type: {action.Type}][redeem: {e.RewardRedeemed.Redemption.Reward.Title}][redeem id: {e.RewardRedeemed.Redemption.Reward.Id}][transaction: {e.RewardRedeemed.Redemption.Id}]");
|
||||
}
|
||||
}
|
||||
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) =>
|
||||
|
@ -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" />
|
||||
|
Loading…
Reference in New Issue
Block a user