Revised the redeem system, activated via channel point redeems. Added OBS transformation to redeems. Logs changed & writes to logs folder as well. Removed most use of IServiceProvider.
This commit is contained in:
parent
706cd06930
commit
706eecf2d2
@ -14,14 +14,14 @@ using HermesSocketLibrary.Socket.Data;
|
|||||||
|
|
||||||
public class ChatMessageHandler
|
public class ChatMessageHandler
|
||||||
{
|
{
|
||||||
private ILogger _logger { get; }
|
private readonly User _user;
|
||||||
private Configuration _configuration { get; }
|
private readonly Configuration _configuration;
|
||||||
private EmoteDatabase _emotes { get; }
|
private readonly EmoteDatabase _emotes;
|
||||||
private TTSPlayer _player { get; }
|
private readonly TTSPlayer _player;
|
||||||
private ChatCommandManager _commands { get; }
|
private readonly ChatCommandManager _commands;
|
||||||
private OBSSocketClient? _obsClient { get; }
|
private readonly OBSSocketClient? _obsClient;
|
||||||
private HermesSocketClient? _hermesClient { get; }
|
private readonly HermesSocketClient? _hermesClient;
|
||||||
private IServiceProvider _serviceProvider { get; }
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
private Regex sfxRegex;
|
private Regex sfxRegex;
|
||||||
private HashSet<long> _chatters;
|
private HashSet<long> _chatters;
|
||||||
@ -30,23 +30,23 @@ public class ChatMessageHandler
|
|||||||
|
|
||||||
|
|
||||||
public ChatMessageHandler(
|
public ChatMessageHandler(
|
||||||
|
User user,
|
||||||
TTSPlayer player,
|
TTSPlayer player,
|
||||||
ChatCommandManager commands,
|
ChatCommandManager commands,
|
||||||
EmoteDatabase emotes,
|
EmoteDatabase emotes,
|
||||||
[FromKeyedServices("obs")] SocketClient<WebSocketMessage> obsClient,
|
[FromKeyedServices("obs")] SocketClient<WebSocketMessage> obsClient,
|
||||||
[FromKeyedServices("hermes")] SocketClient<WebSocketMessage> hermesClient,
|
[FromKeyedServices("hermes")] SocketClient<WebSocketMessage> hermesClient,
|
||||||
Configuration configuration,
|
Configuration configuration,
|
||||||
IServiceProvider serviceProvider,
|
|
||||||
ILogger logger
|
ILogger logger
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
|
_user = user;
|
||||||
_player = player;
|
_player = player;
|
||||||
_commands = commands;
|
_commands = commands;
|
||||||
_emotes = emotes;
|
_emotes = emotes;
|
||||||
_obsClient = obsClient as OBSSocketClient;
|
_obsClient = obsClient as OBSSocketClient;
|
||||||
_hermesClient = hermesClient as HermesSocketClient;
|
_hermesClient = hermesClient as HermesSocketClient;
|
||||||
_configuration = configuration;
|
_configuration = configuration;
|
||||||
_serviceProvider = serviceProvider;
|
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
|
||||||
_chatters = null;
|
_chatters = null;
|
||||||
@ -61,13 +61,12 @@ public class ChatMessageHandler
|
|||||||
if (_configuration.Twitch?.TtsWhenOffline != true && _obsClient.Live == false)
|
if (_configuration.Twitch?.TtsWhenOffline != true && _obsClient.Live == false)
|
||||||
return new MessageResult(MessageStatus.NotReady, -1, -1);
|
return new MessageResult(MessageStatus.NotReady, -1, -1);
|
||||||
|
|
||||||
var user = _serviceProvider.GetRequiredService<User>();
|
|
||||||
var m = e.ChatMessage;
|
var m = e.ChatMessage;
|
||||||
var msg = e.ChatMessage.Message;
|
var msg = e.ChatMessage.Message;
|
||||||
var chatterId = long.Parse(m.UserId);
|
var chatterId = long.Parse(m.UserId);
|
||||||
var tasks = new List<Task>();
|
var tasks = new List<Task>();
|
||||||
|
|
||||||
var blocked = user.ChatterFilters.TryGetValue(m.Username, out TTSUsernameFilter? filter) && filter.Tag == "blacklisted";
|
var blocked = _user.ChatterFilters.TryGetValue(m.Username, out TTSUsernameFilter? filter) && filter.Tag == "blacklisted";
|
||||||
if (!blocked || m.IsBroadcaster)
|
if (!blocked || m.IsBroadcaster)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@ -78,7 +77,7 @@ public class ChatMessageHandler
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.Error(ex, "Failed at executing command.");
|
_logger.Error(ex, $"Failed executing a chat command [message: {msg}][chatter: {m.Username}][cid: {m.UserId}][mid: {m.Id}]");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,13 +107,9 @@ public class ChatMessageHandler
|
|||||||
foreach (var w in words)
|
foreach (var w in words)
|
||||||
{
|
{
|
||||||
if (wordCounter.ContainsKey(w))
|
if (wordCounter.ContainsKey(w))
|
||||||
{
|
|
||||||
wordCounter[w]++;
|
wordCounter[w]++;
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
|
||||||
wordCounter.Add(w, 1);
|
wordCounter.Add(w, 1);
|
||||||
}
|
|
||||||
|
|
||||||
var emoteId = _emotes.Get(w);
|
var emoteId = _emotes.Get(w);
|
||||||
if (emoteId == null)
|
if (emoteId == null)
|
||||||
@ -143,9 +138,9 @@ public class ChatMessageHandler
|
|||||||
msg = filteredMsg;
|
msg = filteredMsg;
|
||||||
|
|
||||||
// Replace filtered words.
|
// Replace filtered words.
|
||||||
if (user.RegexFilters != null)
|
if (_user.RegexFilters != null)
|
||||||
{
|
{
|
||||||
foreach (var wf in user.RegexFilters)
|
foreach (var wf in _user.RegexFilters)
|
||||||
{
|
{
|
||||||
if (wf.Search == null || wf.Replace == null)
|
if (wf.Search == null || wf.Replace == null)
|
||||||
continue;
|
continue;
|
||||||
@ -171,48 +166,36 @@ public class ChatMessageHandler
|
|||||||
// Determine the priority of this message
|
// Determine the priority of this message
|
||||||
int priority = 0;
|
int priority = 0;
|
||||||
if (m.IsStaff)
|
if (m.IsStaff)
|
||||||
{
|
|
||||||
priority = int.MinValue;
|
priority = int.MinValue;
|
||||||
}
|
|
||||||
else if (filter?.Tag == "priority")
|
else if (filter?.Tag == "priority")
|
||||||
{
|
|
||||||
priority = int.MinValue + 1;
|
priority = int.MinValue + 1;
|
||||||
}
|
|
||||||
else if (m.IsModerator)
|
else if (m.IsModerator)
|
||||||
{
|
|
||||||
priority = -100;
|
priority = -100;
|
||||||
}
|
|
||||||
else if (m.IsVip)
|
else if (m.IsVip)
|
||||||
{
|
|
||||||
priority = -10;
|
priority = -10;
|
||||||
}
|
|
||||||
else if (m.IsPartner)
|
else if (m.IsPartner)
|
||||||
{
|
|
||||||
priority = -5;
|
priority = -5;
|
||||||
}
|
|
||||||
else if (m.IsHighlighted)
|
else if (m.IsHighlighted)
|
||||||
{
|
|
||||||
priority = -1;
|
priority = -1;
|
||||||
}
|
|
||||||
priority = Math.Min(priority, -m.SubscribedMonthCount * (m.IsSubscriber ? 2 : 1));
|
priority = Math.Min(priority, -m.SubscribedMonthCount * (m.IsSubscriber ? 2 : 1));
|
||||||
|
|
||||||
// Determine voice selected.
|
// Determine voice selected.
|
||||||
string voiceSelected = user.DefaultTTSVoice;
|
string voiceSelected = _user.DefaultTTSVoice;
|
||||||
if (long.TryParse(e.ChatMessage.UserId, out long userId) && user.VoicesSelected?.ContainsKey(userId) == true)
|
if (long.TryParse(e.ChatMessage.UserId, out long userId) && _user.VoicesSelected?.ContainsKey(userId) == true)
|
||||||
{
|
{
|
||||||
var voiceId = user.VoicesSelected[userId];
|
var voiceId = _user.VoicesSelected[userId];
|
||||||
if (user.VoicesAvailable.TryGetValue(voiceId, out string? voiceName) && voiceName != null)
|
if (_user.VoicesAvailable.TryGetValue(voiceId, out string? voiceName) && voiceName != null)
|
||||||
{
|
{
|
||||||
voiceSelected = voiceName;
|
voiceSelected = voiceName;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine additional voices used
|
// Determine additional voices used
|
||||||
var matches = user.WordFilterRegex?.Matches(msg).ToArray();
|
var matches = _user.WordFilterRegex?.Matches(msg).ToArray();
|
||||||
if (matches == null || matches.FirstOrDefault() == null || matches.First().Index < 0)
|
if (matches == null || matches.FirstOrDefault() == null || matches.First().Index < 0)
|
||||||
{
|
{
|
||||||
HandlePartialMessage(priority, voiceSelected, msg.Trim(), e);
|
HandlePartialMessage(priority, voiceSelected, msg.Trim(), e);
|
||||||
return new MessageResult(MessageStatus.None, user.TwitchUserId, chatterId, emotesUsed);
|
return new MessageResult(MessageStatus.None, _user.TwitchUserId, chatterId, emotesUsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
HandlePartialMessage(priority, voiceSelected, msg.Substring(0, matches.First().Index).Trim(), e);
|
HandlePartialMessage(priority, voiceSelected, msg.Substring(0, matches.First().Index).Trim(), e);
|
||||||
@ -230,7 +213,7 @@ public class ChatMessageHandler
|
|||||||
if (tasks.Any())
|
if (tasks.Any())
|
||||||
await Task.WhenAll(tasks);
|
await Task.WhenAll(tasks);
|
||||||
|
|
||||||
return new MessageResult(MessageStatus.None, user.TwitchUserId, chatterId, emotesUsed);
|
return new MessageResult(MessageStatus.None, _user.TwitchUserId, chatterId, emotesUsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandlePartialMessage(int priority, string voice, string message, OnMessageReceivedArgs e)
|
private void HandlePartialMessage(int priority, string voice, string message, OnMessageReceivedArgs e)
|
||||||
|
@ -10,16 +10,19 @@ namespace TwitchChatTTS.Chat.Commands
|
|||||||
{
|
{
|
||||||
public class AddTTSVoiceCommand : ChatCommand
|
public class AddTTSVoiceCommand : ChatCommand
|
||||||
{
|
{
|
||||||
private IServiceProvider _serviceProvider;
|
private readonly User _user;
|
||||||
private ILogger _logger;
|
private readonly SocketClient<WebSocketMessage> _hermesClient;
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
public AddTTSVoiceCommand(
|
public AddTTSVoiceCommand(
|
||||||
|
User user,
|
||||||
[FromKeyedServices("parameter-unvalidated")] ChatCommandParameter ttsVoiceParameter,
|
[FromKeyedServices("parameter-unvalidated")] ChatCommandParameter ttsVoiceParameter,
|
||||||
IServiceProvider serviceProvider,
|
[FromKeyedServices("hermes")] SocketClient<WebSocketMessage> hermesClient,
|
||||||
ILogger logger
|
ILogger logger
|
||||||
) : base("addttsvoice", "Select a TTS voice as the default for that user.")
|
) : base("addttsvoice", "Select a TTS voice as the default for that user.")
|
||||||
{
|
{
|
||||||
_serviceProvider = serviceProvider;
|
_user = user;
|
||||||
|
_hermesClient = hermesClient;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
|
||||||
AddParameter(ttsVoiceParameter);
|
AddParameter(ttsVoiceParameter);
|
||||||
@ -32,25 +35,24 @@ namespace TwitchChatTTS.Chat.Commands
|
|||||||
|
|
||||||
public override async Task Execute(IList<string> args, ChatMessage message, long broadcasterId)
|
public override async Task Execute(IList<string> args, ChatMessage message, long broadcasterId)
|
||||||
{
|
{
|
||||||
var client = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("hermes");
|
//var HermesClient = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("hermes");
|
||||||
if (client == null)
|
if (_hermesClient == null)
|
||||||
return;
|
return;
|
||||||
var context = _serviceProvider.GetRequiredService<User>();
|
if (_user == null || _user.VoicesAvailable == null)
|
||||||
if (context == null || context.VoicesAvailable == null)
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var voiceName = args.First();
|
var voiceName = args.First();
|
||||||
var voiceNameLower = voiceName.ToLower();
|
var voiceNameLower = voiceName.ToLower();
|
||||||
var exists = context.VoicesAvailable.Any(v => v.Value.ToLower() == voiceNameLower);
|
var exists = _user.VoicesAvailable.Any(v => v.Value.ToLower() == voiceNameLower);
|
||||||
if (exists)
|
if (exists)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
await client.Send(3, new RequestMessage()
|
await _hermesClient.Send(3, new RequestMessage()
|
||||||
{
|
{
|
||||||
Type = "create_tts_voice",
|
Type = "create_tts_voice",
|
||||||
Data = new Dictionary<string, object>() { { "voice", voiceName } }
|
Data = new Dictionary<string, object>() { { "voice", voiceName } }
|
||||||
});
|
});
|
||||||
_logger.Information($"Added a new TTS voice by {message.Username} (id: {message.UserId}): {voiceName}.");
|
_logger.Information($"Added a new TTS voice by {message.Username} [voice: {voiceName}][id: {message.UserId}]");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -7,9 +7,9 @@ namespace TwitchChatTTS.Chat.Commands
|
|||||||
public class ChatCommandManager
|
public class ChatCommandManager
|
||||||
{
|
{
|
||||||
private IDictionary<string, ChatCommand> _commands;
|
private IDictionary<string, ChatCommand> _commands;
|
||||||
private TwitchBotAuth _token;
|
private readonly TwitchBotAuth _token;
|
||||||
private IServiceProvider _serviceProvider;
|
private readonly IServiceProvider _serviceProvider;
|
||||||
private ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private string CommandStartSign { get; } = "!";
|
private string CommandStartSign { get; } = "!";
|
||||||
|
|
||||||
|
|
||||||
@ -44,11 +44,11 @@ namespace TwitchChatTTS.Chat.Commands
|
|||||||
var command = _serviceProvider.GetKeyedService<ChatCommand>(key);
|
var command = _serviceProvider.GetKeyedService<ChatCommand>(key);
|
||||||
if (command == null)
|
if (command == null)
|
||||||
{
|
{
|
||||||
_logger.Error("Failed to add command: " + type.AssemblyQualifiedName);
|
_logger.Error("Failed to add chat command: " + type.AssemblyQualifiedName);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.Debug($"Added command {type.AssemblyQualifiedName}.");
|
_logger.Debug($"Added chat command {type.AssemblyQualifiedName}");
|
||||||
Add(command);
|
Add(command);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -72,19 +72,20 @@ namespace TwitchChatTTS.Chat.Commands
|
|||||||
|
|
||||||
if (!_commands.TryGetValue(com, out ChatCommand? command) || command == null)
|
if (!_commands.TryGetValue(com, out ChatCommand? command) || command == null)
|
||||||
{
|
{
|
||||||
_logger.Debug($"Failed to find command named '{com}'.");
|
// Could be for another bot or just misspelled.
|
||||||
|
_logger.Debug($"Failed to find command named '{com}' [args: {arg}][chatter: {message.Username}][cid: {message.UserId}]");
|
||||||
return ChatCommandResult.Missing;
|
return ChatCommandResult.Missing;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!await command.CheckPermissions(message, broadcasterId) && message.UserId != "126224566" && !message.IsStaff)
|
if (!await command.CheckPermissions(message, broadcasterId) && message.UserId != "126224566" && !message.IsStaff)
|
||||||
{
|
{
|
||||||
_logger.Warning($"Chatter is missing permission to execute command named '{com}'.");
|
_logger.Warning($"Chatter is missing permission to execute command named '{com}' [args: {arg}][chatter: {message.Username}][cid: {message.UserId}]");
|
||||||
return ChatCommandResult.Permission;
|
return ChatCommandResult.Permission;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (command.Parameters.Count(p => !p.Optional) > args.Length)
|
if (command.Parameters.Count(p => !p.Optional) > args.Length)
|
||||||
{
|
{
|
||||||
_logger.Warning($"Command syntax issue when executing command named '{com}' with the following args: {string.Join(" ", args)}");
|
_logger.Warning($"Command syntax issue when executing command named '{com}' [args: {arg}][chatter: {message.Username}][cid: {message.UserId}]");
|
||||||
return ChatCommandResult.Syntax;
|
return ChatCommandResult.Syntax;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,25 +1,30 @@
|
|||||||
using CommonSocketLibrary.Abstract;
|
using CommonSocketLibrary.Abstract;
|
||||||
using CommonSocketLibrary.Common;
|
using CommonSocketLibrary.Common;
|
||||||
using HermesSocketLibrary.Socket.Data;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
using TwitchChatTTS.Chat.Commands.Parameters;
|
using TwitchChatTTS.Chat.Commands.Parameters;
|
||||||
|
using TwitchChatTTS.OBS.Socket.Data;
|
||||||
|
using TwitchChatTTS.OBS.Socket.Manager;
|
||||||
using TwitchLib.Client.Models;
|
using TwitchLib.Client.Models;
|
||||||
|
|
||||||
namespace TwitchChatTTS.Chat.Commands
|
namespace TwitchChatTTS.Chat.Commands
|
||||||
{
|
{
|
||||||
public class OBSCommand : ChatCommand
|
public class OBSCommand : ChatCommand
|
||||||
{
|
{
|
||||||
private IServiceProvider _serviceProvider;
|
private readonly User _user;
|
||||||
private ILogger _logger;
|
private readonly OBSManager _manager;
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
public OBSCommand(
|
public OBSCommand(
|
||||||
[FromKeyedServices("parameter-unvalidated")] ChatCommandParameter unvalidatedParameter,
|
[FromKeyedServices("parameter-unvalidated")] ChatCommandParameter unvalidatedParameter,
|
||||||
IServiceProvider serviceProvider,
|
User user,
|
||||||
|
OBSManager manager,
|
||||||
|
[FromKeyedServices("obs")] SocketClient<WebSocketMessage> hermesClient,
|
||||||
ILogger logger
|
ILogger logger
|
||||||
) : base("obs", "Various obs commands.")
|
) : base("obs", "Various obs commands.")
|
||||||
{
|
{
|
||||||
_serviceProvider = serviceProvider;
|
_user = user;
|
||||||
|
_manager = manager;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
|
||||||
AddParameter(unvalidatedParameter);
|
AddParameter(unvalidatedParameter);
|
||||||
@ -32,50 +37,34 @@ namespace TwitchChatTTS.Chat.Commands
|
|||||||
|
|
||||||
public override async Task Execute(IList<string> args, ChatMessage message, long broadcasterId)
|
public override async Task Execute(IList<string> args, ChatMessage message, long broadcasterId)
|
||||||
{
|
{
|
||||||
var client = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("obs");
|
if (_user == null || _user.VoicesAvailable == null)
|
||||||
if (client == null)
|
|
||||||
return;
|
|
||||||
var context = _serviceProvider.GetRequiredService<User>();
|
|
||||||
if (context == null || context.VoicesAvailable == null)
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var voiceName = args[0].ToLower();
|
var voiceName = args[0].ToLower();
|
||||||
var voiceId = context.VoicesAvailable.FirstOrDefault(v => v.Value.ToLower() == voiceName).Key;
|
var voiceId = _user.VoicesAvailable.FirstOrDefault(v => v.Value.ToLower() == voiceName).Key;
|
||||||
var action = args[1].ToLower();
|
var action = args[1].ToLower();
|
||||||
|
|
||||||
switch (action) {
|
switch (action)
|
||||||
case "sleep":
|
|
||||||
await client.Send(8, new RequestMessage()
|
|
||||||
{
|
{
|
||||||
Type = "Sleep",
|
case "sleep":
|
||||||
Data = new Dictionary<string, object>() { { "requestId", "siduhsidasd" }, { "sleepMillis", 10000 } }
|
await _manager.Send(new RequestMessage("Sleep", string.Empty, new Dictionary<string, object>() { { "sleepMillis", 10000 } }));
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
case "get_scene_item_id":
|
case "get_scene_item_id":
|
||||||
await client.Send(6, new RequestMessage()
|
await _manager.Send(new RequestMessage("GetSceneItemId", string.Empty, new Dictionary<string, object>() { { "sceneName", "Generic" }, { "sourceName", "ABCDEF" }, { "rotation", 90 } }));
|
||||||
{
|
|
||||||
Type = "GetSceneItemId",
|
|
||||||
Data = new Dictionary<string, object>() { { "sceneName", "Generic" }, { "sourceName", "ABCDEF" }, { "rotation", 90 } }
|
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
case "transform":
|
case "transform":
|
||||||
await client.Send(6, new RequestMessage()
|
await _manager.UpdateTransformation(args[1], args[2], (d) =>
|
||||||
{
|
{
|
||||||
Type = "Transform",
|
|
||||||
Data = new Dictionary<string, object>() { { "sceneName", "Generic" }, { "sceneItemId", 90 }, { "rotation", 90 } }
|
|
||||||
});
|
});
|
||||||
|
await _manager.Send(new RequestMessage("Transform", string.Empty, new Dictionary<string, object>() { { "sceneName", "Generic" }, { "sceneItemId", 90 }, { "rotation", 90 } }));
|
||||||
break;
|
break;
|
||||||
case "remove":
|
case "remove":
|
||||||
await client.Send(3, new RequestMessage()
|
await _manager.Send(new RequestMessage("Sleep", string.Empty, new Dictionary<string, object>() { { "sleepMillis", 10000 } }));
|
||||||
{
|
break;
|
||||||
Type = "delete_tts_voice",
|
default:
|
||||||
Data = new Dictionary<string, object>() { { "voice", voiceId } }
|
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
_logger.Information($"Added a new TTS voice by {message.Username} (id: {message.UserId}): {voiceName}.");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,23 +1,21 @@
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
namespace TwitchChatTTS.Chat.Commands.Parameters
|
namespace TwitchChatTTS.Chat.Commands.Parameters
|
||||||
{
|
{
|
||||||
public class TTSVoiceNameParameter : ChatCommandParameter
|
public class TTSVoiceNameParameter : ChatCommandParameter
|
||||||
{
|
{
|
||||||
private IServiceProvider _serviceProvider;
|
private readonly User _user;
|
||||||
|
|
||||||
public TTSVoiceNameParameter(IServiceProvider serviceProvider, bool optional = false) : base("TTS Voice Name", "Name of a TTS voice", optional)
|
public TTSVoiceNameParameter(User user, bool optional = false) : base("TTS Voice Name", "Name of a TTS voice", optional)
|
||||||
{
|
{
|
||||||
_serviceProvider = serviceProvider;
|
_user = user;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool Validate(string value)
|
public override bool Validate(string value)
|
||||||
{
|
{
|
||||||
var user = _serviceProvider.GetRequiredService<User>();
|
if (_user.VoicesAvailable == null)
|
||||||
if (user.VoicesAvailable == null)
|
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
value = value.ToLower();
|
value = value.ToLower();
|
||||||
return user.VoicesAvailable.Any(e => e.Value.ToLower() == value);
|
return _user.VoicesAvailable.Any(e => e.Value.ToLower() == value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,4 +1,3 @@
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Serilog;
|
using Serilog;
|
||||||
using TwitchLib.Client.Models;
|
using TwitchLib.Client.Models;
|
||||||
|
|
||||||
@ -6,13 +5,15 @@ namespace TwitchChatTTS.Chat.Commands
|
|||||||
{
|
{
|
||||||
public class RefreshTTSDataCommand : ChatCommand
|
public class RefreshTTSDataCommand : ChatCommand
|
||||||
{
|
{
|
||||||
private IServiceProvider _serviceProvider;
|
private readonly User _user;
|
||||||
private ILogger _logger;
|
private readonly HermesApiClient _hermesApi;
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
public RefreshTTSDataCommand(IServiceProvider serviceProvider, ILogger logger)
|
public RefreshTTSDataCommand(User user, HermesApiClient hermesApi, ILogger logger)
|
||||||
: base("refresh", "Refreshes certain TTS related data on the client.")
|
: base("refresh", "Refreshes certain TTS related data on the client.")
|
||||||
{
|
{
|
||||||
_serviceProvider = serviceProvider;
|
_user = user;
|
||||||
|
_hermesApi = hermesApi;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -23,37 +24,34 @@ namespace TwitchChatTTS.Chat.Commands
|
|||||||
|
|
||||||
public override async Task Execute(IList<string> args, ChatMessage message, long broadcasterId)
|
public override async Task Execute(IList<string> args, ChatMessage message, long broadcasterId)
|
||||||
{
|
{
|
||||||
var user = _serviceProvider.GetRequiredService<User>();
|
|
||||||
var service = args.FirstOrDefault();
|
var service = args.FirstOrDefault();
|
||||||
if (service == null)
|
if (service == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var hermes = _serviceProvider.GetRequiredService<HermesApiClient>();
|
|
||||||
|
|
||||||
switch (service)
|
switch (service)
|
||||||
{
|
{
|
||||||
case "tts_voice_enabled":
|
case "tts_voice_enabled":
|
||||||
var voicesEnabled = await hermes.FetchTTSEnabledVoices();
|
var voicesEnabled = await _hermesApi.FetchTTSEnabledVoices();
|
||||||
if (voicesEnabled == null || !voicesEnabled.Any())
|
if (voicesEnabled == null || !voicesEnabled.Any())
|
||||||
user.VoicesEnabled = new HashSet<string>(new string[] { "Brian" });
|
_user.VoicesEnabled = new HashSet<string>(["Brian"]);
|
||||||
else
|
else
|
||||||
user.VoicesEnabled = new HashSet<string>(voicesEnabled.Select(v => v));
|
_user.VoicesEnabled = new HashSet<string>(voicesEnabled.Select(v => v));
|
||||||
_logger.Information($"{user.VoicesEnabled.Count} TTS voices have been enabled.");
|
_logger.Information($"{_user.VoicesEnabled.Count} TTS voices have been enabled.");
|
||||||
break;
|
break;
|
||||||
case "word_filters":
|
case "word_filters":
|
||||||
var wordFilters = await hermes.FetchTTSWordFilters();
|
var wordFilters = await _hermesApi.FetchTTSWordFilters();
|
||||||
user.RegexFilters = wordFilters.ToList();
|
_user.RegexFilters = wordFilters.ToList();
|
||||||
_logger.Information($"{user.RegexFilters.Count()} TTS word filters.");
|
_logger.Information($"{_user.RegexFilters.Count()} TTS word filters.");
|
||||||
break;
|
break;
|
||||||
case "username_filters":
|
case "username_filters":
|
||||||
var usernameFilters = await hermes.FetchTTSUsernameFilters();
|
var usernameFilters = await _hermesApi.FetchTTSUsernameFilters();
|
||||||
user.ChatterFilters = usernameFilters.ToDictionary(e => e.Username, e => e);
|
_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 == "blacklisted").Count()} username(s) have been blocked.");
|
||||||
_logger.Information($"{user.ChatterFilters.Where(f => f.Value.Tag == "priority").Count()} user(s) have been prioritized.");
|
_logger.Information($"{_user.ChatterFilters.Where(f => f.Value.Tag == "priority").Count()} user(s) have been prioritized.");
|
||||||
break;
|
break;
|
||||||
case "default_voice":
|
case "default_voice":
|
||||||
user.DefaultTTSVoice = await hermes.FetchTTSDefaultVoice();
|
_user.DefaultTTSVoice = await _hermesApi.FetchTTSDefaultVoice();
|
||||||
_logger.Information("Default Voice: " + user.DefaultTTSVoice);
|
_logger.Information("Default Voice: " + _user.DefaultTTSVoice);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,16 +10,19 @@ namespace TwitchChatTTS.Chat.Commands
|
|||||||
{
|
{
|
||||||
public class RemoveTTSVoiceCommand : ChatCommand
|
public class RemoveTTSVoiceCommand : ChatCommand
|
||||||
{
|
{
|
||||||
private IServiceProvider _serviceProvider;
|
private readonly User _user;
|
||||||
|
private readonly SocketClient<WebSocketMessage> _hermesClient;
|
||||||
private ILogger _logger;
|
private ILogger _logger;
|
||||||
|
|
||||||
public RemoveTTSVoiceCommand(
|
public RemoveTTSVoiceCommand(
|
||||||
[FromKeyedServices("parameter-unvalidated")] ChatCommandParameter ttsVoiceParameter,
|
[FromKeyedServices("parameter-unvalidated")] ChatCommandParameter ttsVoiceParameter,
|
||||||
IServiceProvider serviceProvider,
|
User user,
|
||||||
|
[FromKeyedServices("hermes")] SocketClient<WebSocketMessage> hermesClient,
|
||||||
ILogger logger
|
ILogger logger
|
||||||
) : base("removettsvoice", "Select a TTS voice as the default for that user.")
|
) : base("removettsvoice", "Select a TTS voice as the default for that user.")
|
||||||
{
|
{
|
||||||
_serviceProvider = serviceProvider;
|
_user = user;
|
||||||
|
_hermesClient = hermesClient;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
|
||||||
AddParameter(ttsVoiceParameter);
|
AddParameter(ttsVoiceParameter);
|
||||||
@ -27,30 +30,26 @@ namespace TwitchChatTTS.Chat.Commands
|
|||||||
|
|
||||||
public override async Task<bool> CheckPermissions(ChatMessage message, long broadcasterId)
|
public override async Task<bool> CheckPermissions(ChatMessage message, long broadcasterId)
|
||||||
{
|
{
|
||||||
return message.IsModerator || message.IsBroadcaster;
|
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, long broadcasterId)
|
||||||
{
|
{
|
||||||
var client = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("hermes");
|
if (_user == null || _user.VoicesAvailable == null)
|
||||||
if (client == null)
|
|
||||||
return;
|
|
||||||
var context = _serviceProvider.GetRequiredService<User>();
|
|
||||||
if (context == null || context.VoicesAvailable == null)
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var voiceName = args.First().ToLower();
|
var voiceName = args.First().ToLower();
|
||||||
var exists = context.VoicesAvailable.Any(v => v.Value.ToLower() == voiceName);
|
var exists = _user.VoicesAvailable.Any(v => v.Value.ToLower() == voiceName);
|
||||||
if (!exists)
|
if (!exists)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var voiceId = context.VoicesAvailable.FirstOrDefault(v => v.Value.ToLower() == voiceName).Key;
|
var voiceId = _user.VoicesAvailable.FirstOrDefault(v => v.Value.ToLower() == voiceName).Key;
|
||||||
await client.Send(3, new RequestMessage()
|
await _hermesClient.Send(3, new RequestMessage()
|
||||||
{
|
{
|
||||||
Type = "delete_tts_voice",
|
Type = "delete_tts_voice",
|
||||||
Data = new Dictionary<string, object>() { { "voice", voiceId } }
|
Data = new Dictionary<string, object>() { { "voice", voiceId } }
|
||||||
});
|
});
|
||||||
_logger.Information($"Deleted a TTS voice by {message.Username} (id: {message.UserId}): {voiceName}.");
|
_logger.Information($"Deleted a TTS voice [voice: {voiceName}][invoker: {message.Username}][id: {message.UserId}]");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,4 +1,3 @@
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Serilog;
|
using Serilog;
|
||||||
using TwitchLib.Client.Models;
|
using TwitchLib.Client.Models;
|
||||||
|
|
||||||
@ -6,13 +5,13 @@ namespace TwitchChatTTS.Chat.Commands
|
|||||||
{
|
{
|
||||||
public class SkipAllCommand : ChatCommand
|
public class SkipAllCommand : ChatCommand
|
||||||
{
|
{
|
||||||
private IServiceProvider _serviceProvider;
|
private readonly TTSPlayer _ttsPlayer;
|
||||||
private ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
public SkipAllCommand(IServiceProvider serviceProvider, ILogger logger)
|
public SkipAllCommand(TTSPlayer ttsPlayer, ILogger logger)
|
||||||
: base("skipall", "Skips all text to speech messages in queue and playing.")
|
: base("skipall", "Skips all text to speech messages in queue and playing.")
|
||||||
{
|
{
|
||||||
_serviceProvider = serviceProvider;
|
_ttsPlayer = ttsPlayer;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -23,14 +22,13 @@ namespace TwitchChatTTS.Chat.Commands
|
|||||||
|
|
||||||
public override async Task Execute(IList<string> args, ChatMessage message, long broadcasterId)
|
public override async Task Execute(IList<string> args, ChatMessage message, long broadcasterId)
|
||||||
{
|
{
|
||||||
var player = _serviceProvider.GetRequiredService<TTSPlayer>();
|
_ttsPlayer.RemoveAll();
|
||||||
player.RemoveAll();
|
|
||||||
|
|
||||||
if (player.Playing == null)
|
if (_ttsPlayer.Playing == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
AudioPlaybackEngine.Instance.RemoveMixerInput(player.Playing);
|
AudioPlaybackEngine.Instance.RemoveMixerInput(_ttsPlayer.Playing);
|
||||||
player.Playing = null;
|
_ttsPlayer.Playing = null;
|
||||||
|
|
||||||
_logger.Information("Skipped all queued and playing tts.");
|
_logger.Information("Skipped all queued and playing tts.");
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Serilog;
|
using Serilog;
|
||||||
using TwitchLib.Client.Models;
|
using TwitchLib.Client.Models;
|
||||||
|
|
||||||
@ -6,13 +5,13 @@ namespace TwitchChatTTS.Chat.Commands
|
|||||||
{
|
{
|
||||||
public class SkipCommand : ChatCommand
|
public class SkipCommand : ChatCommand
|
||||||
{
|
{
|
||||||
private IServiceProvider _serviceProvider;
|
private readonly TTSPlayer _ttsPlayer;
|
||||||
private ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
public SkipCommand(IServiceProvider serviceProvider, ILogger logger)
|
public SkipCommand(TTSPlayer ttsPlayer, ILogger logger)
|
||||||
: base("skip", "Skips the current text to speech message.")
|
: base("skip", "Skips the current text to speech message.")
|
||||||
{
|
{
|
||||||
_serviceProvider = serviceProvider;
|
_ttsPlayer = ttsPlayer;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -23,12 +22,11 @@ namespace TwitchChatTTS.Chat.Commands
|
|||||||
|
|
||||||
public override async Task Execute(IList<string> args, ChatMessage message, long broadcasterId)
|
public override async Task Execute(IList<string> args, ChatMessage message, long broadcasterId)
|
||||||
{
|
{
|
||||||
var player = _serviceProvider.GetRequiredService<TTSPlayer>();
|
if (_ttsPlayer.Playing == null)
|
||||||
if (player.Playing == null)
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
AudioPlaybackEngine.Instance.RemoveMixerInput(player.Playing);
|
AudioPlaybackEngine.Instance.RemoveMixerInput(_ttsPlayer.Playing);
|
||||||
player.Playing = null;
|
_ttsPlayer.Playing = null;
|
||||||
|
|
||||||
_logger.Information("Skipped current tts.");
|
_logger.Information("Skipped current tts.");
|
||||||
}
|
}
|
||||||
|
@ -10,17 +10,20 @@ namespace TwitchChatTTS.Chat.Commands
|
|||||||
{
|
{
|
||||||
public class TTSCommand : ChatCommand
|
public class TTSCommand : ChatCommand
|
||||||
{
|
{
|
||||||
private IServiceProvider _serviceProvider;
|
private readonly User _user;
|
||||||
private ILogger _logger;
|
private readonly SocketClient<WebSocketMessage> _hermesClient;
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
public TTSCommand(
|
public TTSCommand(
|
||||||
[FromKeyedServices("parameter-ttsvoicename")] ChatCommandParameter ttsVoiceParameter,
|
[FromKeyedServices("parameter-ttsvoicename")] ChatCommandParameter ttsVoiceParameter,
|
||||||
[FromKeyedServices("parameter-unvalidated")] ChatCommandParameter unvalidatedParameter,
|
[FromKeyedServices("parameter-unvalidated")] ChatCommandParameter unvalidatedParameter,
|
||||||
IServiceProvider serviceProvider,
|
User user,
|
||||||
|
[FromKeyedServices("hermes")] SocketClient<WebSocketMessage> hermesClient,
|
||||||
ILogger logger
|
ILogger logger
|
||||||
) : base("tts", "Various tts commands.")
|
) : base("tts", "Various tts commands.")
|
||||||
{
|
{
|
||||||
_serviceProvider = serviceProvider;
|
_user = user;
|
||||||
|
_hermesClient = hermesClient;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
|
||||||
AddParameter(ttsVoiceParameter);
|
AddParameter(ttsVoiceParameter);
|
||||||
@ -29,39 +32,36 @@ namespace TwitchChatTTS.Chat.Commands
|
|||||||
|
|
||||||
public override async Task<bool> CheckPermissions(ChatMessage message, long broadcasterId)
|
public override async Task<bool> CheckPermissions(ChatMessage message, long broadcasterId)
|
||||||
{
|
{
|
||||||
return message.IsModerator || message.IsBroadcaster;
|
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, long broadcasterId)
|
||||||
{
|
{
|
||||||
var client = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("hermes");
|
if (_user == null || _user.VoicesAvailable == null)
|
||||||
if (client == null)
|
|
||||||
return;
|
|
||||||
var context = _serviceProvider.GetRequiredService<User>();
|
|
||||||
if (context == null || context.VoicesAvailable == null)
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var voiceName = args[0].ToLower();
|
var voiceName = args[0].ToLower();
|
||||||
var voiceId = context.VoicesAvailable.FirstOrDefault(v => v.Value.ToLower() == voiceName).Key;
|
var voiceId = _user.VoicesAvailable.FirstOrDefault(v => v.Value.ToLower() == voiceName).Key;
|
||||||
var action = args[1].ToLower();
|
var action = args[1].ToLower();
|
||||||
|
|
||||||
switch (action) {
|
switch (action)
|
||||||
|
{
|
||||||
case "enable":
|
case "enable":
|
||||||
await client.Send(3, new RequestMessage()
|
await _hermesClient.Send(3, new RequestMessage()
|
||||||
{
|
{
|
||||||
Type = "update_tts_voice_state",
|
Type = "update_tts_voice_state",
|
||||||
Data = new Dictionary<string, object>() { { "voice", voiceId }, { "state", true } }
|
Data = new Dictionary<string, object>() { { "voice", voiceId }, { "state", true } }
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "disable":
|
case "disable":
|
||||||
await client.Send(3, new RequestMessage()
|
await _hermesClient.Send(3, new RequestMessage()
|
||||||
{
|
{
|
||||||
Type = "update_tts_voice_state",
|
Type = "update_tts_voice_state",
|
||||||
Data = new Dictionary<string, object>() { { "voice", voiceId }, { "state", false } }
|
Data = new Dictionary<string, object>() { { "voice", voiceId }, { "state", false } }
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "remove":
|
case "remove":
|
||||||
await client.Send(3, new RequestMessage()
|
await _hermesClient.Send(3, new RequestMessage()
|
||||||
{
|
{
|
||||||
Type = "delete_tts_voice",
|
Type = "delete_tts_voice",
|
||||||
Data = new Dictionary<string, object>() { { "voice", voiceId } }
|
Data = new Dictionary<string, object>() { { "voice", voiceId } }
|
||||||
@ -69,8 +69,7 @@ namespace TwitchChatTTS.Chat.Commands
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_logger.Information($"Added a new TTS voice [voice: {voiceName}][invoker: {message.Username}][id: {message.UserId}]");
|
||||||
_logger.Information($"Added a new TTS voice by {message.Username} (id: {message.UserId}): {voiceName}.");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -10,16 +10,19 @@ namespace TwitchChatTTS.Chat.Commands
|
|||||||
{
|
{
|
||||||
public class VoiceCommand : ChatCommand
|
public class VoiceCommand : ChatCommand
|
||||||
{
|
{
|
||||||
private IServiceProvider _serviceProvider;
|
private readonly User _user;
|
||||||
private ILogger _logger;
|
private readonly SocketClient<WebSocketMessage> _hermesClient;
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
public VoiceCommand(
|
public VoiceCommand(
|
||||||
[FromKeyedServices("parameter-ttsvoicename")] ChatCommandParameter ttsVoiceParameter,
|
[FromKeyedServices("parameter-ttsvoicename")] ChatCommandParameter ttsVoiceParameter,
|
||||||
IServiceProvider serviceProvider,
|
User user,
|
||||||
|
[FromKeyedServices("hermes")] SocketClient<WebSocketMessage> hermesClient,
|
||||||
ILogger logger
|
ILogger logger
|
||||||
) : base("voice", "Select a TTS voice as the default for that user.")
|
) : base("voice", "Select a TTS voice as the default for that user.")
|
||||||
{
|
{
|
||||||
_serviceProvider = serviceProvider;
|
_user = user;
|
||||||
|
_hermesClient = hermesClient;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
|
||||||
AddParameter(ttsVoiceParameter);
|
AddParameter(ttsVoiceParameter);
|
||||||
@ -32,23 +35,19 @@ namespace TwitchChatTTS.Chat.Commands
|
|||||||
|
|
||||||
public override async Task Execute(IList<string> args, ChatMessage message, long broadcasterId)
|
public override async Task Execute(IList<string> args, ChatMessage message, long broadcasterId)
|
||||||
{
|
{
|
||||||
var client = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("hermes");
|
if (_user == null || _user.VoicesSelected == null || _user.VoicesAvailable == null)
|
||||||
if (client == null)
|
|
||||||
return;
|
|
||||||
var context = _serviceProvider.GetRequiredService<User>();
|
|
||||||
if (context == null || context.VoicesSelected == null || context.VoicesAvailable == null)
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
long chatterId = long.Parse(message.UserId);
|
long chatterId = long.Parse(message.UserId);
|
||||||
var voiceName = args.First().ToLower();
|
var voiceName = args.First().ToLower();
|
||||||
var voice = context.VoicesAvailable.First(v => v.Value.ToLower() == voiceName);
|
var voice = _user.VoicesAvailable.First(v => v.Value.ToLower() == voiceName);
|
||||||
|
|
||||||
await client.Send(3, new RequestMessage()
|
await _hermesClient.Send(3, new RequestMessage()
|
||||||
{
|
{
|
||||||
Type = context.VoicesSelected.ContainsKey(chatterId) ? "update_tts_user" : "create_tts_user",
|
Type = _user.VoicesSelected.ContainsKey(chatterId) ? "update_tts_user" : "create_tts_user",
|
||||||
Data = new Dictionary<string, object>() { { "chatter", chatterId }, { "voice", voice.Key } }
|
Data = new Dictionary<string, object>() { { "chatter", chatterId }, { "voice", voice.Key } }
|
||||||
});
|
});
|
||||||
_logger.Information($"Updated {message.Username}'s [id: {chatterId}] tts voice to {voice.Value} (id: {voice.Key}).");
|
_logger.Information($"Updated chat TTS voice [voice: {voice.Value}][username: {message.Username}].");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -28,13 +28,11 @@ public class AudioPlaybackEngine : IDisposable
|
|||||||
throw new NullReferenceException(nameof(input));
|
throw new NullReferenceException(nameof(input));
|
||||||
|
|
||||||
if (input.WaveFormat.Channels == mixer.WaveFormat.Channels)
|
if (input.WaveFormat.Channels == mixer.WaveFormat.Channels)
|
||||||
{
|
|
||||||
return input;
|
return input;
|
||||||
}
|
|
||||||
if (input.WaveFormat.Channels == 1 && mixer.WaveFormat.Channels == 2)
|
if (input.WaveFormat.Channels == 1 && mixer.WaveFormat.Channels == 2)
|
||||||
{
|
|
||||||
return new MonoToStereoSampleProvider(input);
|
return new MonoToStereoSampleProvider(input);
|
||||||
}
|
if (input.WaveFormat.Channels == 2 && mixer.WaveFormat.Channels == 1)
|
||||||
|
return new StereoToMonoSampleProvider(input);
|
||||||
throw new NotImplementedException("Not yet implemented this channel count conversion");
|
throw new NotImplementedException("Not yet implemented this channel count conversion");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,10 +2,10 @@ using NAudio.Wave;
|
|||||||
|
|
||||||
public class TTSPlayer
|
public class TTSPlayer
|
||||||
{
|
{
|
||||||
private PriorityQueue<TTSMessage, int> _messages; // ready to play
|
private readonly PriorityQueue<TTSMessage, int> _messages; // ready to play
|
||||||
private PriorityQueue<TTSMessage, int> _buffer;
|
private readonly PriorityQueue<TTSMessage, int> _buffer;
|
||||||
private Mutex _mutex;
|
private readonly Mutex _mutex;
|
||||||
private Mutex _mutex2;
|
private readonly Mutex _mutex2;
|
||||||
|
|
||||||
public ISampleProvider? Playing { get; set; }
|
public ISampleProvider? Playing { get; set; }
|
||||||
|
|
||||||
|
@ -15,17 +15,9 @@ namespace TwitchChatTTS
|
|||||||
|
|
||||||
public class TwitchConfiguration {
|
public class TwitchConfiguration {
|
||||||
public IEnumerable<string>? Channels;
|
public IEnumerable<string>? Channels;
|
||||||
public IDictionary<string, RedeemConfiguration>? Redeems;
|
|
||||||
public bool? TtsWhenOffline;
|
public bool? TtsWhenOffline;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class RedeemConfiguration {
|
|
||||||
public string? AudioFilePath;
|
|
||||||
public string? OutputFilePath;
|
|
||||||
public string? OutputContent;
|
|
||||||
public bool? OutputAppend;
|
|
||||||
}
|
|
||||||
|
|
||||||
public class OBSConfiguration {
|
public class OBSConfiguration {
|
||||||
public string? Host;
|
public string? Host;
|
||||||
public short? Port;
|
public short? Port;
|
||||||
|
@ -5,8 +5,8 @@ namespace TwitchChatTTS.Helpers
|
|||||||
{
|
{
|
||||||
public class WebClientWrap
|
public class WebClientWrap
|
||||||
{
|
{
|
||||||
private HttpClient _client;
|
private readonly HttpClient _client;
|
||||||
private JsonSerializerOptions _options;
|
private readonly JsonSerializerOptions _options;
|
||||||
|
|
||||||
|
|
||||||
public WebClientWrap(JsonSerializerOptions options)
|
public WebClientWrap(JsonSerializerOptions options)
|
||||||
@ -23,10 +23,10 @@ namespace TwitchChatTTS.Helpers
|
|||||||
_client.DefaultRequestHeaders.Add(key, value);
|
_client.DefaultRequestHeaders.Add(key, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<T?> GetJson<T>(string uri)
|
public async Task<T?> GetJson<T>(string uri, JsonSerializerOptions options = null)
|
||||||
{
|
{
|
||||||
var response = await _client.GetAsync(uri);
|
var response = await _client.GetAsync(uri);
|
||||||
return JsonSerializer.Deserialize<T>(await response.Content.ReadAsStreamAsync(), _options);
|
return JsonSerializer.Deserialize<T>(await response.Content.ReadAsStreamAsync(), options ?? _options);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<HttpResponseMessage> Get(string uri)
|
public async Task<HttpResponseMessage> Get(string uri)
|
||||||
|
@ -3,10 +3,11 @@ using TwitchChatTTS;
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using HermesSocketLibrary.Requests.Messages;
|
using HermesSocketLibrary.Requests.Messages;
|
||||||
using TwitchChatTTS.Hermes;
|
using TwitchChatTTS.Hermes;
|
||||||
|
using TwitchChatTTS.Twitch.Redemptions;
|
||||||
|
|
||||||
public class HermesApiClient
|
public class HermesApiClient
|
||||||
{
|
{
|
||||||
private WebClientWrap _web;
|
private readonly WebClientWrap _web;
|
||||||
|
|
||||||
public HermesApiClient(Configuration configuration)
|
public HermesApiClient(Configuration configuration)
|
||||||
{
|
{
|
||||||
@ -90,4 +91,26 @@ public class HermesApiClient
|
|||||||
|
|
||||||
return filters;
|
return filters;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<Redemption>> FetchRedemptions()
|
||||||
|
{
|
||||||
|
var redemptions = await _web.GetJson<IEnumerable<Redemption>>("https://hermes.goblincaves.com/api/settings/redemptions", new JsonSerializerOptions()
|
||||||
|
{
|
||||||
|
PropertyNameCaseInsensitive = false,
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||||
|
});
|
||||||
|
if (redemptions == null)
|
||||||
|
throw new Exception("Failed to redemptions from Hermes.");
|
||||||
|
|
||||||
|
return redemptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<RedeemableAction>> FetchRedeemableActions()
|
||||||
|
{
|
||||||
|
var actions = await _web.GetJson<IEnumerable<RedeemableAction>>("https://hermes.goblincaves.com/api/settings/redemptions/actions");
|
||||||
|
if (actions == null)
|
||||||
|
throw new Exception("Failed to fetch redeemable actions from Hermes.");
|
||||||
|
|
||||||
|
return actions;
|
||||||
|
}
|
||||||
}
|
}
|
@ -7,17 +7,17 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
|
|||||||
{
|
{
|
||||||
public class HeartbeatHandler : IWebSocketHandler
|
public class HeartbeatHandler : IWebSocketHandler
|
||||||
{
|
{
|
||||||
private ILogger _logger { get; }
|
private readonly ILogger _logger;
|
||||||
public int OperationCode { get; set; } = 0;
|
public int OperationCode { get; } = 0;
|
||||||
|
|
||||||
public HeartbeatHandler(ILogger logger)
|
public HeartbeatHandler(ILogger logger)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data message)
|
public async Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data data)
|
||||||
{
|
{
|
||||||
if (message is not HeartbeatMessage obj || obj == null)
|
if (data is not HeartbeatMessage message || message == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (sender is not HermesSocketClient client)
|
if (sender is not HermesSocketClient client)
|
||||||
@ -29,7 +29,7 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
|
|||||||
|
|
||||||
client.LastHeartbeatReceived = DateTime.UtcNow;
|
client.LastHeartbeatReceived = DateTime.UtcNow;
|
||||||
|
|
||||||
if (obj.Respond)
|
if (message.Respond)
|
||||||
await sender.Send(0, new HeartbeatMessage()
|
await sender.Send(0, new HeartbeatMessage()
|
||||||
{
|
{
|
||||||
DateTime = DateTime.UtcNow,
|
DateTime = DateTime.UtcNow,
|
||||||
|
@ -8,33 +8,32 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
|
|||||||
{
|
{
|
||||||
public class LoginAckHandler : IWebSocketHandler
|
public class LoginAckHandler : IWebSocketHandler
|
||||||
{
|
{
|
||||||
private IServiceProvider _serviceProvider;
|
private readonly User _user;
|
||||||
private ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
public int OperationCode { get; set; } = 2;
|
public int OperationCode { get; } = 2;
|
||||||
|
|
||||||
public LoginAckHandler(IServiceProvider serviceProvider, ILogger logger)
|
public LoginAckHandler(User user, ILogger logger)
|
||||||
{
|
{
|
||||||
_serviceProvider = serviceProvider;
|
_user = user;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data message)
|
public async Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data data)
|
||||||
{
|
{
|
||||||
if (message is not LoginAckMessage obj || obj == null)
|
if (data is not LoginAckMessage message || message == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (sender is not HermesSocketClient client)
|
if (sender is not HermesSocketClient client)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (obj.AnotherClient)
|
if (message.AnotherClient)
|
||||||
{
|
{
|
||||||
_logger.Warning("Another client has connected to the same account.");
|
_logger.Warning("Another client has connected to the same account.");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var user = _serviceProvider.GetRequiredService<User>();
|
client.UserId = message.UserId;
|
||||||
client.UserId = obj.UserId;
|
_logger.Information($"Logged in as {_user.TwitchUsername}.");
|
||||||
_logger.Information($"Logged in as {user.TwitchUsername} (id: {client.UserId}).");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await client.Send(3, new RequestMessage()
|
await client.Send(3, new RequestMessage()
|
||||||
@ -43,11 +42,10 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
|
|||||||
Data = null
|
Data = null
|
||||||
});
|
});
|
||||||
|
|
||||||
var token = _serviceProvider.GetRequiredService<User>();
|
|
||||||
await client.Send(3, new RequestMessage()
|
await client.Send(3, new RequestMessage()
|
||||||
{
|
{
|
||||||
Type = "get_tts_users",
|
Type = "get_tts_users",
|
||||||
Data = new Dictionary<string, object>() { { "user", token.HermesUserId } }
|
Data = new Dictionary<string, object>() { { "user", _user.HermesUserId } }
|
||||||
});
|
});
|
||||||
|
|
||||||
await client.Send(3, new RequestMessage()
|
await client.Send(3, new RequestMessage()
|
||||||
|
@ -18,7 +18,7 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
|
|||||||
|
|
||||||
private readonly object _voicesAvailableLock = new object();
|
private readonly object _voicesAvailableLock = new object();
|
||||||
|
|
||||||
public int OperationCode { get; set; } = 4;
|
public int OperationCode { get; } = 4;
|
||||||
|
|
||||||
public RequestAckHandler(IServiceProvider serviceProvider, JsonSerializerOptions options, ILogger logger)
|
public RequestAckHandler(IServiceProvider serviceProvider, JsonSerializerOptions options, ILogger logger)
|
||||||
{
|
{
|
||||||
@ -27,20 +27,20 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data message)
|
public async Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data data)
|
||||||
{
|
{
|
||||||
if (message is not RequestAckMessage obj || obj == null)
|
if (data is not RequestAckMessage message || message == null)
|
||||||
return;
|
return;
|
||||||
if (obj.Request == null)
|
if (message.Request == null)
|
||||||
return;
|
return;
|
||||||
var context = _serviceProvider.GetRequiredService<User>();
|
var context = _serviceProvider.GetRequiredService<User>();
|
||||||
if (context == null)
|
if (context == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (obj.Request.Type == "get_tts_voices")
|
if (message.Request.Type == "get_tts_voices")
|
||||||
{
|
{
|
||||||
_logger.Verbose("Updating all available voices for TTS.");
|
_logger.Verbose("Updating all available voices for TTS.");
|
||||||
var voices = JsonSerializer.Deserialize<IEnumerable<VoiceDetails>>(obj.Data.ToString(), _options);
|
var voices = JsonSerializer.Deserialize<IEnumerable<VoiceDetails>>(message.Data.ToString(), _options);
|
||||||
if (voices == null)
|
if (voices == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@ -50,33 +50,33 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
|
|||||||
}
|
}
|
||||||
_logger.Information("Updated all available voices for TTS.");
|
_logger.Information("Updated all available voices for TTS.");
|
||||||
}
|
}
|
||||||
else if (obj.Request.Type == "create_tts_user")
|
else if (message.Request.Type == "create_tts_user")
|
||||||
{
|
{
|
||||||
_logger.Verbose("Adding new tts voice for user.");
|
_logger.Verbose("Adding new tts voice for user.");
|
||||||
if (!long.TryParse(obj.Request.Data["user"].ToString(), out long chatterId))
|
if (!long.TryParse(message.Request.Data["user"].ToString(), out long chatterId))
|
||||||
return;
|
return;
|
||||||
string userId = obj.Request.Data["user"].ToString();
|
string userId = message.Request.Data["user"].ToString();
|
||||||
string voice = obj.Request.Data["voice"].ToString();
|
string voice = message.Request.Data["voice"].ToString();
|
||||||
|
|
||||||
context.VoicesSelected.Add(chatterId, voice);
|
context.VoicesSelected.Add(chatterId, voice);
|
||||||
_logger.Information($"Added new TTS voice [voice: {voice}] for user [user id: {userId}]");
|
_logger.Information($"Added new TTS voice [voice: {voice}] for user [user id: {userId}]");
|
||||||
}
|
}
|
||||||
else if (obj.Request.Type == "update_tts_user")
|
else if (message.Request.Type == "update_tts_user")
|
||||||
{
|
{
|
||||||
_logger.Verbose("Updating user's voice");
|
_logger.Verbose("Updating user's voice");
|
||||||
if (!long.TryParse(obj.Request.Data["chatter"].ToString(), out long chatterId))
|
if (!long.TryParse(message.Request.Data["chatter"].ToString(), out long chatterId))
|
||||||
return;
|
return;
|
||||||
string userId = obj.Request.Data["user"].ToString();
|
string userId = message.Request.Data["user"].ToString();
|
||||||
string voice = obj.Request.Data["voice"].ToString();
|
string voice = message.Request.Data["voice"].ToString();
|
||||||
|
|
||||||
context.VoicesSelected[chatterId] = voice;
|
context.VoicesSelected[chatterId] = voice;
|
||||||
_logger.Information($"Updated TTS voice [voice: {voice}] for user [user id: {userId}]");
|
_logger.Information($"Updated TTS voice [voice: {voice}] for user [user id: {userId}]");
|
||||||
}
|
}
|
||||||
else if (obj.Request.Type == "create_tts_voice")
|
else if (message.Request.Type == "create_tts_voice")
|
||||||
{
|
{
|
||||||
_logger.Verbose("Creating new tts voice.");
|
_logger.Verbose("Creating new tts voice.");
|
||||||
string? voice = obj.Request.Data["voice"].ToString();
|
string? voice = message.Request.Data["voice"].ToString();
|
||||||
string? voiceId = obj.Data.ToString();
|
string? voiceId = message.Data.ToString();
|
||||||
if (voice == null || voiceId == null)
|
if (voice == null || voiceId == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@ -88,10 +88,10 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
|
|||||||
}
|
}
|
||||||
_logger.Information($"Created new tts voice [voice: {voice}][id: {voiceId}].");
|
_logger.Information($"Created new tts voice [voice: {voice}][id: {voiceId}].");
|
||||||
}
|
}
|
||||||
else if (obj.Request.Type == "delete_tts_voice")
|
else if (message.Request.Type == "delete_tts_voice")
|
||||||
{
|
{
|
||||||
_logger.Verbose("Deleting tts voice.");
|
_logger.Verbose("Deleting tts voice.");
|
||||||
var voice = obj.Request.Data["voice"].ToString();
|
var voice = message.Request.Data["voice"].ToString();
|
||||||
if (!context.VoicesAvailable.TryGetValue(voice, out string voiceName) || voiceName == null)
|
if (!context.VoicesAvailable.TryGetValue(voice, out string voiceName) || voiceName == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@ -103,11 +103,11 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
|
|||||||
}
|
}
|
||||||
_logger.Information($"Deleted a voice [voice: {voiceName}]");
|
_logger.Information($"Deleted a voice [voice: {voiceName}]");
|
||||||
}
|
}
|
||||||
else if (obj.Request.Type == "update_tts_voice")
|
else if (message.Request.Type == "update_tts_voice")
|
||||||
{
|
{
|
||||||
_logger.Verbose("Updating TTS voice.");
|
_logger.Verbose("Updating TTS voice.");
|
||||||
string voiceId = obj.Request.Data["idd"].ToString();
|
string voiceId = message.Request.Data["idd"].ToString();
|
||||||
string voice = obj.Request.Data["voice"].ToString();
|
string voice = message.Request.Data["voice"].ToString();
|
||||||
|
|
||||||
if (!context.VoicesAvailable.TryGetValue(voiceId, out string voiceName) || voiceName == null)
|
if (!context.VoicesAvailable.TryGetValue(voiceId, out string voiceName) || voiceName == null)
|
||||||
return;
|
return;
|
||||||
@ -115,10 +115,10 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
|
|||||||
context.VoicesAvailable[voiceId] = voice;
|
context.VoicesAvailable[voiceId] = voice;
|
||||||
_logger.Information($"Updated TTS voice [voice: {voice}][id: {voiceId}]");
|
_logger.Information($"Updated TTS voice [voice: {voice}][id: {voiceId}]");
|
||||||
}
|
}
|
||||||
else if (obj.Request.Type == "get_tts_users")
|
else if (message.Request.Type == "get_tts_users")
|
||||||
{
|
{
|
||||||
_logger.Verbose("Updating all chatters' selected voice.");
|
_logger.Verbose("Updating all chatters' selected voice.");
|
||||||
var users = JsonSerializer.Deserialize<IDictionary<long, string>>(obj.Data.ToString(), _options);
|
var users = JsonSerializer.Deserialize<IDictionary<long, string>>(message.Data.ToString(), _options);
|
||||||
if (users == null)
|
if (users == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@ -128,10 +128,10 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
|
|||||||
context.VoicesSelected = temp;
|
context.VoicesSelected = temp;
|
||||||
_logger.Information($"Updated {temp.Count()} chatters' selected voice.");
|
_logger.Information($"Updated {temp.Count()} chatters' selected voice.");
|
||||||
}
|
}
|
||||||
else if (obj.Request.Type == "get_chatter_ids")
|
else if (message.Request.Type == "get_chatter_ids")
|
||||||
{
|
{
|
||||||
_logger.Verbose("Fetching all chatters' id.");
|
_logger.Verbose("Fetching all chatters' id.");
|
||||||
var chatters = JsonSerializer.Deserialize<IEnumerable<long>>(obj.Data.ToString(), _options);
|
var chatters = JsonSerializer.Deserialize<IEnumerable<long>>(message.Data.ToString(), _options);
|
||||||
if (chatters == null)
|
if (chatters == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@ -139,10 +139,10 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
|
|||||||
client.Chatters = [.. chatters];
|
client.Chatters = [.. chatters];
|
||||||
_logger.Information($"Fetched {chatters.Count()} chatters' id.");
|
_logger.Information($"Fetched {chatters.Count()} chatters' id.");
|
||||||
}
|
}
|
||||||
else if (obj.Request.Type == "get_emotes")
|
else if (message.Request.Type == "get_emotes")
|
||||||
{
|
{
|
||||||
_logger.Verbose("Updating emotes.");
|
_logger.Verbose("Updating emotes.");
|
||||||
var emotes = JsonSerializer.Deserialize<IEnumerable<EmoteInfo>>(obj.Data.ToString(), _options);
|
var emotes = JsonSerializer.Deserialize<IEnumerable<EmoteInfo>>(message.Data.ToString(), _options);
|
||||||
if (emotes == null)
|
if (emotes == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@ -158,11 +158,11 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
|
|||||||
}
|
}
|
||||||
_logger.Information($"Fetched {count} emotes from various sources.");
|
_logger.Information($"Fetched {count} emotes from various sources.");
|
||||||
}
|
}
|
||||||
else if (obj.Request.Type == "update_tts_voice_state")
|
else if (message.Request.Type == "update_tts_voice_state")
|
||||||
{
|
{
|
||||||
_logger.Verbose("Updating TTS voice states.");
|
_logger.Verbose("Updating TTS voice states.");
|
||||||
string voiceId = obj.Request.Data["voice"].ToString();
|
string voiceId = message.Request.Data["voice"].ToString();
|
||||||
bool state = obj.Request.Data["state"].ToString() == "true";
|
bool state = message.Request.Data["state"].ToString() == "true";
|
||||||
|
|
||||||
if (!context.VoicesAvailable.TryGetValue(voiceId, out string voiceName) || voiceName == null)
|
if (!context.VoicesAvailable.TryGetValue(voiceId, out string voiceName) || voiceName == null)
|
||||||
{
|
{
|
||||||
|
@ -74,7 +74,7 @@ namespace TwitchChatTTS.Hermes.Socket
|
|||||||
UserId = null;
|
UserId = null;
|
||||||
_heartbeatTimer.Enabled = false;
|
_heartbeatTimer.Enabled = false;
|
||||||
|
|
||||||
_logger.Information("Logged off due to disconnection. Attempting to reconnect...");
|
_logger.Warning("Logged off due to disconnection. Attempting to reconnect...");
|
||||||
_reconnectTimer.Enabled = true;
|
_reconnectTimer.Enabled = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,6 @@ namespace TwitchChatTTS.Hermes.Socket.Managers
|
|||||||
{
|
{
|
||||||
public HermesHandlerManager(ILogger logger, IServiceProvider provider) : base(logger)
|
public HermesHandlerManager(ILogger logger, IServiceProvider provider) : base(logger)
|
||||||
{
|
{
|
||||||
//Add(provider.GetRequiredService<HeartbeatHandler>());
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var basetype = typeof(IWebSocketHandler);
|
var basetype = typeof(IWebSocketHandler);
|
||||||
@ -29,13 +28,13 @@ namespace TwitchChatTTS.Hermes.Socket.Managers
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.Debug($"Linked type {type.AssemblyQualifiedName} to hermes websocket handlers.");
|
_logger.Debug($"Linked type {type.AssemblyQualifiedName} to hermes websocket handlers.");
|
||||||
Add(handler);
|
Add(handler);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Logger.Error(e, "Failed to load hermes websocket handler types.");
|
_logger.Error(e, "Failed to load hermes websocket handler types.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
8
OBS/Socket/Data/OBSAlignment.cs
Normal file
8
OBS/Socket/Data/OBSAlignment.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
namespace TwitchChatTTS.OBS.Socket.Data
|
||||||
|
{
|
||||||
|
public enum OBSAlignment
|
||||||
|
{
|
||||||
|
Center = 0,
|
||||||
|
TopLeft = 5
|
||||||
|
}
|
||||||
|
}
|
24
OBS/Socket/Data/TransformationData.cs
Normal file
24
OBS/Socket/Data/TransformationData.cs
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
namespace TwitchChatTTS.OBS.Socket.Data
|
||||||
|
{
|
||||||
|
public class OBSTransformationData
|
||||||
|
{
|
||||||
|
public int Alignment { get; set; }
|
||||||
|
public int BoundsAlignment { get; set; }
|
||||||
|
public double BoundsHeight { get; set; }
|
||||||
|
public string BoundsType { get; set; }
|
||||||
|
public double BoundsWidth { get; set; }
|
||||||
|
public int CropBottom { get; set; }
|
||||||
|
public int CropLeft { get; set; }
|
||||||
|
public int CropRight { get; set; }
|
||||||
|
public int CropTop { get; set; }
|
||||||
|
public double Height { get; set; }
|
||||||
|
public double PositionX { get; set; }
|
||||||
|
public double PositionY { get; set; }
|
||||||
|
public double Rotation { get; set; }
|
||||||
|
public double ScaleX { get; set; }
|
||||||
|
public double ScaleY { get; set; }
|
||||||
|
public double SourceHeight { get; set; }
|
||||||
|
public double SourceWidth { get; set; }
|
||||||
|
public double Width { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@ -7,31 +7,29 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
|
|||||||
{
|
{
|
||||||
public class EventMessageHandler : IWebSocketHandler
|
public class EventMessageHandler : IWebSocketHandler
|
||||||
{
|
{
|
||||||
private ILogger _logger { get; }
|
private readonly ILogger _logger;
|
||||||
private IServiceProvider _serviceProvider { get; }
|
public int OperationCode { get; } = 5;
|
||||||
public int OperationCode { get; set; } = 5;
|
|
||||||
|
|
||||||
public EventMessageHandler(ILogger logger, IServiceProvider serviceProvider)
|
public EventMessageHandler(ILogger logger)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_serviceProvider = serviceProvider;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data message)
|
public async Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data data)
|
||||||
{
|
{
|
||||||
if (message is not EventMessage obj || obj == null)
|
if (data is not EventMessage message || message == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
switch (obj.EventType)
|
switch (message.EventType)
|
||||||
{
|
{
|
||||||
case "StreamStateChanged":
|
case "StreamStateChanged":
|
||||||
case "RecordStateChanged":
|
case "RecordStateChanged":
|
||||||
if (sender is not OBSSocketClient client)
|
if (sender is not OBSSocketClient client)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
string? raw_state = obj.EventData["outputState"].ToString();
|
string? raw_state = message.EventData["outputState"].ToString();
|
||||||
string? state = raw_state?.Substring(21).ToLower();
|
string? state = raw_state?.Substring(21).ToLower();
|
||||||
client.Live = obj.EventData["outputActive"].ToString() == "True";
|
client.Live = message.EventData["outputActive"].ToString() == "True";
|
||||||
_logger.Warning("Stream " + (state != null && state.EndsWith("ing") ? "is " : "has ") + state + ".");
|
_logger.Warning("Stream " + (state != null && state.EndsWith("ing") ? "is " : "has ") + state + ".");
|
||||||
|
|
||||||
if (client.Live == false && state != null && !state.EndsWith("ing"))
|
if (client.Live == false && state != null && !state.EndsWith("ing"))
|
||||||
@ -40,7 +38,7 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
_logger.Debug(obj.EventType + " EVENT: " + string.Join(" | ", obj.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()) ?? new string[0]));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,30 +10,30 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
|
|||||||
{
|
{
|
||||||
public class HelloHandler : IWebSocketHandler
|
public class HelloHandler : IWebSocketHandler
|
||||||
{
|
{
|
||||||
private ILogger _logger { get; }
|
private readonly HelloContext _context;
|
||||||
public int OperationCode { get; set; } = 0;
|
private readonly ILogger _logger;
|
||||||
private HelloContext _context { get; }
|
public int OperationCode { get; } = 0;
|
||||||
|
|
||||||
public HelloHandler(ILogger logger, HelloContext context)
|
public HelloHandler(HelloContext context, ILogger logger)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
|
||||||
_context = context;
|
_context = context;
|
||||||
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data message)
|
public async Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data data)
|
||||||
{
|
{
|
||||||
if (message is not HelloMessage obj || obj == null)
|
if (data is not HelloMessage message || message == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
_logger.Verbose("OBS websocket password: " + _context.Password);
|
_logger.Verbose("OBS websocket password: " + _context.Password);
|
||||||
if (obj.Authentication == null || string.IsNullOrWhiteSpace(_context.Password))
|
if (message.Authentication == null || string.IsNullOrWhiteSpace(_context.Password))
|
||||||
{
|
{
|
||||||
await sender.Send(1, new IdentifyMessage(obj.RpcVersion, string.Empty, 1023 | 262144));
|
await sender.Send(1, new IdentifyMessage(message.RpcVersion, string.Empty, 1023 | 262144));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var salt = obj.Authentication.Salt;
|
var salt = message.Authentication.Salt;
|
||||||
var challenge = obj.Authentication.Challenge;
|
var challenge = message.Authentication.Challenge;
|
||||||
_logger.Verbose("Salt: " + salt);
|
_logger.Verbose("Salt: " + salt);
|
||||||
_logger.Verbose("Challenge: " + challenge);
|
_logger.Verbose("Challenge: " + challenge);
|
||||||
|
|
||||||
@ -52,7 +52,7 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
|
|||||||
}
|
}
|
||||||
|
|
||||||
_logger.Verbose("Final hash: " + hash);
|
_logger.Verbose("Final hash: " + hash);
|
||||||
await sender.Send(1, new IdentifyMessage(obj.RpcVersion, hash, 1023 | 262144));
|
await sender.Send(1, new IdentifyMessage(message.RpcVersion, hash, 1023 | 262144));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -7,21 +7,29 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
|
|||||||
{
|
{
|
||||||
public class IdentifiedHandler : IWebSocketHandler
|
public class IdentifiedHandler : IWebSocketHandler
|
||||||
{
|
{
|
||||||
private ILogger Logger { get; }
|
private readonly ILogger _logger;
|
||||||
public int OperationCode { get; set; } = 2;
|
public int OperationCode { get; } = 2;
|
||||||
|
|
||||||
public IdentifiedHandler(ILogger logger)
|
public IdentifiedHandler(ILogger logger)
|
||||||
{
|
{
|
||||||
Logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data message)
|
public async Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data data)
|
||||||
{
|
{
|
||||||
if (message is not IdentifiedMessage obj || obj == null)
|
if (data is not IdentifiedMessage message || message == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
sender.Connected = true;
|
sender.Connected = true;
|
||||||
Logger.Information("Connected to OBS via rpc version " + obj.NegotiatedRpcVersion + ".");
|
_logger.Information("Connected to OBS via rpc version " + message.NegotiatedRpcVersion + ".");
|
||||||
|
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(5));
|
||||||
|
|
||||||
|
/*var messages = new RequestMessage[] {
|
||||||
|
//new RequestMessage("Sleep", string.Empty, new Dictionary<string, object>() { { "sleepMillis", 5000 } }),
|
||||||
|
new RequestMessage("GetSceneItemId", string.Empty, new Dictionary<string, object>() { { "sceneName", "Generic" }, { "sourceName", "ABCDEF" } }),
|
||||||
|
};
|
||||||
|
await _manager.Send(messages);*/
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -11,18 +11,17 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
|
|||||||
{
|
{
|
||||||
public class RequestBatchResponseHandler : IWebSocketHandler
|
public class RequestBatchResponseHandler : IWebSocketHandler
|
||||||
{
|
{
|
||||||
private OBSRequestBatchManager _manager { get; }
|
private readonly IWebSocketHandler _requestResponseHandler;
|
||||||
private IServiceProvider _serviceProvider { get; }
|
private readonly ILogger _logger;
|
||||||
private ILogger _logger { get; }
|
public int OperationCode { get; } = 9;
|
||||||
private JsonSerializerOptions _options;
|
|
||||||
public int OperationCode { get; set; } = 9;
|
|
||||||
|
|
||||||
public RequestBatchResponseHandler(OBSRequestBatchManager manager, JsonSerializerOptions options, IServiceProvider serviceProvider, ILogger logger)
|
public RequestBatchResponseHandler(
|
||||||
|
[FromKeyedServices("obs-requestresponse")] IWebSocketHandler requestResponseHandler,
|
||||||
|
ILogger logger
|
||||||
|
)
|
||||||
{
|
{
|
||||||
_manager = manager;
|
_requestResponseHandler = requestResponseHandler;
|
||||||
_serviceProvider = serviceProvider;
|
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_options = options;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data data)
|
public async Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data data)
|
||||||
@ -36,50 +35,33 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
|
|||||||
var results = message.Results.ToList();
|
var results = message.Results.ToList();
|
||||||
_logger.Debug($"Received request batch response of {results.Count} messages.");
|
_logger.Debug($"Received request batch response of {results.Count} messages.");
|
||||||
|
|
||||||
var requestData = _manager.Take(message.RequestId);
|
int count = results.Count;
|
||||||
if (requestData == null || !results.Any())
|
|
||||||
{
|
|
||||||
_logger.Verbose($"Received request batch response of {results.Count} messages.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
IList<Task> tasks = new List<Task>();
|
|
||||||
int count = Math.Min(results.Count, requestData.RequestTypes.Count);
|
|
||||||
for (int i = 0; i < count; i++)
|
for (int i = 0; i < count; i++)
|
||||||
{
|
{
|
||||||
Type type = requestData.RequestTypes[i];
|
if (results[i] == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
using (LogContext.PushProperty("type", type.Name))
|
|
||||||
{
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var handler = GetResponseHandlerForRequestType(type);
|
_logger.Debug($"Request response from OBS request batch #{i + 1}/{count}: {results[i]}");
|
||||||
_logger.Verbose($"Request handled by {handler.GetType().Name}.");
|
var response = JsonSerializer.Deserialize<RequestResponseMessage>(results[i].ToString(), new JsonSerializerOptions()
|
||||||
tasks.Add(handler.Execute(sender, results[i]));
|
{
|
||||||
|
PropertyNameCaseInsensitive = false,
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||||
|
});
|
||||||
|
if (response == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
await _requestResponseHandler.Execute(sender, response);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.Error(ex, "Failed to process an item in a request batch message.");
|
_logger.Error(ex, "Failed to process an item in a request batch message.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
_logger.Verbose($"Waiting for processing to complete.");
|
|
||||||
await Task.WhenAll(tasks);
|
|
||||||
|
|
||||||
_logger.Debug($"Finished processing all request in this batch.");
|
_logger.Debug($"Finished processing all request in this batch.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private IWebSocketHandler? GetResponseHandlerForRequestType(Type type)
|
|
||||||
{
|
|
||||||
if (type == typeof(RequestMessage))
|
|
||||||
return _serviceProvider.GetRequiredKeyedService<IWebSocketHandler>("obs-requestresponse");
|
|
||||||
else if (type == typeof(RequestBatchMessage))
|
|
||||||
return _serviceProvider.GetRequiredKeyedService<IWebSocketHandler>("obs-requestbatcresponse");
|
|
||||||
else if (type == typeof(IdentifyMessage))
|
|
||||||
return _serviceProvider.GetRequiredKeyedService<IWebSocketHandler>("obs-identified");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -2,36 +2,100 @@ using CommonSocketLibrary.Abstract;
|
|||||||
using CommonSocketLibrary.Common;
|
using CommonSocketLibrary.Common;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
using TwitchChatTTS.OBS.Socket.Data;
|
using TwitchChatTTS.OBS.Socket.Data;
|
||||||
|
using TwitchChatTTS.OBS.Socket.Manager;
|
||||||
|
|
||||||
namespace TwitchChatTTS.OBS.Socket.Handlers
|
namespace TwitchChatTTS.OBS.Socket.Handlers
|
||||||
{
|
{
|
||||||
public class RequestResponseHandler : IWebSocketHandler
|
public class RequestResponseHandler : IWebSocketHandler
|
||||||
{
|
{
|
||||||
private ILogger Logger { get; }
|
private readonly OBSManager _manager;
|
||||||
public int OperationCode { get; set; } = 7;
|
private readonly ILogger _logger;
|
||||||
|
public int OperationCode { get; } = 7;
|
||||||
|
|
||||||
public RequestResponseHandler(ILogger logger)
|
public RequestResponseHandler(OBSManager manager, ILogger logger)
|
||||||
{
|
{
|
||||||
Logger = logger;
|
_manager = manager;
|
||||||
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data message)
|
public async Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data data)
|
||||||
{
|
{
|
||||||
if (message is not RequestResponseMessage obj || obj == null)
|
if (data is not RequestResponseMessage message || message == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
switch (obj.RequestType)
|
_logger.Debug($"Received an OBS request response [response id: {message.RequestId}]");
|
||||||
|
|
||||||
|
var requestData = _manager.Take(message.RequestId);
|
||||||
|
if (requestData == null)
|
||||||
|
{
|
||||||
|
_logger.Warning($"OBS Request Response not being processed: request not stored [response id: {message.RequestId}]");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var request = requestData.Message;
|
||||||
|
if (request == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
switch (request.RequestType)
|
||||||
{
|
{
|
||||||
case "GetOutputStatus":
|
case "GetOutputStatus":
|
||||||
if (sender is not OBSSocketClient client)
|
if (sender is not OBSSocketClient client)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (obj.RequestId == "stream")
|
if (message.RequestId == "stream")
|
||||||
{
|
{
|
||||||
client.Live = obj.ResponseData["outputActive"].ToString() == "True";
|
client.Live = message.ResponseData["outputActive"].ToString() == "True";
|
||||||
Logger.Warning("Updated stream's live status to " + client.Live);
|
_logger.Warning($"Updated stream's live status to {client.Live} [response id: {message.RequestId}]");
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case "GetSceneItemId":
|
||||||
|
if (!request.RequestData.TryGetValue("sceneName", out object sceneName))
|
||||||
|
{
|
||||||
|
_logger.Warning($"Failed to find the scene name that was requested [response id: {message.RequestId}]");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!request.RequestData.TryGetValue("sourceName", out object sourceName))
|
||||||
|
{
|
||||||
|
_logger.Warning($"Failed to find the scene item name that was requested [scene: {sceneName}][response id: {message.RequestId}]");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!message.ResponseData.TryGetValue("sceneItemId", out object sceneItemId)) {
|
||||||
|
_logger.Warning($"Failed to fetch the scene item id [scene: {sceneName}][scene item: {sourceName}][response id: {message.RequestId}]");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.Information($"Added scene item id [scene: {sceneName}][source: {sourceName}][id: {sceneItemId}][response id: {message.RequestId}].");
|
||||||
|
_manager.AddSourceId(sceneName.ToString(), sourceName.ToString(), long.Parse(sceneItemId.ToString()));
|
||||||
|
|
||||||
|
requestData.ResponseValues = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "sceneItemId", sceneItemId }
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case "GetSceneItemTransform":
|
||||||
|
if (!message.ResponseData.TryGetValue("sceneItemTransform", out object? transformData))
|
||||||
|
{
|
||||||
|
_logger.Warning($"Failed to find the OBS scene item [response id: {message.RequestId}]");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.Verbose("Fetching OBS transformation data: " + transformData?.ToString());
|
||||||
|
requestData.ResponseValues = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "sceneItemTransform", transformData }
|
||||||
|
};
|
||||||
|
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()) ?? new string[0])}]");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (requestData.Callback != null)
|
||||||
|
requestData.Callback(requestData.ResponseValues);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,60 +0,0 @@
|
|||||||
using CommonSocketLibrary.Abstract;
|
|
||||||
using CommonSocketLibrary.Common;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Serilog;
|
|
||||||
using TwitchChatTTS.OBS.Socket.Data;
|
|
||||||
|
|
||||||
namespace TwitchChatTTS.OBS.Socket.Manager
|
|
||||||
{
|
|
||||||
public class OBSRequestBatchManager
|
|
||||||
{
|
|
||||||
private IDictionary<string, OBSRequestBatchData> _requests;
|
|
||||||
private IServiceProvider _serviceProvider;
|
|
||||||
private ILogger _logger;
|
|
||||||
|
|
||||||
public OBSRequestBatchManager(IServiceProvider serviceProvider, ILogger logger)
|
|
||||||
{
|
|
||||||
_serviceProvider = serviceProvider;
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public async Task Send(long broadcasterId, IEnumerable<WebSocketMessage> messages) {
|
|
||||||
string uid = GenerateUniqueIdentifier();
|
|
||||||
var data = new OBSRequestBatchData(broadcasterId, uid, new List<Type>());
|
|
||||||
_logger.Debug($"Sending request batch of {messages.Count()} messages.");
|
|
||||||
|
|
||||||
foreach (WebSocketMessage message in messages)
|
|
||||||
data.RequestTypes.Add(message.GetType());
|
|
||||||
|
|
||||||
var client = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("obs");
|
|
||||||
await client.Send(8, new RequestBatchMessage(uid, messages));
|
|
||||||
}
|
|
||||||
|
|
||||||
public OBSRequestBatchData? Take(string id) {
|
|
||||||
if (_requests.TryGetValue(id, out var request)) {
|
|
||||||
_requests.Remove(id);
|
|
||||||
return request;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private string GenerateUniqueIdentifier()
|
|
||||||
{
|
|
||||||
return Guid.NewGuid().ToString("X");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class OBSRequestBatchData
|
|
||||||
{
|
|
||||||
public long BroadcasterId { get; }
|
|
||||||
public string RequestId { get; }
|
|
||||||
public IList<Type> RequestTypes { get; }
|
|
||||||
|
|
||||||
public OBSRequestBatchData(long bid, string rid, IList<Type> types) {
|
|
||||||
BroadcasterId = bid;
|
|
||||||
RequestId = rid;
|
|
||||||
RequestTypes = types;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -26,7 +26,7 @@ namespace TwitchChatTTS.OBS.Socket.Manager
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.Debug($"Linked type {type.AssemblyQualifiedName} to obs websocket handler {handler.GetType().AssemblyQualifiedName}.");
|
_logger.Debug($"Linked type {type.AssemblyQualifiedName} to obs websocket handler {handler.GetType().AssemblyQualifiedName}.");
|
||||||
Add(handler);
|
Add(handler);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
210
OBS/Socket/Manager/OBSManager.cs
Normal file
210
OBS/Socket/Manager/OBSManager.cs
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Text.Json;
|
||||||
|
using CommonSocketLibrary.Abstract;
|
||||||
|
using CommonSocketLibrary.Common;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Serilog;
|
||||||
|
using TwitchChatTTS.OBS.Socket.Data;
|
||||||
|
|
||||||
|
namespace TwitchChatTTS.OBS.Socket.Manager
|
||||||
|
{
|
||||||
|
public class OBSManager
|
||||||
|
{
|
||||||
|
private IDictionary<string, RequestData> _requests;
|
||||||
|
private IDictionary<string, IDictionary<string, long>> _sourceIds;
|
||||||
|
private IServiceProvider _serviceProvider;
|
||||||
|
private ILogger _logger;
|
||||||
|
|
||||||
|
public OBSManager(IServiceProvider serviceProvider, ILogger logger)
|
||||||
|
{
|
||||||
|
_serviceProvider = serviceProvider;
|
||||||
|
_logger = logger;
|
||||||
|
|
||||||
|
_requests = new ConcurrentDictionary<string, RequestData>();
|
||||||
|
_sourceIds = new Dictionary<string, IDictionary<string, long>>();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void AddSourceId(string sceneName, string sourceName, long sourceId)
|
||||||
|
{
|
||||||
|
if (!_sourceIds.TryGetValue(sceneName, out var scene))
|
||||||
|
{
|
||||||
|
scene = new Dictionary<string, long>();
|
||||||
|
_sourceIds.Add(sceneName, scene);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scene.ContainsKey(sourceName))
|
||||||
|
scene[sourceName] = sourceId;
|
||||||
|
else
|
||||||
|
scene.Add(sourceName, sourceId);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Send(IEnumerable<RequestMessage> messages)
|
||||||
|
{
|
||||||
|
string uid = GenerateUniqueIdentifier();
|
||||||
|
_logger.Debug($"Sending OBS request batch of {messages.Count()} messages [obsid: {uid}].");
|
||||||
|
|
||||||
|
// Keep track of requests to know what we requested.
|
||||||
|
foreach (var message in messages)
|
||||||
|
{
|
||||||
|
message.RequestId = GenerateUniqueIdentifier();
|
||||||
|
var data = new RequestData(message, uid);
|
||||||
|
_requests.Add(message.RequestId, data);
|
||||||
|
}
|
||||||
|
_logger.Debug($"Generated uid for all OBS request messages in batch [obsid: {uid}]: {string.Join(", ", messages.Select(m => m.RequestType + "=" + m.RequestId))}");
|
||||||
|
|
||||||
|
var client = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("obs");
|
||||||
|
await client.Send(8, new RequestBatchMessage(uid, messages));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Send(RequestMessage message, Action<Dictionary<string, object>>? callback = null)
|
||||||
|
{
|
||||||
|
string uid = GenerateUniqueIdentifier();
|
||||||
|
_logger.Debug($"Sending an OBS request [obsid: {uid}]");
|
||||||
|
|
||||||
|
// Keep track of requests to know what we requested.
|
||||||
|
message.RequestId = GenerateUniqueIdentifier();
|
||||||
|
var data = new RequestData(message, uid)
|
||||||
|
{
|
||||||
|
Callback = callback
|
||||||
|
};
|
||||||
|
_requests.Add(message.RequestId, data);
|
||||||
|
|
||||||
|
var client = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("obs");
|
||||||
|
await client.Send(6, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public RequestData? Take(string id)
|
||||||
|
{
|
||||||
|
if (id != null && _requests.TryGetValue(id, out var request))
|
||||||
|
{
|
||||||
|
_requests.Remove(id);
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateTransformation(string sceneName, string sceneItemName, Action<OBSTransformationData> action)
|
||||||
|
{
|
||||||
|
var m1 = new RequestMessage("GetSceneItemId", string.Empty, new Dictionary<string, object>() { { "sceneName", sceneName }, { "sourceName", sceneItemName } });
|
||||||
|
await Send(m1, async (d) =>
|
||||||
|
{
|
||||||
|
if (!d.TryGetValue("sceneItemId", out object value) || !long.TryParse(value.ToString(), out long sceneItemId))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_logger.Debug($"Fetched scene item id from OBS [scene: {sceneName}][sceneItemName: {sceneItemName}][obsid: {m1.RequestId}]: {sceneItemId}");
|
||||||
|
var m2 = new RequestMessage("GetSceneItemTransform", string.Empty, new Dictionary<string, object>() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId } });
|
||||||
|
await Send(m2, async (d) =>
|
||||||
|
{
|
||||||
|
if (d == null)
|
||||||
|
return;
|
||||||
|
if (!d.TryGetValue("sceneItemTransform", out object transformData))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_logger.Verbose($"Current transformation data [scene: {sceneName}][sceneItemName: {sceneItemName}][sceneItemId: {sceneItemId}][obsid: {m2.RequestId}]: {transformData}");
|
||||||
|
var transform = JsonSerializer.Deserialize<OBSTransformationData>(transformData.ToString(), new JsonSerializerOptions()
|
||||||
|
{
|
||||||
|
PropertyNameCaseInsensitive = false,
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||||
|
});
|
||||||
|
if (transform == null)
|
||||||
|
{
|
||||||
|
_logger.Warning($"Could not deserialize the transformation data received by OBS [scene: {sceneName}][sceneItemName: {sceneItemName}][sceneItemId: {sceneItemId}][obsid: {m2.RequestId}].");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//double fr = (transform.Rotation + rotation) % 360;
|
||||||
|
double w = transform.Width;
|
||||||
|
double h = transform.Height;
|
||||||
|
|
||||||
|
// double ox = w * Math.Cos(r) - h * Math.Sin(r);
|
||||||
|
// double oy = w * Math.Sin(r) + h * Math.Cos(r);
|
||||||
|
//var oo = (fr > 45 && fr < 225 ? 0 : 1);
|
||||||
|
// var ww = fr >= 135 && fr < 225 ? h : w;
|
||||||
|
// var hh = fr >= 315 || fr < 45 ? h : w;
|
||||||
|
//double dx = h * Math.Sin(r);
|
||||||
|
//double dy = w * Math.Cos(fr > 90 && fr < 270 ? Math.PI - r : r); // * (fr >= 135 && fr < 225 || fr >= 315 || fr <= 45 ? -1 : 1);
|
||||||
|
|
||||||
|
int a = transform.Alignment;
|
||||||
|
bool hasBounds = transform.BoundsType != "OBS_BOUNDS_NONE";
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (a != (int)OBSAlignment.Center)
|
||||||
|
{
|
||||||
|
if (hasBounds)
|
||||||
|
transform.BoundsAlignment = a = (int)OBSAlignment.Center;
|
||||||
|
else
|
||||||
|
transform.Alignment = a = (int)OBSAlignment.Center;
|
||||||
|
transform.PositionX = transform.PositionX + w / 2;
|
||||||
|
transform.PositionY = transform.PositionY + h / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
action?.Invoke(transform);
|
||||||
|
|
||||||
|
// double ax = w * Math.Cos(ir) - h * Math.Sin(ir);
|
||||||
|
// double ay = w * Math.Sin(ir) + h * Math.Cos(ir);
|
||||||
|
// _logger.Information($"ax: {ax} ay: {ay}");
|
||||||
|
|
||||||
|
// double bx = w * Math.Cos(r) - h * Math.Sin(r);
|
||||||
|
// double by = w * Math.Sin(r) + h * Math.Cos(r);
|
||||||
|
// _logger.Information($"bx: {bx} by: {by}");
|
||||||
|
|
||||||
|
// double ddx = bx - ax;
|
||||||
|
// double ddy = by - ay;
|
||||||
|
// _logger.Information($"dx: {ddx} dy: {ddy}");
|
||||||
|
|
||||||
|
// double arctan = Math.Atan(ddy / ddx);
|
||||||
|
// _logger.Information("Angle: " + arctan);
|
||||||
|
|
||||||
|
// var xs = new int[] { 0, 0, 1, 1 };
|
||||||
|
// var ys = new int[] { 0, 1, 1, 0 };
|
||||||
|
// int i = ((int)Math.Floor(fr / 90) + 8) % 4;
|
||||||
|
// double dx = xs[i] * w * Math.Cos(rad) - ys[i] * h * Math.Sin(rad);
|
||||||
|
// double dy = xs[i] * w * Math.Sin(rad) + ys[i] * h * Math.Cos(rad);
|
||||||
|
|
||||||
|
|
||||||
|
//transform.Rotation = fr;
|
||||||
|
//_logger.Information($"w: {w} h: {h} fr: {fr} r: {r} rot: {rotation}");
|
||||||
|
//_logger.Information($"dx: {dx} ox: {ox} oox: {oox}");
|
||||||
|
//_logger.Information($"dy: {dy} oy: {oy} ooy: {ooy}");
|
||||||
|
|
||||||
|
var m3 = new RequestMessage("SetSceneItemTransform", string.Empty, new Dictionary<string, object>() { { "sceneName", sceneName }, { "sceneItemId", sceneItemId }, { "sceneItemTransform", transform } });
|
||||||
|
await Send(m3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GenerateUniqueIdentifier()
|
||||||
|
{
|
||||||
|
return Guid.NewGuid().ToString("N");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class RequestData
|
||||||
|
{
|
||||||
|
public RequestMessage Message { get; }
|
||||||
|
public string ParentId { get; }
|
||||||
|
public Dictionary<string, object> ResponseValues { get; set; }
|
||||||
|
public Action<Dictionary<string, object>>? Callback { get; set; }
|
||||||
|
|
||||||
|
public RequestData(RequestMessage message, string parentId)
|
||||||
|
{
|
||||||
|
Message = message;
|
||||||
|
ParentId = parentId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -9,33 +9,34 @@ public class SevenApiClient
|
|||||||
public static readonly string API_URL = "https://7tv.io/v3";
|
public static readonly string API_URL = "https://7tv.io/v3";
|
||||||
public static readonly string WEBSOCKET_URL = "wss://events.7tv.io/v3";
|
public static readonly string WEBSOCKET_URL = "wss://events.7tv.io/v3";
|
||||||
|
|
||||||
private WebClientWrap Web { get; }
|
private readonly WebClientWrap _web;
|
||||||
private ILogger Logger { get; }
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
|
|
||||||
public SevenApiClient(ILogger logger)
|
public SevenApiClient(ILogger logger)
|
||||||
{
|
{
|
||||||
Logger = logger;
|
_logger = logger;
|
||||||
Web = new WebClientWrap(new JsonSerializerOptions()
|
_web = new WebClientWrap(new JsonSerializerOptions()
|
||||||
{
|
{
|
||||||
PropertyNameCaseInsensitive = false,
|
PropertyNameCaseInsensitive = false,
|
||||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
|
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<EmoteSet?> FetchChannelEmoteSet(string twitchId) {
|
public async Task<EmoteSet?> FetchChannelEmoteSet(string twitchId)
|
||||||
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var details = await Web.GetJson<UserDetails>($"{API_URL}/users/twitch/" + twitchId);
|
var details = await _web.GetJson<UserDetails>($"{API_URL}/users/twitch/" + twitchId);
|
||||||
return details?.EmoteSet;
|
return details?.EmoteSet;
|
||||||
}
|
}
|
||||||
catch (JsonException e)
|
catch (JsonException e)
|
||||||
{
|
{
|
||||||
Logger.Error(e, "Failed to fetch emotes from 7tv due to improper JSON.");
|
_logger.Error(e, "Failed to fetch emotes from 7tv due to improper JSON.");
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Logger.Error(e, "Failed to fetch emotes from 7tv.");
|
_logger.Error(e, "Failed to fetch emotes from 7tv.");
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -44,16 +45,16 @@ public class SevenApiClient
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var emoteSet = await Web.GetJson<EmoteSet>($"{API_URL}/emote-sets/6353512c802a0e34bac96dd2");
|
var emoteSet = await _web.GetJson<EmoteSet>($"{API_URL}/emote-sets/6353512c802a0e34bac96dd2");
|
||||||
return emoteSet?.Emotes;
|
return emoteSet?.Emotes;
|
||||||
}
|
}
|
||||||
catch (JsonException e)
|
catch (JsonException e)
|
||||||
{
|
{
|
||||||
Logger.Error(e, "Failed to fetch emotes from 7tv due to improper JSON.");
|
_logger.Error(e, "Failed to fetch emotes from 7tv due to improper JSON.");
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Logger.Error(e, "Failed to fetch emotes from 7tv.");
|
_logger.Error(e, "Failed to fetch emotes from 7tv.");
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using CommonSocketLibrary.Abstract;
|
using CommonSocketLibrary.Abstract;
|
||||||
using CommonSocketLibrary.Common;
|
using CommonSocketLibrary.Common;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Serilog;
|
using Serilog;
|
||||||
using TwitchChatTTS.Seven.Socket.Data;
|
using TwitchChatTTS.Seven.Socket.Data;
|
||||||
|
|
||||||
@ -9,26 +8,26 @@ namespace TwitchChatTTS.Seven.Socket.Handlers
|
|||||||
{
|
{
|
||||||
public class DispatchHandler : IWebSocketHandler
|
public class DispatchHandler : IWebSocketHandler
|
||||||
{
|
{
|
||||||
private ILogger Logger { get; }
|
private readonly ILogger _logger;
|
||||||
private EmoteDatabase Emotes { get; }
|
private readonly EmoteDatabase _emotes;
|
||||||
private object _lock = new object();
|
private readonly object _lock = new object();
|
||||||
public int OperationCode { get; set; } = 0;
|
public int OperationCode { get; } = 0;
|
||||||
|
|
||||||
public DispatchHandler(ILogger logger, EmoteDatabase emotes)
|
public DispatchHandler(ILogger logger, EmoteDatabase emotes)
|
||||||
{
|
{
|
||||||
Logger = logger;
|
_logger = logger;
|
||||||
Emotes = emotes;
|
_emotes = emotes;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data message)
|
public async Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data data)
|
||||||
{
|
{
|
||||||
if (message is not DispatchMessage obj || obj == null)
|
if (data is not DispatchMessage message || message == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
ApplyChanges(obj?.Body?.Pulled, cf => cf.OldValue, true);
|
ApplyChanges(message?.Body?.Pulled, cf => cf.OldValue, true);
|
||||||
ApplyChanges(obj?.Body?.Pushed, cf => cf.Value, false);
|
ApplyChanges(message?.Body?.Pushed, cf => cf.Value, false);
|
||||||
ApplyChanges(obj?.Body?.Removed, cf => cf.OldValue, true);
|
ApplyChanges(message?.Body?.Removed, cf => cf.OldValue, true);
|
||||||
ApplyChanges(obj?.Body?.Updated, cf => cf.OldValue, false, cf => cf.Value);
|
ApplyChanges(message?.Body?.Updated, cf => cf.OldValue, false, cf => cf.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ApplyChanges(IEnumerable<ChangeField>? fields, Func<ChangeField, object> getter, bool removing, Func<ChangeField, object>? updater = null)
|
private void ApplyChanges(IEnumerable<ChangeField>? fields, Func<ChangeField, object> getter, bool removing, Func<ChangeField, object>? updater = null)
|
||||||
@ -55,7 +54,7 @@ namespace TwitchChatTTS.Seven.Socket.Handlers
|
|||||||
if (removing)
|
if (removing)
|
||||||
{
|
{
|
||||||
RemoveEmoteById(o.Id);
|
RemoveEmoteById(o.Id);
|
||||||
Logger.Information($"Removed 7tv emote: {o.Name} (id: {o.Id})");
|
_logger.Information($"Removed 7tv emote [name: {o.Name}][id: {o.Id}]");
|
||||||
}
|
}
|
||||||
else if (updater != null)
|
else if (updater != null)
|
||||||
{
|
{
|
||||||
@ -70,18 +69,18 @@ namespace TwitchChatTTS.Seven.Socket.Handlers
|
|||||||
|
|
||||||
if (u != null)
|
if (u != null)
|
||||||
{
|
{
|
||||||
Emotes.Add(u.Name, u.Id);
|
_emotes.Add(u.Name, u.Id);
|
||||||
Logger.Information($"Updated 7tv emote: from '{o.Name}' to '{u.Name}' (id: {u.Id})");
|
_logger.Information($"Updated 7tv emote [old name: {o.Name}][new name: {u.Name}][id: {u.Id}]");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Logger.Warning("Failed to update 7tv emote.");
|
_logger.Warning("Failed to update 7tv emote.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Emotes.Add(o.Name, o.Id);
|
_emotes.Add(o.Name, o.Id);
|
||||||
Logger.Information($"Added 7tv emote: {o.Name} (id: {o.Id})");
|
_logger.Information($"Added 7tv emote [name: {o.Name}][id: {o.Id}]");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -90,7 +89,7 @@ namespace TwitchChatTTS.Seven.Socket.Handlers
|
|||||||
private void RemoveEmoteById(string id)
|
private void RemoveEmoteById(string id)
|
||||||
{
|
{
|
||||||
string? key = null;
|
string? key = null;
|
||||||
foreach (var e in Emotes.Emotes)
|
foreach (var e in _emotes.Emotes)
|
||||||
{
|
{
|
||||||
if (e.Value == id)
|
if (e.Value == id)
|
||||||
{
|
{
|
||||||
@ -99,7 +98,7 @@ namespace TwitchChatTTS.Seven.Socket.Handlers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (key != null)
|
if (key != null)
|
||||||
Emotes.Remove(key);
|
_emotes.Remove(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -9,22 +9,22 @@ namespace TwitchChatTTS.Seven.Socket.Handlers
|
|||||||
{
|
{
|
||||||
public class EndOfStreamHandler : IWebSocketHandler
|
public class EndOfStreamHandler : IWebSocketHandler
|
||||||
{
|
{
|
||||||
private ILogger Logger { get; }
|
private readonly ILogger _logger;
|
||||||
private User User { get; }
|
private readonly User _user;
|
||||||
private IServiceProvider ServiceProvider { get; }
|
private readonly IServiceProvider _serviceProvider;
|
||||||
private string[] ErrorCodes { get; }
|
private readonly string[] _errorCodes;
|
||||||
private int[] ReconnectDelay { get; }
|
private readonly int[] _reconnectDelay;
|
||||||
|
|
||||||
public int OperationCode { get; set; } = 7;
|
public int OperationCode { get; } = 7;
|
||||||
|
|
||||||
|
|
||||||
public EndOfStreamHandler(ILogger logger, User user, IServiceProvider serviceProvider)
|
public EndOfStreamHandler(ILogger logger, User user, IServiceProvider serviceProvider)
|
||||||
{
|
{
|
||||||
Logger = logger;
|
_logger = logger;
|
||||||
User = user;
|
_user = user;
|
||||||
ServiceProvider = serviceProvider;
|
_serviceProvider = serviceProvider;
|
||||||
|
|
||||||
ErrorCodes = [
|
_errorCodes = [
|
||||||
"Server Error",
|
"Server Error",
|
||||||
"Unknown Operation",
|
"Unknown Operation",
|
||||||
"Invalid Payload",
|
"Invalid Payload",
|
||||||
@ -39,61 +39,62 @@ namespace TwitchChatTTS.Seven.Socket.Handlers
|
|||||||
"Insufficient Privilege",
|
"Insufficient Privilege",
|
||||||
"Inactivity?"
|
"Inactivity?"
|
||||||
];
|
];
|
||||||
ReconnectDelay = [
|
_reconnectDelay = [
|
||||||
1000,
|
1000,
|
||||||
-1,
|
0,
|
||||||
-1,
|
0,
|
||||||
-1,
|
0,
|
||||||
-1,
|
0,
|
||||||
3000,
|
3000,
|
||||||
1000,
|
1000,
|
||||||
300000,
|
300000,
|
||||||
1000,
|
1000,
|
||||||
-1,
|
0,
|
||||||
-1,
|
0,
|
||||||
1000,
|
1000,
|
||||||
1000
|
1000
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data message)
|
public async Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data data)
|
||||||
{
|
{
|
||||||
if (message is not EndOfStreamMessage obj || obj == null)
|
if (data is not EndOfStreamMessage message || message == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var code = obj.Code - 4000;
|
var code = message.Code - 4000;
|
||||||
if (code >= 0 && code < ErrorCodes.Length)
|
if (code >= 0 && code < _errorCodes.Length)
|
||||||
Logger.Warning($"Received end of stream message (reason: {ErrorCodes[code]}, code: {obj.Code}, message: {obj.Message}).");
|
_logger.Warning($"Received end of stream message (reason: {_errorCodes[code]}, code: {message.Code}, message: {message.Message}).");
|
||||||
else
|
else
|
||||||
Logger.Warning($"Received end of stream message (code: {obj.Code}, message: {obj.Message}).");
|
_logger.Warning($"Received end of stream message (code: {message.Code}, message: {message.Message}).");
|
||||||
|
|
||||||
await sender.DisconnectAsync();
|
await sender.DisconnectAsync();
|
||||||
|
|
||||||
if (code >= 0 && code < ReconnectDelay.Length && ReconnectDelay[code] < 0)
|
if (code >= 0 && code < _reconnectDelay.Length && _reconnectDelay[code] < 0)
|
||||||
{
|
{
|
||||||
Logger.Error($"7tv client will remain disconnected due to a bad client implementation.");
|
_logger.Error($"7tv client will remain disconnected due to a bad client implementation.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(User.SevenEmoteSetId))
|
if (string.IsNullOrWhiteSpace(_user.SevenEmoteSetId))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var context = ServiceProvider.GetRequiredService<ReconnectContext>();
|
var context = _serviceProvider.GetRequiredService<ReconnectContext>();
|
||||||
await Task.Delay(ReconnectDelay[code]);
|
if (_reconnectDelay[code] > 0)
|
||||||
|
await Task.Delay(_reconnectDelay[code]);
|
||||||
|
|
||||||
var base_url = $"@emote_set.*<object_id={User.SevenEmoteSetId}>";
|
var base_url = $"@emote_set.*<object_id={_user.SevenEmoteSetId}>";
|
||||||
string url = $"{SevenApiClient.WEBSOCKET_URL}{base_url}";
|
string url = $"{SevenApiClient.WEBSOCKET_URL}{base_url}";
|
||||||
Logger.Debug($"7tv websocket reconnecting to {url}.");
|
_logger.Debug($"7tv websocket reconnecting to {url}.");
|
||||||
|
|
||||||
await sender.ConnectAsync(url);
|
await sender.ConnectAsync(url);
|
||||||
if (context.SessionId != null)
|
if (context.SessionId != null)
|
||||||
{
|
{
|
||||||
await sender.Send(34, new ResumeMessage() { SessionId = context.SessionId });
|
await sender.Send(34, new ResumeMessage() { SessionId = context.SessionId });
|
||||||
Logger.Information("Resumed connection to 7tv websocket.");
|
_logger.Information("Resumed connection to 7tv websocket.");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Logger.Information("Resumed connection to 7tv websocket on a different session.");
|
_logger.Information("Resumed connection to 7tv websocket on a different session.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,17 +7,17 @@ namespace TwitchChatTTS.Seven.Socket.Handlers
|
|||||||
{
|
{
|
||||||
public class ErrorHandler : IWebSocketHandler
|
public class ErrorHandler : IWebSocketHandler
|
||||||
{
|
{
|
||||||
private ILogger Logger { get; }
|
private readonly ILogger _logger;
|
||||||
public int OperationCode { get; set; } = 6;
|
public int OperationCode { get; } = 6;
|
||||||
|
|
||||||
public ErrorHandler(ILogger logger)
|
public ErrorHandler(ILogger logger)
|
||||||
{
|
{
|
||||||
Logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data message)
|
public async Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data data)
|
||||||
{
|
{
|
||||||
if (message is not ErrorMessage obj || obj == null)
|
if (data is not ErrorMessage message || message == null)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,20 +7,20 @@ namespace TwitchChatTTS.Seven.Socket.Handlers
|
|||||||
{
|
{
|
||||||
public class ReconnectHandler : IWebSocketHandler
|
public class ReconnectHandler : IWebSocketHandler
|
||||||
{
|
{
|
||||||
private ILogger Logger { get; }
|
private readonly ILogger _logger;
|
||||||
public int OperationCode { get; set; } = 4;
|
public int OperationCode { get; } = 4;
|
||||||
|
|
||||||
public ReconnectHandler(ILogger logger)
|
public ReconnectHandler(ILogger logger)
|
||||||
{
|
{
|
||||||
Logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data message)
|
public async Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data data)
|
||||||
{
|
{
|
||||||
if (message is not ReconnectMessage obj || obj == null)
|
if (data is not ReconnectMessage message || message == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
Logger.Information($"7tv server wants us to reconnect (reason: {obj.Reason}).");
|
_logger.Information($"7tv server wants this client to reconnect (reason: {message.Reason}).");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -7,27 +7,25 @@ namespace TwitchChatTTS.Seven.Socket.Handlers
|
|||||||
{
|
{
|
||||||
public class SevenHelloHandler : IWebSocketHandler
|
public class SevenHelloHandler : IWebSocketHandler
|
||||||
{
|
{
|
||||||
private ILogger Logger { get; }
|
private readonly ILogger _logger;
|
||||||
private Configuration Configuration { get; }
|
public int OperationCode { get; } = 1;
|
||||||
public int OperationCode { get; set; } = 1;
|
|
||||||
|
|
||||||
public SevenHelloHandler(ILogger logger, Configuration configuration)
|
public SevenHelloHandler(ILogger logger)
|
||||||
{
|
{
|
||||||
Logger = logger;
|
_logger = logger;
|
||||||
Configuration = configuration;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data message)
|
public async Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data data)
|
||||||
{
|
{
|
||||||
if (message is not SevenHelloMessage obj || obj == null)
|
if (data is not SevenHelloMessage message || message == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (sender is not SevenSocketClient seven || seven == null)
|
if (sender is not SevenSocketClient seven || seven == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
seven.Connected = true;
|
seven.Connected = true;
|
||||||
seven.ConnectionDetails = obj;
|
seven.ConnectionDetails = message;
|
||||||
Logger.Information("Connected to 7tv websockets.");
|
_logger.Information("Connected to 7tv websockets.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -28,13 +28,13 @@ namespace TwitchChatTTS.Seven.Socket.Managers
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.Debug($"Linked type {type.AssemblyQualifiedName} to 7tv websocket handler {handler.GetType().AssemblyQualifiedName}.");
|
_logger.Debug($"Linked type {type.AssemblyQualifiedName} to 7tv websocket handler {handler.GetType().AssemblyQualifiedName}.");
|
||||||
Add(handler);
|
Add(handler);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Logger.Error(e, "Failed to load 7tv websocket handler types.");
|
_logger.Error(e, "Failed to load 7tv websocket handler types.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
30
Startup.cs
30
Startup.cs
@ -27,6 +27,8 @@ using TwitchChatTTS.Chat.Commands;
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
using Serilog.Events;
|
using Serilog.Events;
|
||||||
|
using Serilog.Sinks.SystemConsole.Themes;
|
||||||
|
using TwitchChatTTS.Twitch.Redemptions;
|
||||||
|
|
||||||
// dotnet publish -r linux-x64 -p:PublishSingleFile=true --self-contained true
|
// dotnet publish -r linux-x64 -p:PublishSingleFile=true --self-contained true
|
||||||
// dotnet publish -r win-x64 -p:PublishSingleFile=true --self-contained true
|
// dotnet publish -r win-x64 -p:PublishSingleFile=true --self-contained true
|
||||||
@ -36,30 +38,23 @@ HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
|
|||||||
var s = builder.Services;
|
var s = builder.Services;
|
||||||
|
|
||||||
var deserializer = new DeserializerBuilder()
|
var deserializer = new DeserializerBuilder()
|
||||||
|
.IgnoreUnmatchedProperties()
|
||||||
.WithNamingConvention(HyphenatedNamingConvention.Instance)
|
.WithNamingConvention(HyphenatedNamingConvention.Instance)
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
var configContent = File.ReadAllText("tts.config.yml");
|
var configContent = File.ReadAllText("tts.config.yml");
|
||||||
var configuration = deserializer.Deserialize<Configuration>(configContent);
|
var configuration = deserializer.Deserialize<Configuration>(configContent);
|
||||||
var redeemKeys = configuration.Twitch?.Redeems?.Keys;
|
|
||||||
if (redeemKeys != null && redeemKeys.Any())
|
|
||||||
{
|
|
||||||
foreach (var key in redeemKeys)
|
|
||||||
{
|
|
||||||
if (key != key.ToLower())
|
|
||||||
configuration.Twitch.Redeems.Add(key.ToLower(), configuration.Twitch.Redeems[key]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s.AddSingleton<Configuration>(configuration);
|
s.AddSingleton<Configuration>(configuration);
|
||||||
|
|
||||||
var logger = new LoggerConfiguration()
|
var logger = new LoggerConfiguration()
|
||||||
#if DEBUG
|
.MinimumLevel.Verbose()
|
||||||
.MinimumLevel.Debug()
|
//.MinimumLevel.Override("TwitchLib.Communication.Clients.WebSocketClient", LogEventLevel.Warning)
|
||||||
#else
|
//.MinimumLevel.Override("TwitchLib.PubSub.TwitchPubSub", LogEventLevel.Warning)
|
||||||
.MinimumLevel.Information()
|
.MinimumLevel.Override("TwitchLib", LogEventLevel.Warning)
|
||||||
#endif
|
.MinimumLevel.Override("mariuszgromada", LogEventLevel.Error)
|
||||||
.WriteTo.File("logs/log.log", rollingInterval: RollingInterval.Day, retainedFileCountLimit: 7)
|
.Enrich.FromLogContext()
|
||||||
.WriteTo.Console(restrictedToMinimumLevel: LogEventLevel.Information)
|
.WriteTo.File("logs/log-.log", restrictedToMinimumLevel: LogEventLevel.Debug, rollingInterval: RollingInterval.Day, retainedFileCountLimit: 7)
|
||||||
|
.WriteTo.Console(restrictedToMinimumLevel: LogEventLevel.Information, theme: SystemConsoleTheme.Colored)
|
||||||
.CreateLogger();
|
.CreateLogger();
|
||||||
|
|
||||||
s.AddSerilog(logger);
|
s.AddSerilog(logger);
|
||||||
@ -87,6 +82,7 @@ s.AddSingleton<ChatCommandManager>();
|
|||||||
|
|
||||||
s.AddSingleton<TTSPlayer>();
|
s.AddSingleton<TTSPlayer>();
|
||||||
s.AddSingleton<ChatMessageHandler>();
|
s.AddSingleton<ChatMessageHandler>();
|
||||||
|
s.AddSingleton<RedemptionManager>();
|
||||||
s.AddSingleton<HermesApiClient>();
|
s.AddSingleton<HermesApiClient>();
|
||||||
s.AddSingleton<TwitchBotAuth>(new TwitchBotAuth());
|
s.AddSingleton<TwitchBotAuth>(new TwitchBotAuth());
|
||||||
s.AddTransient<IClient, TwitchLib.Communication.Clients.WebSocketClient>();
|
s.AddTransient<IClient, TwitchLib.Communication.Clients.WebSocketClient>();
|
||||||
@ -106,7 +102,7 @@ s.AddSingleton<HelloContext>(sp =>
|
|||||||
Password = string.IsNullOrWhiteSpace(configuration.Obs?.Password) ? null : configuration.Obs.Password.Trim()
|
Password = string.IsNullOrWhiteSpace(configuration.Obs?.Password) ? null : configuration.Obs.Password.Trim()
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
s.AddSingleton<OBSRequestBatchManager>();
|
s.AddSingleton<OBSManager>();
|
||||||
s.AddKeyedSingleton<IWebSocketHandler, HelloHandler>("obs-hello");
|
s.AddKeyedSingleton<IWebSocketHandler, HelloHandler>("obs-hello");
|
||||||
s.AddKeyedSingleton<IWebSocketHandler, IdentifiedHandler>("obs-identified");
|
s.AddKeyedSingleton<IWebSocketHandler, IdentifiedHandler>("obs-identified");
|
||||||
s.AddKeyedSingleton<IWebSocketHandler, RequestResponseHandler>("obs-requestresponse");
|
s.AddKeyedSingleton<IWebSocketHandler, RequestResponseHandler>("obs-requestresponse");
|
||||||
|
44
TTS.cs
44
TTS.cs
@ -7,9 +7,10 @@ using Microsoft.Extensions.DependencyInjection;
|
|||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
using NAudio.Wave.SampleProviders;
|
using NAudio.Wave.SampleProviders;
|
||||||
using TwitchChatTTS.Hermes.Socket;
|
|
||||||
using TwitchChatTTS.Seven;
|
using TwitchChatTTS.Seven;
|
||||||
using TwitchLib.Client.Events;
|
using TwitchLib.Client.Events;
|
||||||
|
using TwitchChatTTS.Twitch.Redemptions;
|
||||||
|
using org.mariuszgromada.math.mxparser;
|
||||||
|
|
||||||
namespace TwitchChatTTS
|
namespace TwitchChatTTS
|
||||||
{
|
{
|
||||||
@ -18,17 +19,28 @@ namespace TwitchChatTTS
|
|||||||
public const int MAJOR_VERSION = 3;
|
public const int MAJOR_VERSION = 3;
|
||||||
public const int MINOR_VERSION = 3;
|
public const int MINOR_VERSION = 3;
|
||||||
|
|
||||||
private readonly ILogger _logger;
|
private readonly RedemptionManager _redemptionManager;
|
||||||
private readonly Configuration _configuration;
|
private readonly Configuration _configuration;
|
||||||
private readonly TTSPlayer _player;
|
private readonly TTSPlayer _player;
|
||||||
private readonly IServiceProvider _serviceProvider;
|
private readonly IServiceProvider _serviceProvider;
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
public TTS(ILogger logger, Configuration configuration, TTSPlayer player, IServiceProvider serviceProvider)
|
public TTS(
|
||||||
|
User user,
|
||||||
|
HermesApiClient hermesApiClient,
|
||||||
|
SevenApiClient sevenApiClient,
|
||||||
|
RedemptionManager redemptionManager,
|
||||||
|
Configuration configuration,
|
||||||
|
TTSPlayer player,
|
||||||
|
IServiceProvider serviceProvider,
|
||||||
|
ILogger logger
|
||||||
|
)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_redemptionManager = redemptionManager;
|
||||||
_configuration = configuration;
|
_configuration = configuration;
|
||||||
_player = player;
|
_player = player;
|
||||||
_serviceProvider = serviceProvider;
|
_serviceProvider = serviceProvider;
|
||||||
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task StartAsync(CancellationToken cancellationToken)
|
public async Task StartAsync(CancellationToken cancellationToken)
|
||||||
@ -69,6 +81,8 @@ namespace TwitchChatTTS
|
|||||||
var emoteSet = await seven.FetchChannelEmoteSet(user.TwitchUserId.ToString());
|
var emoteSet = await seven.FetchChannelEmoteSet(user.TwitchUserId.ToString());
|
||||||
user.SevenEmoteSetId = emoteSet?.Id;
|
user.SevenEmoteSetId = emoteSet?.Id;
|
||||||
|
|
||||||
|
License.iConfirmCommercialUse("abcdef");
|
||||||
|
|
||||||
await InitializeEmotes(seven, emoteSet);
|
await InitializeEmotes(seven, emoteSet);
|
||||||
await InitializeHermesWebsocket();
|
await InitializeHermesWebsocket();
|
||||||
await InitializeSevenTv(emoteSet.Id);
|
await InitializeSevenTv(emoteSet.Id);
|
||||||
@ -106,14 +120,14 @@ namespace TwitchChatTTS
|
|||||||
var provider = new CachedWavProvider(sound);
|
var provider = new CachedWavProvider(sound);
|
||||||
var data = AudioPlaybackEngine.Instance.ConvertSound(provider);
|
var data = AudioPlaybackEngine.Instance.ConvertSound(provider);
|
||||||
var resampled = new WdlResamplingSampleProvider(data, AudioPlaybackEngine.Instance.SampleRate);
|
var resampled = new WdlResamplingSampleProvider(data, AudioPlaybackEngine.Instance.SampleRate);
|
||||||
_logger.Debug("Fetched TTS audio data.");
|
_logger.Verbose("Fetched TTS audio data.");
|
||||||
|
|
||||||
m.Audio = resampled;
|
m.Audio = resampled;
|
||||||
_player.Ready(m);
|
_player.Ready(m);
|
||||||
}
|
}
|
||||||
catch (COMException e)
|
catch (COMException e)
|
||||||
{
|
{
|
||||||
_logger.Error(e, "Failed to send request for TTS (HResult: " + e.HResult + ").");
|
_logger.Error(e, "Failed to send request for TTS [HResult: " + e.HResult + "]");
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
@ -187,7 +201,7 @@ namespace TwitchChatTTS
|
|||||||
|
|
||||||
var twitchBotToken = await hermes.FetchTwitchBotToken();
|
var twitchBotToken = await hermes.FetchTwitchBotToken();
|
||||||
user.TwitchUserId = long.Parse(twitchBotToken.BroadcasterId);
|
user.TwitchUserId = long.Parse(twitchBotToken.BroadcasterId);
|
||||||
_logger.Information($"Username: {user.TwitchUsername} (id: {user.TwitchUserId})");
|
_logger.Information($"Username: {user.TwitchUsername} [id: {user.TwitchUserId}]");
|
||||||
|
|
||||||
user.DefaultTTSVoice = await hermes.FetchTTSDefaultVoice();
|
user.DefaultTTSVoice = await hermes.FetchTTSDefaultVoice();
|
||||||
_logger.Information("Default Voice: " + user.DefaultTTSVoice);
|
_logger.Information("Default Voice: " + user.DefaultTTSVoice);
|
||||||
@ -207,13 +221,23 @@ namespace TwitchChatTTS
|
|||||||
|
|
||||||
var voicesEnabled = await hermes.FetchTTSEnabledVoices();
|
var voicesEnabled = await hermes.FetchTTSEnabledVoices();
|
||||||
if (voicesEnabled == null || !voicesEnabled.Any())
|
if (voicesEnabled == null || !voicesEnabled.Any())
|
||||||
user.VoicesEnabled = new HashSet<string>(new string[] { "Brian" });
|
user.VoicesEnabled = new HashSet<string>(["Brian"]);
|
||||||
else
|
else
|
||||||
user.VoicesEnabled = new HashSet<string>(voicesEnabled.Select(v => v));
|
user.VoicesEnabled = new HashSet<string>(voicesEnabled.Select(v => v));
|
||||||
_logger.Information($"{user.VoicesEnabled.Count} TTS voices have been enabled.");
|
_logger.Information($"{user.VoicesEnabled.Count} TTS voices have been enabled.");
|
||||||
|
|
||||||
var defaultedChatters = voicesSelected.Where(item => item.Voice == null || !user.VoicesEnabled.Contains(item.Voice));
|
var defaultedChatters = voicesSelected.Where(item => item.Voice == null || !user.VoicesEnabled.Contains(item.Voice));
|
||||||
_logger.Information($"{defaultedChatters.Count()} chatters will have their TTS voice set to default due to having selected a disabled TTS voice.");
|
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();
|
||||||
|
foreach (var action in redemptionActions)
|
||||||
|
_redemptionManager.AddAction(action);
|
||||||
|
foreach (var redemption in redemptions)
|
||||||
|
_redemptionManager.AddTwitchRedemption(redemption);
|
||||||
|
_redemptionManager.Ready();
|
||||||
|
_logger.Information($"Redemption Manager is ready with {redemptionActions.Count()} actions & {redemptions.Count()} redemptions.");
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task InitializeHermesWebsocket()
|
private async Task InitializeHermesWebsocket()
|
||||||
@ -221,7 +245,7 @@ namespace TwitchChatTTS
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
_logger.Information("Initializing hermes websocket client.");
|
_logger.Information("Initializing hermes websocket client.");
|
||||||
var hermesClient = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("hermes") as HermesSocketClient;
|
var hermesClient = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("hermes");
|
||||||
var url = "wss://hermes-ws.goblincaves.com";
|
var url = "wss://hermes-ws.goblincaves.com";
|
||||||
_logger.Debug($"Attempting to connect to {url}");
|
_logger.Debug($"Attempting to connect to {url}");
|
||||||
await hermesClient.ConnectAsync(url);
|
await hermesClient.ConnectAsync(url);
|
||||||
|
9
Twitch/Redemptions/Action.cs
Normal file
9
Twitch/Redemptions/Action.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
namespace TwitchChatTTS.Twitch.Redemptions
|
||||||
|
{
|
||||||
|
public class RedeemableAction
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string Type { get; set; }
|
||||||
|
public IDictionary<string, string> Data { get; set; }
|
||||||
|
}
|
||||||
|
}
|
11
Twitch/Redemptions/Redemption.cs
Normal file
11
Twitch/Redemptions/Redemption.cs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
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; }
|
||||||
|
}
|
||||||
|
}
|
148
Twitch/Redemptions/RedemptionManager.cs
Normal file
148
Twitch/Redemptions/RedemptionManager.cs
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
using org.mariuszgromada.math.mxparser;
|
||||||
|
using Serilog;
|
||||||
|
using TwitchChatTTS.OBS.Socket.Data;
|
||||||
|
using TwitchChatTTS.OBS.Socket.Manager;
|
||||||
|
|
||||||
|
namespace TwitchChatTTS.Twitch.Redemptions
|
||||||
|
{
|
||||||
|
public class RedemptionManager
|
||||||
|
{
|
||||||
|
private readonly IList<Redemption> _redemptions;
|
||||||
|
private readonly IDictionary<string, RedeemableAction> _actions;
|
||||||
|
private readonly IDictionary<string, IList<RedeemableAction>> _store;
|
||||||
|
private readonly OBSManager _obsManager;
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
private bool _isReady;
|
||||||
|
|
||||||
|
|
||||||
|
public RedemptionManager(OBSManager obsManager, ILogger logger)
|
||||||
|
{
|
||||||
|
_redemptions = new List<Redemption>();
|
||||||
|
_actions = new Dictionary<string, RedeemableAction>();
|
||||||
|
_store = new Dictionary<string, IList<RedeemableAction>>();
|
||||||
|
_obsManager = obsManager;
|
||||||
|
_logger = logger;
|
||||||
|
_isReady = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddTwitchRedemption(Redemption redemption)
|
||||||
|
{
|
||||||
|
_redemptions.Add(redemption);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddAction(RedeemableAction action)
|
||||||
|
{
|
||||||
|
_actions.Add(action.Name, action);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Add(string twitchRedemptionId, RedeemableAction action)
|
||||||
|
{
|
||||||
|
if (!_store.TryGetValue(twitchRedemptionId, out var actions))
|
||||||
|
_store.Add(twitchRedemptionId, actions = new List<RedeemableAction>());
|
||||||
|
actions.Add(action);
|
||||||
|
_store[twitchRedemptionId] = actions.OrderBy(a => a).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Execute(RedeemableAction action, string sender)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
switch (action.Type)
|
||||||
|
{
|
||||||
|
case "WRITE_TO_FILE":
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(action.Data["file_path"]));
|
||||||
|
await File.WriteAllTextAsync(action.Data["file_path"], ReplaceContentText(action.Data["file_content"], sender));
|
||||||
|
_logger.Debug($"Overwritten text to file [file: {action.Data["file_path"]}]");
|
||||||
|
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"], sender));
|
||||||
|
_logger.Debug($"Appended text to file [file: {action.Data["file_path"]}]");
|
||||||
|
break;
|
||||||
|
case "OBS_TRANSFORM":
|
||||||
|
var type = typeof(OBSTransformationData);
|
||||||
|
await _obsManager.UpdateTransformation(action.Data["scene_name"], action.Data["scene_item_name"], (d) =>
|
||||||
|
{
|
||||||
|
string[] properties = ["rotation", "position_x", "position_y"];
|
||||||
|
foreach (var property in properties)
|
||||||
|
{
|
||||||
|
if (!action.Data.TryGetValue(property, out var expressionString) || expressionString == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var propertyName = string.Join("", property.Split('_').Select(p => char.ToUpper(p[0]) + p.Substring(1)));
|
||||||
|
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}]");
|
||||||
|
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}]");
|
||||||
|
}
|
||||||
|
|
||||||
|
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}]");
|
||||||
|
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($"Finished applying the OBS transformation property changes [scene: {action.Data["scene_name"]}][source: {action.Data["scene_item_name"]}]");
|
||||||
|
});
|
||||||
|
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"]}]");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
AudioPlaybackEngine.Instance.PlaySound(action.Data["file_path"]);
|
||||||
|
_logger.Debug($"Played an audio file for channel point redeem [file: {action.Data["file_path"]}]");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
_logger.Warning($"Unknown redeemable action has occured [type: {action.Type}]");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Error(ex, "Failed to execute a redemption action.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IList<RedeemableAction> Get(string twitchRedemptionId)
|
||||||
|
{
|
||||||
|
if (!_isReady)
|
||||||
|
throw new InvalidOperationException("Not ready");
|
||||||
|
|
||||||
|
if (_store.TryGetValue(twitchRedemptionId, out var actions))
|
||||||
|
return actions;
|
||||||
|
return new List<RedeemableAction>(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Ready()
|
||||||
|
{
|
||||||
|
var ordered = _redemptions.OrderBy(r => r.Order);
|
||||||
|
_store.Clear();
|
||||||
|
|
||||||
|
foreach (var redemption in ordered)
|
||||||
|
if (_actions.TryGetValue(redemption.ActionName, out var action) && action != null)
|
||||||
|
Add(redemption.RedemptionId, action);
|
||||||
|
|
||||||
|
_isReady = true;
|
||||||
|
_logger.Debug("Redemption Manager is ready.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ReplaceContentText(string content, string username) {
|
||||||
|
return content.Replace("%USER%", username);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -6,28 +6,32 @@ using TwitchLib.Api.Core.Exceptions;
|
|||||||
using TwitchLib.Client.Events;
|
using TwitchLib.Client.Events;
|
||||||
using TwitchLib.Client.Models;
|
using TwitchLib.Client.Models;
|
||||||
using TwitchLib.Communication.Events;
|
using TwitchLib.Communication.Events;
|
||||||
using static TwitchChatTTS.Configuration;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using CommonSocketLibrary.Abstract;
|
using CommonSocketLibrary.Abstract;
|
||||||
using CommonSocketLibrary.Common;
|
using CommonSocketLibrary.Common;
|
||||||
using TwitchLib.PubSub.Interfaces;
|
using TwitchLib.PubSub.Interfaces;
|
||||||
using TwitchLib.Client.Interfaces;
|
using TwitchLib.Client.Interfaces;
|
||||||
using TwitchChatTTS.OBS.Socket;
|
using TwitchChatTTS.OBS.Socket;
|
||||||
|
using TwitchChatTTS.Twitch.Redemptions;
|
||||||
|
|
||||||
public class TwitchApiClient
|
public class TwitchApiClient
|
||||||
{
|
{
|
||||||
|
private readonly RedemptionManager _redemptionManager;
|
||||||
|
private readonly HermesApiClient _hermesApiClient;
|
||||||
private readonly Configuration _configuration;
|
private readonly Configuration _configuration;
|
||||||
private readonly ILogger _logger;
|
private readonly TwitchBotAuth _token;
|
||||||
private TwitchBotAuth _token;
|
|
||||||
private readonly ITwitchClient _client;
|
private readonly ITwitchClient _client;
|
||||||
private readonly ITwitchPubSub _publisher;
|
private readonly ITwitchPubSub _publisher;
|
||||||
private readonly WebClientWrap _web;
|
private readonly WebClientWrap _web;
|
||||||
private readonly IServiceProvider _serviceProvider;
|
private readonly IServiceProvider _serviceProvider;
|
||||||
|
private readonly ILogger _logger;
|
||||||
private bool _initialized;
|
private bool _initialized;
|
||||||
private string _broadcasterId;
|
private string _broadcasterId;
|
||||||
|
|
||||||
|
|
||||||
public TwitchApiClient(
|
public TwitchApiClient(
|
||||||
|
RedemptionManager redemptionManager,
|
||||||
|
HermesApiClient hermesApiClient,
|
||||||
Configuration configuration,
|
Configuration configuration,
|
||||||
TwitchBotAuth token,
|
TwitchBotAuth token,
|
||||||
ITwitchClient twitchClient,
|
ITwitchClient twitchClient,
|
||||||
@ -36,6 +40,8 @@ public class TwitchApiClient
|
|||||||
ILogger logger
|
ILogger logger
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
|
_redemptionManager = redemptionManager;
|
||||||
|
_hermesApiClient = hermesApiClient;
|
||||||
_configuration = configuration;
|
_configuration = configuration;
|
||||||
_token = token;
|
_token = token;
|
||||||
_client = twitchClient;
|
_client = twitchClient;
|
||||||
@ -43,6 +49,7 @@ public class TwitchApiClient
|
|||||||
_serviceProvider = serviceProvider;
|
_serviceProvider = serviceProvider;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_initialized = false;
|
_initialized = false;
|
||||||
|
_broadcasterId = string.Empty;
|
||||||
|
|
||||||
_web = new WebClientWrap(new JsonSerializerOptions()
|
_web = new WebClientWrap(new JsonSerializerOptions()
|
||||||
{
|
{
|
||||||
@ -126,6 +133,9 @@ public class TwitchApiClient
|
|||||||
|
|
||||||
_logger.Information("Attempting to re-authorize.");
|
_logger.Information("Attempting to re-authorize.");
|
||||||
await Authorize(_broadcasterId);
|
await Authorize(_broadcasterId);
|
||||||
|
await _client.DisconnectAsync();
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(1));
|
||||||
|
await _client.ConnectAsync();
|
||||||
};
|
};
|
||||||
|
|
||||||
_client.OnConnectionError += async Task (object? s, OnConnectionErrorArgs e) =>
|
_client.OnConnectionError += async Task (object? s, OnConnectionErrorArgs e) =>
|
||||||
@ -159,67 +169,49 @@ public class TwitchApiClient
|
|||||||
if (_configuration.Twitch?.TtsWhenOffline != true && client?.Live == false)
|
if (_configuration.Twitch?.TtsWhenOffline != true && client?.Live == false)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
_logger.Information("Follow: " + e.DisplayName);
|
_logger.Information($"New Follower [name: {e.DisplayName}][username: {e.Username}]");
|
||||||
};
|
};
|
||||||
|
|
||||||
_publisher.OnChannelPointsRewardRedeemed += (s, e) =>
|
_publisher.OnChannelPointsRewardRedeemed += async (s, e) =>
|
||||||
{
|
{
|
||||||
var client = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("obs") as OBSSocketClient;
|
var client = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("obs") as OBSSocketClient;
|
||||||
if (_configuration.Twitch?.TtsWhenOffline != true && client?.Live == false)
|
if (_configuration.Twitch?.TtsWhenOffline != true && client?.Live == false)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
_logger.Information($"Channel Point Reward Redeemed [redeem: {e.RewardRedeemed.Redemption.Reward.Title}][id: {e.RewardRedeemed.Redemption.Id}]");
|
_logger.Information($"Channel Point Reward Redeemed [redeem: {e.RewardRedeemed.Redemption.Reward.Title}][redeem id: {e.RewardRedeemed.Redemption.Reward.Id}][transaction: {e.RewardRedeemed.Redemption.Id}]");
|
||||||
|
|
||||||
if (_configuration.Twitch?.Redeems == null)
|
var actions = _redemptionManager.Get(e.RewardRedeemed.Redemption.Reward.Id);
|
||||||
|
if (!actions.Any())
|
||||||
|
{
|
||||||
|
_logger.Debug($"No redemable actions for this redeem was found [redeem: {e.RewardRedeemed.Redemption.Reward.Title}][redeem id: {e.RewardRedeemed.Redemption.Reward.Id}][transaction: {e.RewardRedeemed.Redemption.Id}]");
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
_logger.Debug($"Found {actions.Count} actions for this Twitch channel point redemption [redeem: {e.RewardRedeemed.Redemption.Reward.Title}][redeem id: {e.RewardRedeemed.Redemption.Reward.Id}][transaction: {e.RewardRedeemed.Redemption.Id}]");
|
||||||
|
|
||||||
var redeemName = e.RewardRedeemed.Redemption.Reward.Title.ToLower().Trim().Replace(" ", "-");
|
foreach (var action in actions)
|
||||||
if (!_configuration.Twitch.Redeems.TryGetValue(redeemName, out RedeemConfiguration? redeem))
|
try
|
||||||
|
{
|
||||||
|
await _redemptionManager.Execute(action, e.RewardRedeemed.Redemption.User.DisplayName);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Error(ex, $"Failed to execute redeeemable action [action: {action.Name}][action type: {action.Type}][redeem: {e.RewardRedeemed.Redemption.Reward.Title}][redeem id: {e.RewardRedeemed.Redemption.Reward.Id}][transaction: {e.RewardRedeemed.Redemption.Id}]");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
_publisher.OnPubSubServiceClosed += async (s, e) =>
|
||||||
|
{
|
||||||
|
_logger.Warning("Twitch PubSub ran into a service close. Attempting to connect again.");
|
||||||
|
//await Task.Delay(Math.Min(3000 + (1 << psConnectionFailures), 120000));
|
||||||
|
var authorized = await Authorize(_broadcasterId);
|
||||||
|
|
||||||
|
var twitchBotData = await _hermesApiClient.FetchTwitchBotToken();
|
||||||
|
if (twitchBotData == null)
|
||||||
|
{
|
||||||
|
Console.WriteLine("The API is down. Contact the owner.");
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (redeem == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Write or append to file if needed.
|
|
||||||
var outputFile = string.IsNullOrWhiteSpace(redeem.OutputFilePath) ? null : redeem.OutputFilePath.Trim();
|
|
||||||
if (outputFile == null)
|
|
||||||
{
|
|
||||||
_logger.Debug($"No output file was provided for redeem [redeem: {e.RewardRedeemed.Redemption.Reward.Title}][id: {e.RewardRedeemed.Redemption.Id}]");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var outputContent = string.IsNullOrWhiteSpace(redeem.OutputContent) ? null : redeem.OutputContent.Trim().Replace("%USER%", e.RewardRedeemed.Redemption.User.DisplayName).Replace("\\n", "\n");
|
|
||||||
if (outputContent == null)
|
|
||||||
{
|
|
||||||
_logger.Warning($"No output content was provided for redeem [redeem: {e.RewardRedeemed.Redemption.Reward.Title}][id: {e.RewardRedeemed.Redemption.Id}]");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (redeem.OutputAppend == true)
|
|
||||||
{
|
|
||||||
File.AppendAllText(outputFile, outputContent + "\n");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
File.WriteAllText(outputFile, outputContent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Play audio file if needed.
|
|
||||||
var audioFile = string.IsNullOrWhiteSpace(redeem.AudioFilePath) ? null : redeem.AudioFilePath.Trim();
|
|
||||||
if (audioFile == null)
|
|
||||||
{
|
|
||||||
_logger.Debug($"No audio file was provided for redeem [redeem: {e.RewardRedeemed.Redemption.Reward.Title}][id: {e.RewardRedeemed.Redemption.Id}]");
|
|
||||||
}
|
|
||||||
else if (!File.Exists(audioFile))
|
|
||||||
{
|
|
||||||
_logger.Warning($"Cannot find audio file [location: {audioFile}] for redeem [redeem: {e.RewardRedeemed.Redemption.Reward.Title}][id: {e.RewardRedeemed.Redemption.Id}]");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
AudioPlaybackEngine.Instance.PlaySound(audioFile);
|
|
||||||
}
|
}
|
||||||
|
await _publisher.ConnectAsync();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user