Added hermes websocket support. Added chat command support. Added selectable voice command via websocket. Added websocket heartbeat management.
This commit is contained in:
parent
b5cc6b5706
commit
d4004d6230
@ -4,22 +4,22 @@ using TwitchChatTTS.OBS.Socket;
|
|||||||
using CommonSocketLibrary.Abstract;
|
using CommonSocketLibrary.Abstract;
|
||||||
using CommonSocketLibrary.Common;
|
using CommonSocketLibrary.Common;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using TwitchChatTTS.Twitch;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using TwitchChatTTS;
|
using TwitchChatTTS;
|
||||||
using TwitchChatTTS.Seven;
|
using TwitchChatTTS.Seven;
|
||||||
|
using TwitchChatTTS.Chat.Commands;
|
||||||
|
|
||||||
|
|
||||||
public class ChatMessageHandler {
|
public class ChatMessageHandler {
|
||||||
private ILogger<ChatMessageHandler> Logger { get; }
|
private ILogger<ChatMessageHandler> _logger { get; }
|
||||||
private Configuration Configuration { get; }
|
private Configuration _configuration { get; }
|
||||||
public EmoteCounter EmoteCounter { get; }
|
public EmoteCounter _emoteCounter { get; }
|
||||||
private EmoteDatabase Emotes { get; }
|
private EmoteDatabase _emotes { get; }
|
||||||
private TTSPlayer Player { get; }
|
private TTSPlayer _player { get; }
|
||||||
private OBSSocketClient? Client { get; }
|
private ChatCommandManager _commands { get; }
|
||||||
private TTSContext Context { get; }
|
private OBSSocketClient? _obsClient { get; }
|
||||||
|
private IServiceProvider _serviceProvider { get; }
|
||||||
|
|
||||||
private Regex? voicesRegex;
|
|
||||||
private Regex sfxRegex;
|
private Regex sfxRegex;
|
||||||
|
|
||||||
|
|
||||||
@ -29,46 +29,53 @@ public class ChatMessageHandler {
|
|||||||
EmoteCounter emoteCounter,
|
EmoteCounter emoteCounter,
|
||||||
EmoteDatabase emotes,
|
EmoteDatabase emotes,
|
||||||
TTSPlayer player,
|
TTSPlayer player,
|
||||||
|
ChatCommandManager commands,
|
||||||
[FromKeyedServices("obs")] SocketClient<WebSocketMessage> client,
|
[FromKeyedServices("obs")] SocketClient<WebSocketMessage> client,
|
||||||
TTSContext context
|
IServiceProvider serviceProvider
|
||||||
) {
|
) {
|
||||||
Logger = logger;
|
_logger = logger;
|
||||||
Configuration = configuration;
|
_configuration = configuration;
|
||||||
EmoteCounter = emoteCounter;
|
_emoteCounter = emoteCounter;
|
||||||
Emotes = emotes;
|
_emotes = emotes;
|
||||||
Player = player;
|
_player = player;
|
||||||
Client = client as OBSSocketClient;
|
_commands = commands;
|
||||||
Context = context;
|
_obsClient = client as OBSSocketClient;
|
||||||
|
_serviceProvider = serviceProvider;
|
||||||
|
|
||||||
voicesRegex = GenerateEnabledVoicesRegex();
|
|
||||||
sfxRegex = new Regex(@"\(([A-Za-z0-9_-]+)\)");
|
sfxRegex = new Regex(@"\(([A-Za-z0-9_-]+)\)");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public MessageResult Handle(OnMessageReceivedArgs e) {
|
public async Task<MessageResult> Handle(OnMessageReceivedArgs e) {
|
||||||
if (Configuration.Twitch?.TtsWhenOffline != true && Client?.Live != true)
|
if (_configuration.Twitch?.TtsWhenOffline != true && _obsClient?.Live == false)
|
||||||
return MessageResult.Blocked;
|
return MessageResult.Blocked;
|
||||||
|
|
||||||
|
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);
|
||||||
// Skip TTS messages
|
|
||||||
if (m.IsVip || m.IsModerator || m.IsBroadcaster) {
|
var blocked = user.ChatterFilters.TryGetValue(m.Username, out TTSUsernameFilter? filter) && filter.Tag == "blacklisted";
|
||||||
if (msg.ToLower().StartsWith("!skip ") || msg.ToLower() == "!skip")
|
|
||||||
return MessageResult.Skip;
|
if (!blocked || m.IsBroadcaster) {
|
||||||
|
try {
|
||||||
if (msg.ToLower().StartsWith("!skipall ") || msg.ToLower() == "!skipall")
|
var commandResult = await _commands.Execute(msg, m);
|
||||||
return MessageResult.SkipAll;
|
if (commandResult != ChatCommandResult.Unknown) {
|
||||||
|
return MessageResult.Command;
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
_logger.LogError(ex, "Failed at executing command.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Context.UsernameFilters.TryGetValue(m.Username, out TTSUsernameFilter? filter) && filter.Tag == "blacklisted") {
|
if (blocked) {
|
||||||
Logger.LogTrace($"Blocked message by {m.Username}: {msg}");
|
_logger.LogTrace($"Blocked message by {m.Username}: {msg}");
|
||||||
return MessageResult.Blocked;
|
return MessageResult.Blocked;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replace filtered words.
|
// Replace filtered words.
|
||||||
if (Context.WordFilters is not null) {
|
if (user.RegexFilters != null) {
|
||||||
foreach (var wf in Context.WordFilters) {
|
foreach (var wf in user.RegexFilters) {
|
||||||
if (wf.Search == null || wf.Replace == null)
|
if (wf.Search == null || wf.Replace == null)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
@ -87,6 +94,7 @@ public class ChatMessageHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Filter highly repetitive words (like emotes) from the message.
|
// Filter highly repetitive words (like emotes) from the message.
|
||||||
|
int totalEmoteUsed = 0;
|
||||||
var emotesUsed = new HashSet<string>();
|
var emotesUsed = new HashSet<string>();
|
||||||
var words = msg.Split(" ");
|
var words = msg.Split(" ");
|
||||||
var wordCounter = new Dictionary<string, int>();
|
var wordCounter = new Dictionary<string, int>();
|
||||||
@ -98,24 +106,31 @@ public class ChatMessageHandler {
|
|||||||
wordCounter.Add(w, 1);
|
wordCounter.Add(w, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
var emoteId = Emotes?.Get(w);
|
var emoteId = _emotes?.Get(w);
|
||||||
if (emoteId != null)
|
if (emoteId == null)
|
||||||
emotesUsed.Add("7tv-" + emoteId);
|
emoteId = m.EmoteSet.Emotes.FirstOrDefault(e => e.Name == w)?.Id;
|
||||||
|
if (emoteId != null) {
|
||||||
|
emotesUsed.Add(emoteId);
|
||||||
|
totalEmoteUsed++;
|
||||||
|
}
|
||||||
|
|
||||||
if (wordCounter[w] <= 4 && (emoteId == null || emotesUsed.Count <= 4))
|
if (wordCounter[w] <= 4 && (emoteId == null || totalEmoteUsed <= 5))
|
||||||
filteredMsg += w + " ";
|
filteredMsg += w + " ";
|
||||||
}
|
}
|
||||||
msg = filteredMsg;
|
msg = filteredMsg;
|
||||||
|
|
||||||
// Adding twitch emotes to the counter.
|
// Adding twitch emotes to the counter.
|
||||||
foreach (var emote in e.ChatMessage.EmoteSet.Emotes)
|
foreach (var emote in e.ChatMessage.EmoteSet.Emotes) {
|
||||||
emotesUsed.Add("twitch-" + emote.Id);
|
_logger.LogTrace("Twitch emote name used: " + emote.Name);
|
||||||
|
emotesUsed.Add(emote.Id);
|
||||||
|
}
|
||||||
|
|
||||||
if (long.TryParse(e.ChatMessage.UserId, out long userId))
|
if (long.TryParse(e.ChatMessage.UserId, out long userId))
|
||||||
EmoteCounter.Add(userId, emotesUsed);
|
_emoteCounter.Add(userId, emotesUsed);
|
||||||
if (emotesUsed.Any())
|
if (emotesUsed.Any())
|
||||||
Logger.LogDebug("Emote counters for user #" + userId + ": " + string.Join(" | ", emotesUsed.Select(e => e + "=" + EmoteCounter.Get(userId, e))));
|
_logger.LogDebug("Emote counters for user #" + userId + ": " + string.Join(" | ", emotesUsed.Select(e => e + "=" + _emoteCounter.Get(userId, e))));
|
||||||
|
|
||||||
|
// Determine the priority of this message
|
||||||
int priority = 0;
|
int priority = 0;
|
||||||
if (m.IsStaff) {
|
if (m.IsStaff) {
|
||||||
priority = int.MinValue;
|
priority = int.MinValue;
|
||||||
@ -130,19 +145,30 @@ public class ChatMessageHandler {
|
|||||||
} else if (m.IsHighlighted) {
|
} else if (m.IsHighlighted) {
|
||||||
priority = -1;
|
priority = -1;
|
||||||
}
|
}
|
||||||
priority = (int) Math.Round(Math.Min(priority, -m.SubscribedMonthCount * (m.Badges.Any(b => b.Key == "subscriber") ? 1.2 : 1)));
|
priority = Math.Min(priority, -m.SubscribedMonthCount * (m.IsSubscriber ? 2 : 1));
|
||||||
|
|
||||||
var matches = voicesRegex?.Matches(msg).ToArray() ?? new Match[0];
|
// Determine voice selected.
|
||||||
int defaultEnd = matches.FirstOrDefault()?.Index ?? msg.Length;
|
string voiceSelected = user.DefaultTTSVoice;
|
||||||
if (defaultEnd > 0) {
|
if (user.VoicesSelected?.ContainsKey(userId) == true) {
|
||||||
HandlePartialMessage(priority, Context.DefaultVoice, msg.Substring(0, defaultEnd).Trim(), e);
|
var voiceId = user.VoicesSelected[userId];
|
||||||
|
if (user.VoicesAvailable.TryGetValue(voiceId, out string? voiceName) && voiceName != null) {
|
||||||
|
voiceSelected = voiceName;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Determine additional voices used
|
||||||
|
var voicesRegex = user.GenerateEnabledVoicesRegex();
|
||||||
|
var matches = voicesRegex?.Matches(msg).ToArray();
|
||||||
|
if (matches == null || matches.FirstOrDefault() == null || matches.FirstOrDefault().Index == 0) {
|
||||||
|
HandlePartialMessage(priority, voiceSelected, msg.Trim(), e);
|
||||||
|
return MessageResult.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
HandlePartialMessage(priority, voiceSelected, msg.Substring(0, matches.FirstOrDefault().Index).Trim(), e);
|
||||||
foreach (Match match in matches) {
|
foreach (Match match in matches) {
|
||||||
var message = match.Groups[2].ToString();
|
var message = match.Groups[2].ToString();
|
||||||
if (string.IsNullOrWhiteSpace(message)) {
|
if (string.IsNullOrWhiteSpace(message))
|
||||||
continue;
|
continue;
|
||||||
}
|
|
||||||
|
|
||||||
var voice = match.Groups[1].ToString();
|
var voice = match.Groups[1].ToString();
|
||||||
voice = voice[0].ToString().ToUpper() + voice.Substring(1).ToLower();
|
voice = voice[0].ToString().ToUpper() + voice.Substring(1).ToLower();
|
||||||
@ -162,8 +188,8 @@ public class ChatMessageHandler {
|
|||||||
var badgesString = string.Join(", ", e.ChatMessage.Badges.Select(b => b.Key + " = " + b.Value));
|
var badgesString = string.Join(", ", e.ChatMessage.Badges.Select(b => b.Key + " = " + b.Value));
|
||||||
|
|
||||||
if (parts.Length == 1) {
|
if (parts.Length == 1) {
|
||||||
Logger.LogInformation($"Voice: {voice}; Priority: {priority}; Message: {message}; Month: {m.SubscribedMonthCount}; {badgesString}");
|
_logger.LogInformation($"Voice: {voice}; Priority: {priority}; Message: {message}; Month: {m.SubscribedMonthCount}; {badgesString}");
|
||||||
Player.Add(new TTSMessage() {
|
_player.Add(new TTSMessage() {
|
||||||
Voice = voice,
|
Voice = voice,
|
||||||
Message = message,
|
Message = message,
|
||||||
Moderator = m.IsModerator,
|
Moderator = m.IsModerator,
|
||||||
@ -189,8 +215,8 @@ public class ChatMessageHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(parts[i * 2])) {
|
if (!string.IsNullOrWhiteSpace(parts[i * 2])) {
|
||||||
Logger.LogInformation($"Voice: {voice}; Priority: {priority}; Message: {parts[i * 2]}; Month: {m.SubscribedMonthCount}; {badgesString}");
|
_logger.LogInformation($"Username: {m.Username}; User ID: {m.UserId}; Voice: {voice}; Priority: {priority}; Message: {parts[i * 2]}; Month: {m.SubscribedMonthCount}; {badgesString}");
|
||||||
Player.Add(new TTSMessage() {
|
_player.Add(new TTSMessage() {
|
||||||
Voice = voice,
|
Voice = voice,
|
||||||
Message = parts[i * 2],
|
Message = parts[i * 2],
|
||||||
Moderator = m.IsModerator,
|
Moderator = m.IsModerator,
|
||||||
@ -202,8 +228,8 @@ public class ChatMessageHandler {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.LogInformation($"Voice: {voice}; Priority: {priority}; SFX: {sfxName}; Month: {m.SubscribedMonthCount}; {badgesString}");
|
_logger.LogInformation($"Username: {m.Username}; User ID: {m.UserId}; Voice: {voice}; Priority: {priority}; SFX: {sfxName}; Month: {m.SubscribedMonthCount}; {badgesString}");
|
||||||
Player.Add(new TTSMessage() {
|
_player.Add(new TTSMessage() {
|
||||||
Voice = voice,
|
Voice = voice,
|
||||||
Message = sfxName,
|
Message = sfxName,
|
||||||
File = $"sfx/{sfxName}.mp3",
|
File = $"sfx/{sfxName}.mp3",
|
||||||
@ -217,8 +243,8 @@ public class ChatMessageHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(parts.Last())) {
|
if (!string.IsNullOrWhiteSpace(parts.Last())) {
|
||||||
Logger.LogInformation($"Voice: {voice}; Priority: {priority}; Message: {parts.Last()}; Month: {m.SubscribedMonthCount}; {badgesString}");
|
_logger.LogInformation($"Username: {m.Username}; User ID: {m.UserId}; Voice: {voice}; Priority: {priority}; Message: {parts.Last()}; Month: {m.SubscribedMonthCount}; {badgesString}");
|
||||||
Player.Add(new TTSMessage() {
|
_player.Add(new TTSMessage() {
|
||||||
Voice = voice,
|
Voice = voice,
|
||||||
Message = parts.Last(),
|
Message = parts.Last(),
|
||||||
Moderator = m.IsModerator,
|
Moderator = m.IsModerator,
|
||||||
@ -230,13 +256,4 @@ public class ChatMessageHandler {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Regex? GenerateEnabledVoicesRegex() {
|
|
||||||
if (Context.EnabledVoices == null || Context.EnabledVoices.Count() <= 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var enabledVoicesString = string.Join("|", Context.EnabledVoices.Select(v => v.Label));
|
|
||||||
return new Regex($@"\b({enabledVoicesString})\:(.*?)(?=\Z|\b(?:{enabledVoicesString})\:)", RegexOptions.IgnoreCase);
|
|
||||||
}
|
|
||||||
}
|
}
|
54
Chat/Commands/AddTTSVoiceCommand.cs
Normal file
54
Chat/Commands/AddTTSVoiceCommand.cs
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
using CommonSocketLibrary.Abstract;
|
||||||
|
using CommonSocketLibrary.Common;
|
||||||
|
using HermesSocketLibrary.Socket.Data;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using TwitchChatTTS.Chat.Commands.Parameters;
|
||||||
|
using TwitchLib.Client.Models;
|
||||||
|
|
||||||
|
namespace TwitchChatTTS.Chat.Commands
|
||||||
|
{
|
||||||
|
public class AddTTSVoiceCommand : ChatCommand
|
||||||
|
{
|
||||||
|
private IServiceProvider _serviceProvider;
|
||||||
|
private ILogger<AddTTSVoiceCommand> _logger;
|
||||||
|
|
||||||
|
public AddTTSVoiceCommand(
|
||||||
|
[FromKeyedServices("parameter-unvalidated")] ChatCommandParameter ttsVoiceParameter,
|
||||||
|
IServiceProvider serviceProvider,
|
||||||
|
ILogger<AddTTSVoiceCommand> logger
|
||||||
|
) : base("addttsvoice", "Select a TTS voice as the default for that user.") {
|
||||||
|
_serviceProvider = serviceProvider;
|
||||||
|
_logger = logger;
|
||||||
|
|
||||||
|
AddParameter(ttsVoiceParameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<bool> CheckPermissions(ChatMessage message, long broadcasterId)
|
||||||
|
{
|
||||||
|
return message.IsModerator || message.IsBroadcaster || message.UserId == "126224566";
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task Execute(IList<string> args, ChatMessage message, long broadcasterId)
|
||||||
|
{
|
||||||
|
var client = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("hermes");
|
||||||
|
if (client == null)
|
||||||
|
return;
|
||||||
|
var context = _serviceProvider.GetRequiredService<User>();
|
||||||
|
if (context == null || context.VoicesAvailable == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var voiceName = args.First();
|
||||||
|
var voiceNameLower = voiceName.ToLower();
|
||||||
|
var exists = context.VoicesAvailable.Any(v => v.Value.ToLower() == voiceNameLower);
|
||||||
|
if (exists)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await client.Send(3, new RequestMessage() {
|
||||||
|
Type = "create_tts_voice",
|
||||||
|
Data = new Dictionary<string, string>() { { "@voice", voiceName } }
|
||||||
|
});
|
||||||
|
_logger.LogInformation($"Added a new TTS voice by {message.Username} (id: {message.UserId}): {voiceName}.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
27
Chat/Commands/ChatCommand.cs
Normal file
27
Chat/Commands/ChatCommand.cs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
using TwitchChatTTS.Chat.Commands.Parameters;
|
||||||
|
using TwitchLib.Client.Models;
|
||||||
|
|
||||||
|
namespace TwitchChatTTS.Chat.Commands
|
||||||
|
{
|
||||||
|
public abstract class ChatCommand
|
||||||
|
{
|
||||||
|
public string Name { get; }
|
||||||
|
public string Description { get; }
|
||||||
|
public IList<ChatCommandParameter> Parameters { get => _parameters.AsReadOnly(); }
|
||||||
|
private IList<ChatCommandParameter> _parameters;
|
||||||
|
|
||||||
|
public ChatCommand(string name, string description) {
|
||||||
|
Name = name;
|
||||||
|
Description = description;
|
||||||
|
_parameters = new List<ChatCommandParameter>();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void AddParameter(ChatCommandParameter parameter) {
|
||||||
|
if (parameter != null)
|
||||||
|
_parameters.Add(parameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract Task<bool> CheckPermissions(ChatMessage message, long broadcasterId);
|
||||||
|
public abstract Task Execute(IList<string> args, ChatMessage message, long broadcasterId);
|
||||||
|
}
|
||||||
|
}
|
100
Chat/Commands/ChatCommandManager.cs
Normal file
100
Chat/Commands/ChatCommandManager.cs
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using TwitchLib.Client.Models;
|
||||||
|
|
||||||
|
namespace TwitchChatTTS.Chat.Commands
|
||||||
|
{
|
||||||
|
public class ChatCommandManager
|
||||||
|
{
|
||||||
|
private IDictionary<string, ChatCommand> _commands;
|
||||||
|
private TwitchBotToken _token;
|
||||||
|
private IServiceProvider _serviceProvider;
|
||||||
|
private ILogger<ChatCommandManager> _logger;
|
||||||
|
private string CommandStartSign { get; } = "!";
|
||||||
|
|
||||||
|
|
||||||
|
public ChatCommandManager(TwitchBotToken token, IServiceProvider serviceProvider, ILogger<ChatCommandManager> logger) {
|
||||||
|
_token = token;
|
||||||
|
_serviceProvider = serviceProvider;
|
||||||
|
_logger = logger;
|
||||||
|
|
||||||
|
_commands = new Dictionary<string, ChatCommand>();
|
||||||
|
GenerateCommands();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Add(ChatCommand command) {
|
||||||
|
_commands.Add(command.Name.ToLower(), command);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GenerateCommands() {
|
||||||
|
var basetype = typeof(ChatCommand);
|
||||||
|
var assembly = GetType().Assembly;
|
||||||
|
var types = assembly.GetTypes().Where(t => t.IsClass && !t.IsAbstract && basetype.IsAssignableFrom(t) && t.AssemblyQualifiedName?.Contains(".Chat.") == true);
|
||||||
|
|
||||||
|
foreach (var type in types) {
|
||||||
|
var key = "command-" + type.Name.Replace("Commands", "Comm#ands")
|
||||||
|
.Replace("Command", "")
|
||||||
|
.Replace("Comm#ands", "Commands")
|
||||||
|
.ToLower();
|
||||||
|
|
||||||
|
var command = _serviceProvider.GetKeyedService<ChatCommand>(key);
|
||||||
|
if (command == null) {
|
||||||
|
_logger.LogError("Failed to add command: " + type.AssemblyQualifiedName);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogDebug($"Added command {type.AssemblyQualifiedName}.");
|
||||||
|
Add(command);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ChatCommandResult> Execute(string arg, ChatMessage message) {
|
||||||
|
if (_token.BroadcasterId == null)
|
||||||
|
return ChatCommandResult.Unknown;
|
||||||
|
if (string.IsNullOrWhiteSpace(arg))
|
||||||
|
return ChatCommandResult.Unknown;
|
||||||
|
|
||||||
|
arg = arg.Trim();
|
||||||
|
|
||||||
|
if (!arg.StartsWith(CommandStartSign))
|
||||||
|
return ChatCommandResult.Unknown;
|
||||||
|
|
||||||
|
string[] parts = arg.Split(" ");
|
||||||
|
string com = parts.First().Substring(CommandStartSign.Length).ToLower();
|
||||||
|
string[] args = parts.Skip(1).ToArray();
|
||||||
|
long broadcasterId = long.Parse(_token.BroadcasterId);
|
||||||
|
|
||||||
|
if (!_commands.TryGetValue(com, out ChatCommand? command) || command == null) {
|
||||||
|
_logger.LogDebug($"Failed to find command named '{com}'.");
|
||||||
|
return ChatCommandResult.Missing;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!await command.CheckPermissions(message, broadcasterId)) {
|
||||||
|
_logger.LogWarning($"Chatter is missing permission to execute command named '{com}'.");
|
||||||
|
return ChatCommandResult.Permission;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command.Parameters.Count(p => !p.Optional) > args.Length) {
|
||||||
|
_logger.LogWarning($"Command syntax issue when executing command named '{com}' with the following args: {string.Join(" ", args)}");
|
||||||
|
return ChatCommandResult.Syntax;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < Math.Min(args.Length, command.Parameters.Count); i++) {
|
||||||
|
if (!command.Parameters[i].Validate(args[i])) {
|
||||||
|
_logger.LogWarning($"Commmand '{com}' failed because of the #{i + 1} argument. Invalid value: {args[i]}");
|
||||||
|
return ChatCommandResult.Syntax;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await command.Execute(args, message, broadcasterId);
|
||||||
|
} catch (Exception e) {
|
||||||
|
_logger.LogError(e, $"Command '{arg}' failed.");
|
||||||
|
return ChatCommandResult.Fail;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation($"Execute the {com} command with the following args: " + string.Join(" ", args));
|
||||||
|
return ChatCommandResult.Success;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
12
Chat/Commands/ChatCommandResult.cs
Normal file
12
Chat/Commands/ChatCommandResult.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
namespace TwitchChatTTS.Chat.Commands
|
||||||
|
{
|
||||||
|
public enum ChatCommandResult
|
||||||
|
{
|
||||||
|
Unknown = 0,
|
||||||
|
Missing = 1,
|
||||||
|
Success = 2,
|
||||||
|
Permission = 3,
|
||||||
|
Syntax = 4,
|
||||||
|
Fail = 5
|
||||||
|
}
|
||||||
|
}
|
17
Chat/Commands/Parameters/ChatCommandParameter.cs
Normal file
17
Chat/Commands/Parameters/ChatCommandParameter.cs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
namespace TwitchChatTTS.Chat.Commands.Parameters
|
||||||
|
{
|
||||||
|
public abstract class ChatCommandParameter
|
||||||
|
{
|
||||||
|
public string Name { get; }
|
||||||
|
public string Description { get; }
|
||||||
|
public bool Optional { get; }
|
||||||
|
|
||||||
|
public ChatCommandParameter(string name, string description, bool optional = false) {
|
||||||
|
Name = name;
|
||||||
|
Description = description;
|
||||||
|
Optional = optional;
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract bool Validate(string value);
|
||||||
|
}
|
||||||
|
}
|
23
Chat/Commands/Parameters/TTSVoiceNameParameter.cs
Normal file
23
Chat/Commands/Parameters/TTSVoiceNameParameter.cs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
namespace TwitchChatTTS.Chat.Commands.Parameters
|
||||||
|
{
|
||||||
|
public class TTSVoiceNameParameter : ChatCommandParameter
|
||||||
|
{
|
||||||
|
private IServiceProvider _serviceProvider;
|
||||||
|
|
||||||
|
public TTSVoiceNameParameter(IServiceProvider serviceProvider, bool optional = false) : base("TTS Voice Name", "Name of a TTS voice", optional)
|
||||||
|
{
|
||||||
|
_serviceProvider = serviceProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Validate(string value)
|
||||||
|
{
|
||||||
|
var user = _serviceProvider.GetRequiredService<User>();
|
||||||
|
if (user.VoicesAvailable == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
value = value.ToLower();
|
||||||
|
return user.VoicesAvailable.Any(e => e.Value.ToLower() == value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
14
Chat/Commands/Parameters/UnvalidatedParameter.cs
Normal file
14
Chat/Commands/Parameters/UnvalidatedParameter.cs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
namespace TwitchChatTTS.Chat.Commands.Parameters
|
||||||
|
{
|
||||||
|
public class UnvalidatedParameter : ChatCommandParameter
|
||||||
|
{
|
||||||
|
public UnvalidatedParameter(bool optional = false) : base("TTS Voice Name", "Name of a TTS voice", optional)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Validate(string value)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
54
Chat/Commands/RemoveTTSVoiceCommand.cs
Normal file
54
Chat/Commands/RemoveTTSVoiceCommand.cs
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
using CommonSocketLibrary.Abstract;
|
||||||
|
using CommonSocketLibrary.Common;
|
||||||
|
using HermesSocketLibrary.Socket.Data;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using TwitchChatTTS.Chat.Commands.Parameters;
|
||||||
|
using TwitchLib.Client.Models;
|
||||||
|
|
||||||
|
namespace TwitchChatTTS.Chat.Commands
|
||||||
|
{
|
||||||
|
public class RemoveTTSVoiceCommand : ChatCommand
|
||||||
|
{
|
||||||
|
private IServiceProvider _serviceProvider;
|
||||||
|
private ILogger<RemoveTTSVoiceCommand> _logger;
|
||||||
|
|
||||||
|
public RemoveTTSVoiceCommand(
|
||||||
|
[FromKeyedServices("parameter-unvalidated")] ChatCommandParameter ttsVoiceParameter,
|
||||||
|
IServiceProvider serviceProvider,
|
||||||
|
ILogger<RemoveTTSVoiceCommand> logger
|
||||||
|
) : base("removettsvoice", "Select a TTS voice as the default for that user.") {
|
||||||
|
_serviceProvider = serviceProvider;
|
||||||
|
_logger = logger;
|
||||||
|
|
||||||
|
AddParameter(ttsVoiceParameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<bool> CheckPermissions(ChatMessage message, long broadcasterId)
|
||||||
|
{
|
||||||
|
return message.IsModerator || message.IsBroadcaster || message.UserId == "126224566";
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task Execute(IList<string> args, ChatMessage message, long broadcasterId)
|
||||||
|
{
|
||||||
|
var client = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("hermes");
|
||||||
|
if (client == null)
|
||||||
|
return;
|
||||||
|
var context = _serviceProvider.GetRequiredService<User>();
|
||||||
|
if (context == null || context.VoicesAvailable == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var voiceName = args.First().ToLower();
|
||||||
|
var exists = context.VoicesAvailable.Any(v => v.Value.ToLower() == voiceName);
|
||||||
|
if (!exists)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var voiceId = context.VoicesAvailable.FirstOrDefault(v => v.Value.ToLower() == voiceName).Key;
|
||||||
|
await client.Send(3, new RequestMessage() {
|
||||||
|
Type = "delete_tts_voice",
|
||||||
|
Data = new Dictionary<string, string>() { { "@voice", voiceId } }
|
||||||
|
});
|
||||||
|
_logger.LogInformation($"Deleted a TTS voice by {message.Username} (id: {message.UserId}): {voiceName}.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
37
Chat/Commands/SkipAllCommand.cs
Normal file
37
Chat/Commands/SkipAllCommand.cs
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using TwitchLib.Client.Models;
|
||||||
|
|
||||||
|
namespace TwitchChatTTS.Chat.Commands
|
||||||
|
{
|
||||||
|
public class SkipAllCommand : ChatCommand
|
||||||
|
{
|
||||||
|
private IServiceProvider _serviceProvider;
|
||||||
|
private ILogger<SkipAllCommand> _logger;
|
||||||
|
|
||||||
|
public SkipAllCommand(IServiceProvider serviceProvider, ILogger<SkipAllCommand> logger)
|
||||||
|
: base("skipall", "Skips all text to speech messages in queue and playing.") {
|
||||||
|
_serviceProvider = serviceProvider;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<bool> CheckPermissions(ChatMessage message, long broadcasterId)
|
||||||
|
{
|
||||||
|
return message.IsModerator || message.IsVip || message.IsBroadcaster;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task Execute(IList<string> args, ChatMessage message, long broadcasterId)
|
||||||
|
{
|
||||||
|
var player = _serviceProvider.GetRequiredService<TTSPlayer>();
|
||||||
|
player.RemoveAll();
|
||||||
|
|
||||||
|
if (player.Playing == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
AudioPlaybackEngine.Instance.RemoveMixerInput(player.Playing);
|
||||||
|
player.Playing = null;
|
||||||
|
|
||||||
|
_logger.LogInformation("Skipped all queued and playing tts.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
35
Chat/Commands/SkipCommand.cs
Normal file
35
Chat/Commands/SkipCommand.cs
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using TwitchLib.Client.Models;
|
||||||
|
|
||||||
|
namespace TwitchChatTTS.Chat.Commands
|
||||||
|
{
|
||||||
|
public class SkipCommand : ChatCommand
|
||||||
|
{
|
||||||
|
private IServiceProvider _serviceProvider;
|
||||||
|
private ILogger<SkipCommand> _logger;
|
||||||
|
|
||||||
|
public SkipCommand(IServiceProvider serviceProvider, ILogger<SkipCommand> logger)
|
||||||
|
: base("skip", "Skips the current text to speech message.") {
|
||||||
|
_serviceProvider = serviceProvider;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<bool> CheckPermissions(ChatMessage message, long broadcasterId)
|
||||||
|
{
|
||||||
|
return message.IsModerator || message.IsVip || message.IsBroadcaster;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task Execute(IList<string> args, ChatMessage message, long broadcasterId)
|
||||||
|
{
|
||||||
|
var player = _serviceProvider.GetRequiredService<TTSPlayer>();
|
||||||
|
if (player.Playing == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
AudioPlaybackEngine.Instance.RemoveMixerInput(player.Playing);
|
||||||
|
player.Playing = null;
|
||||||
|
|
||||||
|
_logger.LogInformation("Skipped current tts.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
60
Chat/Commands/VoiceCommand.cs
Normal file
60
Chat/Commands/VoiceCommand.cs
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
using CommonSocketLibrary.Abstract;
|
||||||
|
using CommonSocketLibrary.Common;
|
||||||
|
using HermesSocketLibrary.Socket.Data;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using TwitchChatTTS.Chat.Commands.Parameters;
|
||||||
|
using TwitchLib.Client.Models;
|
||||||
|
|
||||||
|
namespace TwitchChatTTS.Chat.Commands
|
||||||
|
{
|
||||||
|
public class VoiceCommand : ChatCommand
|
||||||
|
{
|
||||||
|
private IServiceProvider _serviceProvider;
|
||||||
|
private ILogger<VoiceCommand> _logger;
|
||||||
|
|
||||||
|
public VoiceCommand(
|
||||||
|
[FromKeyedServices("parameter-ttsvoicename")] ChatCommandParameter ttsVoiceParameter,
|
||||||
|
IServiceProvider serviceProvider,
|
||||||
|
ILogger<VoiceCommand> logger
|
||||||
|
) : base("voice", "Select a TTS voice as the default for that user.") {
|
||||||
|
_serviceProvider = serviceProvider;
|
||||||
|
_logger = logger;
|
||||||
|
|
||||||
|
AddParameter(ttsVoiceParameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<bool> CheckPermissions(ChatMessage message, long broadcasterId)
|
||||||
|
{
|
||||||
|
return message.IsModerator || message.IsBroadcaster || message.IsSubscriber || message.Bits >= 100 || message.UserId == "126224566";
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task Execute(IList<string> args, ChatMessage message, long broadcasterId)
|
||||||
|
{
|
||||||
|
var client = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("hermes");
|
||||||
|
if (client == null)
|
||||||
|
return;
|
||||||
|
var context = _serviceProvider.GetRequiredService<User>();
|
||||||
|
if (context == null || context.VoicesSelected == null || context.VoicesAvailable == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
long chatterId = long.Parse(message.UserId);
|
||||||
|
var voiceName = args.First().ToLower();
|
||||||
|
var voice = context.VoicesAvailable.First(v => v.Value.ToLower() == voiceName);
|
||||||
|
|
||||||
|
if (context.VoicesSelected.ContainsKey(chatterId)) {
|
||||||
|
await client.Send(3, new RequestMessage() {
|
||||||
|
Type = "update_tts_user",
|
||||||
|
Data = new Dictionary<string, string>() { { "@user", message.UserId }, { "@broadcaster", broadcasterId.ToString() }, { "@voice", voice.Key } }
|
||||||
|
});
|
||||||
|
_logger.LogInformation($"Updated {message.Username}'s (id: {message.UserId}) tts voice to {voice.Value} (id: {voice.Key}).");
|
||||||
|
} else {
|
||||||
|
await client.Send(3, new RequestMessage() {
|
||||||
|
Type = "create_tts_user",
|
||||||
|
Data = new Dictionary<string, string>() { { "@user", message.UserId }, { "@broadcaster", broadcasterId.ToString() }, { "@voice", voice.Key } }
|
||||||
|
});
|
||||||
|
_logger.LogInformation($"Added {message.Username}'s (id: {message.UserId}) tts voice as {voice.Value} (id: {voice.Key}).");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
public enum MessageResult {
|
public enum MessageResult {
|
||||||
Skip = 1,
|
None = 0,
|
||||||
SkipAll = 2,
|
NotReady = 1,
|
||||||
Blocked = 3,
|
Blocked = 2,
|
||||||
None = 0
|
Command = 3
|
||||||
}
|
}
|
@ -24,7 +24,7 @@ public class AudioPlaybackEngine : IDisposable
|
|||||||
|
|
||||||
private ISampleProvider ConvertToRightChannelCount(ISampleProvider? input)
|
private ISampleProvider ConvertToRightChannelCount(ISampleProvider? input)
|
||||||
{
|
{
|
||||||
if (input is null)
|
if (input == null)
|
||||||
throw new NullReferenceException(nameof(input));
|
throw new NullReferenceException(nameof(input));
|
||||||
|
|
||||||
if (input.WaveFormat.Channels == mixer.WaveFormat.Channels)
|
if (input.WaveFormat.Channels == mixer.WaveFormat.Channels)
|
||||||
|
@ -6,6 +6,8 @@ public class TTSPlayer {
|
|||||||
private Mutex _mutex;
|
private Mutex _mutex;
|
||||||
private Mutex _mutex2;
|
private Mutex _mutex2;
|
||||||
|
|
||||||
|
public ISampleProvider? Playing { get; set; }
|
||||||
|
|
||||||
public TTSPlayer() {
|
public TTSPlayer() {
|
||||||
_messages = new PriorityQueue<TTSMessage, int>();
|
_messages = new PriorityQueue<TTSMessage, int>();
|
||||||
_buffer = new PriorityQueue<TTSMessage, int>();
|
_buffer = new PriorityQueue<TTSMessage, int>();
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
using TwitchChatTTS.Seven.Socket.Context;
|
|
||||||
|
|
||||||
namespace TwitchChatTTS
|
namespace TwitchChatTTS
|
||||||
{
|
{
|
||||||
public class Configuration
|
public class Configuration
|
||||||
@ -39,10 +37,7 @@ namespace TwitchChatTTS
|
|||||||
}
|
}
|
||||||
|
|
||||||
public class SevenConfiguration {
|
public class SevenConfiguration {
|
||||||
public string? Protocol;
|
public string? UserId;
|
||||||
public string? Url;
|
|
||||||
|
|
||||||
public IEnumerable<SevenSubscriptionConfiguration>? InitialSubscriptions;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -4,18 +4,10 @@ using TwitchChatTTS.Hermes;
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
public class HermesClient {
|
public class HermesClient {
|
||||||
private Account? account;
|
|
||||||
private WebClientWrap _web;
|
private WebClientWrap _web;
|
||||||
private Configuration Configuration { get; }
|
|
||||||
|
|
||||||
public string? Id { get => account?.Id; }
|
|
||||||
public string? Username { get => account?.Username; }
|
|
||||||
|
|
||||||
|
|
||||||
public HermesClient(Configuration configuration) {
|
public HermesClient(Configuration configuration) {
|
||||||
Configuration = configuration;
|
if (string.IsNullOrWhiteSpace(configuration.Hermes?.Token)) {
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(Configuration.Hermes?.Token)) {
|
|
||||||
throw new Exception("Ensure you have written your API key in \".token\" file, in the same folder as this application.");
|
throw new Exception("Ensure you have written your API key in \".token\" file, in the same folder as this application.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -23,54 +15,52 @@ public class HermesClient {
|
|||||||
PropertyNameCaseInsensitive = false,
|
PropertyNameCaseInsensitive = false,
|
||||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
|
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
|
||||||
});
|
});
|
||||||
_web.AddHeader("x-api-key", Configuration.Hermes.Token);
|
_web.AddHeader("x-api-key", configuration.Hermes.Token);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task FetchHermesAccountDetails() {
|
public async Task<Account> FetchHermesAccountDetails() {
|
||||||
account = await _web.GetJson<Account>("https://hermes.goblincaves.com/api/account");
|
var account = await _web.GetJson<Account>("https://hermes.goblincaves.com/api/account");
|
||||||
|
if (account == null || account.Id == null || account.Username == null)
|
||||||
|
throw new NullReferenceException("Invalid value found while fetching for hermes account data.");
|
||||||
|
return account;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<TwitchBotToken> FetchTwitchBotToken() {
|
public async Task<TwitchBotToken> FetchTwitchBotToken() {
|
||||||
var token = await _web.GetJson<TwitchBotToken>("https://hermes.goblincaves.com/api/token/bot");
|
var token = await _web.GetJson<TwitchBotToken>("https://hermes.goblincaves.com/api/token/bot");
|
||||||
if (token == null) {
|
if (token == null || token.ClientId == null || token.AccessToken == null || token.RefreshToken == null || token.ClientSecret == null)
|
||||||
throw new Exception("Failed to fetch Twitch API token from Hermes.");
|
throw new Exception("Failed to fetch Twitch API token from Hermes.");
|
||||||
}
|
|
||||||
|
|
||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<TTSUsernameFilter>> FetchTTSUsernameFilters() {
|
public async Task<IEnumerable<TTSUsernameFilter>> FetchTTSUsernameFilters() {
|
||||||
var filters = await _web.GetJson<IEnumerable<TTSUsernameFilter>>("https://hermes.goblincaves.com/api/settings/tts/filter/users");
|
var filters = await _web.GetJson<IEnumerable<TTSUsernameFilter>>("https://hermes.goblincaves.com/api/settings/tts/filter/users");
|
||||||
if (filters == null) {
|
if (filters == null)
|
||||||
throw new Exception("Failed to fetch TTS username filters from Hermes.");
|
throw new Exception("Failed to fetch TTS username filters from Hermes.");
|
||||||
}
|
|
||||||
|
|
||||||
return filters;
|
return filters;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<string?> FetchTTSDefaultVoice() {
|
public async Task<string> FetchTTSDefaultVoice() {
|
||||||
var data = await _web.GetJson<TTSVoice>("https://hermes.goblincaves.com/api/settings/tts/default");
|
var data = await _web.GetJson<TTSVoice>("https://hermes.goblincaves.com/api/settings/tts/default");
|
||||||
if (data == null) {
|
if (data == null)
|
||||||
throw new Exception("Failed to fetch TTS default voice from Hermes.");
|
throw new Exception("Failed to fetch TTS default voice from Hermes.");
|
||||||
}
|
|
||||||
|
|
||||||
return data.Label;
|
return data.Label;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<TTSVoice>> FetchTTSEnabledVoices() {
|
public async Task<IEnumerable<TTSVoice>> FetchTTSEnabledVoices() {
|
||||||
var voices = await _web.GetJson<IEnumerable<TTSVoice>>("https://hermes.goblincaves.com/api/settings/tts");
|
var voices = await _web.GetJson<IEnumerable<TTSVoice>>("https://hermes.goblincaves.com/api/settings/tts");
|
||||||
if (voices == null) {
|
if (voices == null)
|
||||||
throw new Exception("Failed to fetch TTS enabled voices from Hermes.");
|
throw new Exception("Failed to fetch TTS enabled voices from Hermes.");
|
||||||
}
|
|
||||||
|
|
||||||
return voices;
|
return voices;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<TTSWordFilter>> FetchTTSWordFilters() {
|
public async Task<IEnumerable<TTSWordFilter>> FetchTTSWordFilters() {
|
||||||
var filters = await _web.GetJson<IEnumerable<TTSWordFilter>>("https://hermes.goblincaves.com/api/settings/tts/filter/words");
|
var filters = await _web.GetJson<IEnumerable<TTSWordFilter>>("https://hermes.goblincaves.com/api/settings/tts/filter/words");
|
||||||
if (filters == null) {
|
if (filters == null)
|
||||||
throw new Exception("Failed to fetch TTS word filters from Hermes.");
|
throw new Exception("Failed to fetch TTS word filters from Hermes.");
|
||||||
}
|
|
||||||
|
|
||||||
return filters;
|
return filters;
|
||||||
}
|
}
|
||||||
|
35
Hermes/Socket/Handlers/HeartbeatHandler.cs
Normal file
35
Hermes/Socket/Handlers/HeartbeatHandler.cs
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
using CommonSocketLibrary.Abstract;
|
||||||
|
using CommonSocketLibrary.Common;
|
||||||
|
using HermesSocketLibrary.Socket.Data;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace TwitchChatTTS.Hermes.Socket.Handlers
|
||||||
|
{
|
||||||
|
public class HeartbeatHandler : IWebSocketHandler
|
||||||
|
{
|
||||||
|
private ILogger _logger { get; }
|
||||||
|
public int OperationCode { get; set; } = 0;
|
||||||
|
|
||||||
|
public HeartbeatHandler(ILogger<HeartbeatHandler> logger) {
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data message)
|
||||||
|
{
|
||||||
|
if (message is not HeartbeatMessage obj || obj == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (sender is not HermesSocketClient client) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogTrace("Received heartbeat.");
|
||||||
|
|
||||||
|
client.LastHeartbeat = DateTime.UtcNow;
|
||||||
|
|
||||||
|
await sender.Send(0, new HeartbeatMessage() {
|
||||||
|
DateTime = DateTime.UtcNow
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
34
Hermes/Socket/Handlers/LoginAckHandler.cs
Normal file
34
Hermes/Socket/Handlers/LoginAckHandler.cs
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
using CommonSocketLibrary.Abstract;
|
||||||
|
using CommonSocketLibrary.Common;
|
||||||
|
using HermesSocketLibrary.Socket.Data;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace TwitchChatTTS.Hermes.Socket.Handlers
|
||||||
|
{
|
||||||
|
public class LoginAckHandler : IWebSocketHandler
|
||||||
|
{
|
||||||
|
private ILogger _logger { get; }
|
||||||
|
public int OperationCode { get; set; } = 2;
|
||||||
|
|
||||||
|
public LoginAckHandler(ILogger<LoginAckHandler> logger) {
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data message)
|
||||||
|
{
|
||||||
|
if (message is not LoginAckMessage obj || obj == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (sender is not HermesSocketClient client) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj.AnotherClient) {
|
||||||
|
_logger.LogWarning("Another client has connected to the same account.");
|
||||||
|
} else {
|
||||||
|
client.UserId = obj.UserId;
|
||||||
|
_logger.LogInformation($"Logged in as {client.UserId}.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
106
Hermes/Socket/Handlers/RequestAckHandler.cs
Normal file
106
Hermes/Socket/Handlers/RequestAckHandler.cs
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Text.Json;
|
||||||
|
using CommonSocketLibrary.Abstract;
|
||||||
|
using CommonSocketLibrary.Common;
|
||||||
|
using HermesSocketLibrary.Requests.Messages;
|
||||||
|
using HermesSocketLibrary.Socket.Data;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace TwitchChatTTS.Hermes.Socket.Handlers
|
||||||
|
{
|
||||||
|
public class RequestAckHandler : IWebSocketHandler
|
||||||
|
{
|
||||||
|
private readonly IServiceProvider _serviceProvider;
|
||||||
|
private readonly JsonSerializerOptions _options;
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
public int OperationCode { get; set; } = 4;
|
||||||
|
|
||||||
|
public RequestAckHandler(IServiceProvider serviceProvider, JsonSerializerOptions options, ILogger<RequestAckHandler> logger) {
|
||||||
|
_serviceProvider = serviceProvider;
|
||||||
|
_options = options;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data message)
|
||||||
|
{
|
||||||
|
if (message is not RequestAckMessage obj || obj == null)
|
||||||
|
return;
|
||||||
|
if (obj.Request == null)
|
||||||
|
return;
|
||||||
|
var context = _serviceProvider.GetRequiredService<User>();
|
||||||
|
if (context == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (obj.Request.Type == "get_tts_voices") {
|
||||||
|
_logger.LogDebug("Updating all available voices.");
|
||||||
|
var voices = JsonSerializer.Deserialize<IEnumerable<VoiceDetails>>(obj.Data.ToString(), _options);
|
||||||
|
if (voices == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
context.VoicesAvailable = voices.ToDictionary(e => e.Id, e => e.Name);
|
||||||
|
_logger.LogInformation("Updated all available voices.");
|
||||||
|
} else if (obj.Request.Type == "create_tts_user") {
|
||||||
|
_logger.LogDebug("Creating new tts voice.");
|
||||||
|
if (!long.TryParse(obj.Request.Data["@user"], out long userId))
|
||||||
|
return;
|
||||||
|
string broadcasterId = obj.Request.Data["@broadcaster"].ToString();
|
||||||
|
// TODO: validate broadcaster id.
|
||||||
|
string voice = obj.Request.Data["@voice"].ToString();
|
||||||
|
|
||||||
|
context.VoicesSelected.Add(userId, voice);
|
||||||
|
_logger.LogInformation("Created new tts user.");
|
||||||
|
} else if (obj.Request.Type == "update_tts_user") {
|
||||||
|
_logger.LogDebug("Updating user's voice");
|
||||||
|
if (!long.TryParse(obj.Request.Data["@user"], out long userId))
|
||||||
|
return;
|
||||||
|
string broadcasterId = obj.Request.Data["@broadcaster"].ToString();
|
||||||
|
string voice = obj.Request.Data["@voice"].ToString();
|
||||||
|
|
||||||
|
context.VoicesSelected[userId] = voice;
|
||||||
|
_logger.LogInformation($"Updated user's voice to {voice}.");
|
||||||
|
} else if (obj.Request.Type == "create_tts_voice") {
|
||||||
|
_logger.LogDebug("Creating new tts voice.");
|
||||||
|
string? voice = obj.Request.Data["@voice"];
|
||||||
|
string? voiceId = obj.Data.ToString();
|
||||||
|
if (voice == null || voiceId == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
context.VoicesAvailable.Add(voiceId, voice);
|
||||||
|
_logger.LogInformation($"Created new tts voice named {voice} (id: {voiceId}).");
|
||||||
|
} else if (obj.Request.Type == "delete_tts_voice") {
|
||||||
|
_logger.LogDebug("Deleting tts voice.");
|
||||||
|
|
||||||
|
var voice = obj.Request.Data["@voice"];
|
||||||
|
if (!context.VoicesAvailable.TryGetValue(voice, out string voiceName) || voiceName == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.VoicesAvailable.Remove(voice);
|
||||||
|
_logger.LogInformation("Deleted a voice, named " + voiceName + ".");
|
||||||
|
} else if (obj.Request.Type == "update_tts_voice") {
|
||||||
|
_logger.LogDebug("Updating tts voice.");
|
||||||
|
string voiceId = obj.Request.Data["@idd"].ToString();
|
||||||
|
string voice = obj.Request.Data["@voice"].ToString();
|
||||||
|
|
||||||
|
if (!context.VoicesAvailable.TryGetValue(voiceId, out string voiceName) || voiceName == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.VoicesAvailable[voiceId] = voice;
|
||||||
|
_logger.LogInformation("Update tts voice: " + voice);
|
||||||
|
} else if (obj.Request.Type == "get_tts_users") {
|
||||||
|
_logger.LogDebug("Attempting to update all chatters' selected voice.");
|
||||||
|
var users = JsonSerializer.Deserialize<IDictionary<long, string>>(obj.Data.ToString(), _options);
|
||||||
|
if (users == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var temp = new ConcurrentDictionary<long, string>();
|
||||||
|
foreach (var entry in users)
|
||||||
|
temp.TryAdd(entry.Key, entry.Value);
|
||||||
|
context.VoicesSelected = temp;
|
||||||
|
_logger.LogInformation($"Fetched {temp.Count()} chatters' selected voice.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
24
Hermes/Socket/HermesSocketClient.cs
Normal file
24
Hermes/Socket/HermesSocketClient.cs
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using CommonSocketLibrary.Abstract;
|
||||||
|
using CommonSocketLibrary.Common;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace TwitchChatTTS.Hermes.Socket
|
||||||
|
{
|
||||||
|
public class HermesSocketClient : WebSocketClient {
|
||||||
|
public DateTime LastHeartbeat { get; set; }
|
||||||
|
public string? UserId { get; set; }
|
||||||
|
|
||||||
|
public HermesSocketClient(
|
||||||
|
ILogger<HermesSocketClient> logger,
|
||||||
|
[FromKeyedServices("hermes")] HandlerManager<WebSocketClient, IWebSocketHandler> handlerManager,
|
||||||
|
[FromKeyedServices("hermes")] HandlerTypeManager<WebSocketClient, IWebSocketHandler> typeManager
|
||||||
|
) : base(logger, handlerManager, typeManager, new JsonSerializerOptions() {
|
||||||
|
PropertyNameCaseInsensitive = false,
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
|
||||||
|
}) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
36
Hermes/Socket/Managers/HermesHandlerManager.cs
Normal file
36
Hermes/Socket/Managers/HermesHandlerManager.cs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
using CommonSocketLibrary.Common;
|
||||||
|
using CommonSocketLibrary.Socket.Manager;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace TwitchChatTTS.Hermes.Socket.Managers
|
||||||
|
{
|
||||||
|
public class HermesHandlerManager : WebSocketHandlerManager
|
||||||
|
{
|
||||||
|
public HermesHandlerManager(ILogger<HermesHandlerManager> logger, IServiceProvider provider) : base(logger) {
|
||||||
|
//Add(provider.GetRequiredService<HeartbeatHandler>());
|
||||||
|
try {
|
||||||
|
var basetype = typeof(IWebSocketHandler);
|
||||||
|
var assembly = GetType().Assembly;
|
||||||
|
var types = assembly.GetTypes().Where(t => t.IsClass && basetype.IsAssignableFrom(t) && t.AssemblyQualifiedName?.Contains(".Hermes.") == true);
|
||||||
|
|
||||||
|
foreach (var type in types) {
|
||||||
|
var key = "hermes-" + type.Name.Replace("Handlers", "Hand#lers")
|
||||||
|
.Replace("Handler", "")
|
||||||
|
.Replace("Hand#lers", "Handlers")
|
||||||
|
.ToLower();
|
||||||
|
var handler = provider.GetKeyedService<IWebSocketHandler>(key);
|
||||||
|
if (handler == null) {
|
||||||
|
logger.LogError("Failed to find hermes websocket handler: " + type.AssemblyQualifiedName);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.LogDebug($"Linked type {type.AssemblyQualifiedName} to hermes websocket handlers.");
|
||||||
|
Add(handler);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Logger.LogError(e, "Failed to load hermes websocket handler types.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
32
Hermes/Socket/Managers/HermesHandlerTypeManager.cs
Normal file
32
Hermes/Socket/Managers/HermesHandlerTypeManager.cs
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
using CommonSocketLibrary.Abstract;
|
||||||
|
using CommonSocketLibrary.Common;
|
||||||
|
using CommonSocketLibrary.Socket.Manager;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace TwitchChatTTS.Hermes.Socket.Managers
|
||||||
|
{
|
||||||
|
public class HermesHandlerTypeManager : WebSocketHandlerTypeManager
|
||||||
|
{
|
||||||
|
public HermesHandlerTypeManager(
|
||||||
|
ILogger<HermesHandlerTypeManager> factory,
|
||||||
|
[FromKeyedServices("hermes")] HandlerManager<WebSocketClient, IWebSocketHandler> handlers
|
||||||
|
) : base(factory, handlers)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Type? FetchMessageType(Type handlerType)
|
||||||
|
{
|
||||||
|
if (handlerType == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var name = handlerType.Namespace + "." + handlerType.Name;
|
||||||
|
name = name.Replace(".Handlers.", ".Data.")
|
||||||
|
.Replace("Handler", "Message")
|
||||||
|
.Replace("TwitchChatTTS.Hermes.", "HermesSocketLibrary.");
|
||||||
|
|
||||||
|
return Assembly.Load("HermesSocketLibrary").GetType(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
public class TTSUsernameFilter {
|
public class TTSUsernameFilter {
|
||||||
public string? Username { get; set; }
|
public string Username { get; set; }
|
||||||
public string? Tag { get; set; }
|
public string Tag { get; set; }
|
||||||
public string? UserId { get; set; }
|
public string UserId { get; set; }
|
||||||
}
|
}
|
@ -1,5 +1,5 @@
|
|||||||
public class TTSVoice {
|
public class TTSVoice {
|
||||||
public string? Label { get; set; }
|
public string Label { get; set; }
|
||||||
public int Value { get; set; }
|
public int Value { get; set; }
|
||||||
public string? Gender { get; set; }
|
public string? Gender { get; set; }
|
||||||
public string? Language { get; set; }
|
public string? Language { get; set; }
|
||||||
|
@ -1,8 +1,3 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace TwitchChatTTS.Hermes
|
namespace TwitchChatTTS.Hermes
|
||||||
{
|
{
|
||||||
public class TTSWordFilter
|
public class TTSWordFilter
|
||||||
|
@ -3,8 +3,8 @@ namespace TwitchChatTTS.OBS.Socket.Data
|
|||||||
[Serializable]
|
[Serializable]
|
||||||
public class EventMessage
|
public class EventMessage
|
||||||
{
|
{
|
||||||
public string eventType { get; set; }
|
public string EventType { get; set; }
|
||||||
public int eventIntent { get; set; }
|
public int EventIntent { get; set; }
|
||||||
public Dictionary<string, object> eventData { get; set; }
|
public Dictionary<string, object> EventData { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -3,13 +3,13 @@ namespace TwitchChatTTS.OBS.Socket.Data
|
|||||||
[Serializable]
|
[Serializable]
|
||||||
public class HelloMessage
|
public class HelloMessage
|
||||||
{
|
{
|
||||||
public string obsWebSocketVersion { get; set; }
|
public string ObsWebSocketVersion { get; set; }
|
||||||
public int rpcVersion { get; set; }
|
public int RpcVersion { get; set; }
|
||||||
public AuthenticationMessage authentication { get; set; }
|
public AuthenticationMessage Authentication { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class AuthenticationMessage {
|
public class AuthenticationMessage {
|
||||||
public string challenge { get; set; }
|
public string Challenge { get; set; }
|
||||||
public string salt { get; set; }
|
public string Salt { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -3,6 +3,6 @@ namespace TwitchChatTTS.OBS.Socket.Data
|
|||||||
[Serializable]
|
[Serializable]
|
||||||
public class IdentifiedMessage
|
public class IdentifiedMessage
|
||||||
{
|
{
|
||||||
public int negotiatedRpcVersion { get; set; }
|
public int NegotiatedRpcVersion { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -3,14 +3,14 @@ namespace TwitchChatTTS.OBS.Socket.Data
|
|||||||
[Serializable]
|
[Serializable]
|
||||||
public class IdentifyMessage
|
public class IdentifyMessage
|
||||||
{
|
{
|
||||||
public int rpcVersion { get; set; }
|
public int RpcVersion { get; set; }
|
||||||
public string? authentication { get; set; }
|
public string? Authentication { get; set; }
|
||||||
public int eventSubscriptions { get; set; }
|
public int EventSubscriptions { get; set; }
|
||||||
|
|
||||||
public IdentifyMessage(int version, string auth, int subscriptions) {
|
public IdentifyMessage(int version, string auth, int subscriptions) {
|
||||||
rpcVersion = version;
|
RpcVersion = version;
|
||||||
authentication = auth;
|
Authentication = auth;
|
||||||
eventSubscriptions = subscriptions;
|
EventSubscriptions = subscriptions;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -3,14 +3,14 @@ namespace TwitchChatTTS.OBS.Socket.Data
|
|||||||
[Serializable]
|
[Serializable]
|
||||||
public class RequestMessage
|
public class RequestMessage
|
||||||
{
|
{
|
||||||
public string requestType { get; set; }
|
public string RequestType { get; set; }
|
||||||
public string requestId { get; set; }
|
public string RequestId { get; set; }
|
||||||
public Dictionary<string, object> requestData { get; set; }
|
public Dictionary<string, object> RequestData { get; set; }
|
||||||
|
|
||||||
public RequestMessage(string type, string id, Dictionary<string, object> data) {
|
public RequestMessage(string type, string id, Dictionary<string, object> data) {
|
||||||
requestType = type;
|
RequestType = type;
|
||||||
requestId = id;
|
RequestId = id;
|
||||||
requestData = data;
|
RequestData = data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -3,9 +3,9 @@ namespace TwitchChatTTS.OBS.Socket.Data
|
|||||||
[Serializable]
|
[Serializable]
|
||||||
public class RequestResponseMessage
|
public class RequestResponseMessage
|
||||||
{
|
{
|
||||||
public string requestType { get; set; }
|
public string RequestType { get; set; }
|
||||||
public string requestId { get; set; }
|
public string RequestId { get; set; }
|
||||||
public object requestStatus { get; set; }
|
public object RequestStatus { get; set; }
|
||||||
public Dictionary<string, object> responseData { get; set; }
|
public Dictionary<string, object> ResponseData { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -21,15 +21,15 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
|
|||||||
if (message is not EventMessage obj || obj == null)
|
if (message is not EventMessage obj || obj == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
switch (obj.eventType) {
|
switch (obj.EventType) {
|
||||||
case "StreamStateChanged":
|
case "StreamStateChanged":
|
||||||
case "RecordStateChanged":
|
case "RecordStateChanged":
|
||||||
if (sender is not OBSSocketClient client)
|
if (sender is not OBSSocketClient client)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
string? raw_state = obj.eventData["outputState"].ToString();
|
string? raw_state = obj.EventData["outputState"].ToString();
|
||||||
string? state = raw_state?.Substring(21).ToLower();
|
string? state = raw_state?.Substring(21).ToLower();
|
||||||
client.Live = obj.eventData["outputActive"].ToString() == "True";
|
client.Live = obj.EventData["outputActive"].ToString() == "True";
|
||||||
Logger.LogWarning("Stream " + (state != null && state.EndsWith("ing") ? "is " : "has ") + state + ".");
|
Logger.LogWarning("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")) {
|
||||||
@ -37,7 +37,7 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
Logger.LogDebug(obj.eventType + " EVENT: " + string.Join(" | ", obj.eventData?.Select(x => x.Key + "=" + x.Value?.ToString()) ?? new string[0]));
|
Logger.LogDebug(obj.EventType + " EVENT: " + string.Join(" | ", obj.EventData?.Select(x => x.Key + "=" + x.Value?.ToString()) ?? new string[0]));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,15 +25,14 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
Logger.LogTrace("OBS websocket password: " + Context.Password);
|
Logger.LogTrace("OBS websocket password: " + Context.Password);
|
||||||
if (obj.authentication is null || Context.Password is null) // TODO: send re-identify message.
|
if (obj.Authentication == null || Context.Password == null) // TODO: send re-identify message.
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var salt = obj.authentication.salt;
|
var salt = obj.Authentication.Salt;
|
||||||
var challenge = obj.authentication.challenge;
|
var challenge = obj.Authentication.Challenge;
|
||||||
Logger.LogTrace("Salt: " + salt);
|
Logger.LogTrace("Salt: " + salt);
|
||||||
Logger.LogTrace("Challenge: " + challenge);
|
Logger.LogTrace("Challenge: " + challenge);
|
||||||
|
|
||||||
|
|
||||||
string secret = Context.Password + salt;
|
string secret = Context.Password + salt;
|
||||||
byte[] bytes = Encoding.UTF8.GetBytes(secret);
|
byte[] bytes = Encoding.UTF8.GetBytes(secret);
|
||||||
string hash = null;
|
string hash = null;
|
||||||
@ -48,8 +47,7 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
|
|||||||
}
|
}
|
||||||
|
|
||||||
Logger.LogTrace("Final hash: " + hash);
|
Logger.LogTrace("Final hash: " + hash);
|
||||||
//await sender.Send(1, new IdentifyMessage(obj.rpcVersion, hash, 1023 | 262144 | 524288));
|
await sender.Send(1, new IdentifyMessage(obj.RpcVersion, hash, 1023 | 262144));
|
||||||
await sender.Send(1, new IdentifyMessage(obj.rpcVersion, hash, 1023 | 262144));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -20,7 +20,7 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
sender.Connected = true;
|
sender.Connected = true;
|
||||||
Logger.LogInformation("Connected to OBS via rpc version " + obj.negotiatedRpcVersion + ".");
|
Logger.LogInformation("Connected to OBS via rpc version " + obj.NegotiatedRpcVersion + ".");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -19,13 +19,13 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
|
|||||||
if (message is not RequestResponseMessage obj || obj == null)
|
if (message is not RequestResponseMessage obj || obj == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
switch (obj.requestType) {
|
switch (obj.RequestType) {
|
||||||
case "GetOutputStatus":
|
case "GetOutputStatus":
|
||||||
if (sender is not OBSSocketClient client)
|
if (sender is not OBSSocketClient client)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (obj.requestId == "stream") {
|
if (obj.RequestId == "stream") {
|
||||||
client.Live = obj.responseData["outputActive"].ToString() == "True";
|
client.Live = obj.ResponseData["outputActive"].ToString() == "True";
|
||||||
Logger.LogWarning("Updated stream's live status to " + client.Live);
|
Logger.LogWarning("Updated stream's live status to " + client.Live);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -10,8 +10,7 @@ namespace TwitchChatTTS.OBS.Socket.Manager
|
|||||||
{
|
{
|
||||||
public OBSHandlerTypeManager(
|
public OBSHandlerTypeManager(
|
||||||
ILogger<OBSHandlerTypeManager> factory,
|
ILogger<OBSHandlerTypeManager> factory,
|
||||||
[FromKeyedServices("obs")] HandlerManager<WebSocketClient,
|
[FromKeyedServices("obs")] HandlerManager<WebSocketClient, IWebSocketHandler> handlers
|
||||||
IWebSocketHandler> handlers
|
|
||||||
) : base(factory, handlers)
|
) : base(factory, handlers)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
using TwitchChatTTS.OBS.Socket.Manager;
|
|
||||||
using CommonSocketLibrary.Common;
|
using CommonSocketLibrary.Common;
|
||||||
using CommonSocketLibrary.Abstract;
|
using CommonSocketLibrary.Abstract;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
@ -73,7 +73,6 @@ namespace TwitchChatTTS.Seven
|
|||||||
public IList<Emote> Emotes { get; set; }
|
public IList<Emote> Emotes { get; set; }
|
||||||
public int EmoteCount { get; set; }
|
public int EmoteCount { get; set; }
|
||||||
public int Capacity { get; set; }
|
public int Capacity { get; set; }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Emote {
|
public class Emote {
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using TwitchChatTTS.Helpers;
|
using TwitchChatTTS.Helpers;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using TwitchChatTTS;
|
|
||||||
using TwitchChatTTS.Seven;
|
using TwitchChatTTS.Seven;
|
||||||
|
|
||||||
public class SevenApiClient {
|
public class SevenApiClient {
|
||||||
|
public static readonly string API_URL = "https://7tv.io/v3";
|
||||||
|
public static readonly string WEBSOCKET_URL = "wss://events.7tv.io/v3";
|
||||||
|
|
||||||
private WebClientWrap Web { get; }
|
private WebClientWrap Web { get; }
|
||||||
private Configuration Configuration { get; }
|
|
||||||
private ILogger<SevenApiClient> Logger { get; }
|
private ILogger<SevenApiClient> Logger { get; }
|
||||||
private long? Id { get; }
|
private long? Id { get; }
|
||||||
|
|
||||||
|
|
||||||
public SevenApiClient(Configuration configuration, ILogger<SevenApiClient> logger, TwitchBotToken token) {
|
public SevenApiClient(ILogger<SevenApiClient> logger, TwitchBotToken token) {
|
||||||
Configuration = configuration;
|
|
||||||
Logger = logger;
|
Logger = logger;
|
||||||
Id = long.TryParse(token?.BroadcasterId, out long id) ? id : -1;
|
Id = long.TryParse(token?.BroadcasterId, out long id) ? id : -1;
|
||||||
|
|
||||||
@ -23,16 +23,16 @@ public class SevenApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async Task<EmoteDatabase?> GetSevenEmotes() {
|
public async Task<EmoteDatabase?> GetSevenEmotes() {
|
||||||
if (Id is null)
|
if (Id == null)
|
||||||
throw new NullReferenceException(nameof(Id));
|
throw new NullReferenceException(nameof(Id));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var details = await Web.GetJson<UserDetails>("https://7tv.io/v3/users/twitch/" + Id);
|
var details = await Web.GetJson<UserDetails>($"{API_URL}/users/twitch/" + Id);
|
||||||
if (details is null)
|
if (details == null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
var emotes = new EmoteDatabase();
|
var emotes = new EmoteDatabase();
|
||||||
if (details.EmoteSet is not null)
|
if (details.EmoteSet != null)
|
||||||
foreach (var emote in details.EmoteSet.Emotes)
|
foreach (var emote in details.EmoteSet.Emotes)
|
||||||
emotes.Add(emote.Name, emote.Id);
|
emotes.Add(emote.Name, emote.Id);
|
||||||
Logger.LogInformation($"Loaded {details.EmoteSet?.Emotes.Count() ?? 0} emotes from 7tv.");
|
Logger.LogInformation($"Loaded {details.EmoteSet?.Emotes.Count() ?? 0} emotes from 7tv.");
|
||||||
|
@ -2,8 +2,6 @@ namespace TwitchChatTTS.Seven.Socket.Context
|
|||||||
{
|
{
|
||||||
public class ReconnectContext
|
public class ReconnectContext
|
||||||
{
|
{
|
||||||
public string? Protocol;
|
|
||||||
public string Url;
|
|
||||||
public string? SessionId;
|
public string? SessionId;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,12 +0,0 @@
|
|||||||
namespace TwitchChatTTS.Seven.Socket.Context
|
|
||||||
{
|
|
||||||
public class SevenHelloContext
|
|
||||||
{
|
|
||||||
public IEnumerable<SevenSubscriptionConfiguration>? Subscriptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
public class SevenSubscriptionConfiguration {
|
|
||||||
public string? Type;
|
|
||||||
public IDictionary<string, string>? Condition;
|
|
||||||
}
|
|
||||||
}
|
|
@ -10,12 +10,12 @@ namespace TwitchChatTTS.Seven.Socket.Handlers
|
|||||||
public class DispatchHandler : IWebSocketHandler
|
public class DispatchHandler : IWebSocketHandler
|
||||||
{
|
{
|
||||||
private ILogger Logger { get; }
|
private ILogger Logger { get; }
|
||||||
private IServiceProvider ServiceProvider { get; }
|
private EmoteDatabase Emotes { get; }
|
||||||
public int OperationCode { get; set; } = 0;
|
public int OperationCode { get; set; } = 0;
|
||||||
|
|
||||||
public DispatchHandler(ILogger<DispatchHandler> logger, IServiceProvider serviceProvider) {
|
public DispatchHandler(ILogger<DispatchHandler> logger, EmoteDatabase emotes) {
|
||||||
Logger = logger;
|
Logger = logger;
|
||||||
ServiceProvider = serviceProvider;
|
Emotes = emotes;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data message)
|
public async Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data message)
|
||||||
@ -23,23 +23,31 @@ namespace TwitchChatTTS.Seven.Socket.Handlers
|
|||||||
if (message is not DispatchMessage obj || obj == null)
|
if (message is not DispatchMessage obj || obj == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
Do(obj?.Body?.Pulled, cf => cf.OldValue);
|
ApplyChanges(obj?.Body?.Pulled, cf => cf.OldValue, true);
|
||||||
Do(obj?.Body?.Pushed, cf => cf.Value);
|
ApplyChanges(obj?.Body?.Pushed, cf => cf.Value, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Do(IEnumerable<ChangeField>? fields, Func<ChangeField, object> getter) {
|
private void ApplyChanges(IEnumerable<ChangeField>? fields, Func<ChangeField, object> getter, bool removing) {
|
||||||
if (fields is null)
|
if (fields == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
//ServiceProvider.GetRequiredService<EmoteDatabase>()
|
|
||||||
foreach (var val in fields) {
|
foreach (var val in fields) {
|
||||||
if (getter(val) == null)
|
var value = getter(val);
|
||||||
|
if (value == null)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var o = JsonSerializer.Deserialize<EmoteField>(val.OldValue.ToString(), new JsonSerializerOptions() {
|
var o = JsonSerializer.Deserialize<EmoteField>(value.ToString(), new JsonSerializerOptions() {
|
||||||
PropertyNameCaseInsensitive = false,
|
PropertyNameCaseInsensitive = false,
|
||||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
|
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (removing) {
|
||||||
|
Emotes.Remove(o.Name);
|
||||||
|
Logger.LogInformation($"Removed 7tv emote: {o.Name} (id: {o.Id})");
|
||||||
|
} else {
|
||||||
|
Emotes.Add(o.Name, o.Id);
|
||||||
|
Logger.LogInformation($"Added 7tv emote: {o.Name} (id: {o.Id})");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ namespace TwitchChatTTS.Seven.Socket.Handlers
|
|||||||
public class EndOfStreamHandler : IWebSocketHandler
|
public class EndOfStreamHandler : IWebSocketHandler
|
||||||
{
|
{
|
||||||
private ILogger Logger { get; }
|
private ILogger Logger { get; }
|
||||||
|
private Configuration Configuration { get; }
|
||||||
private IServiceProvider ServiceProvider { get; }
|
private IServiceProvider ServiceProvider { get; }
|
||||||
private string[] ErrorCodes { get; }
|
private string[] ErrorCodes { get; }
|
||||||
private int[] ReconnectDelay { get; }
|
private int[] ReconnectDelay { get; }
|
||||||
@ -17,8 +18,9 @@ namespace TwitchChatTTS.Seven.Socket.Handlers
|
|||||||
public int OperationCode { get; set; } = 7;
|
public int OperationCode { get; set; } = 7;
|
||||||
|
|
||||||
|
|
||||||
public EndOfStreamHandler(ILogger<EndOfStreamHandler> logger, IServiceProvider serviceProvider) {
|
public EndOfStreamHandler(ILogger<EndOfStreamHandler> logger, Configuration configuration, IServiceProvider serviceProvider) {
|
||||||
Logger = logger;
|
Logger = logger;
|
||||||
|
Configuration = configuration;
|
||||||
ServiceProvider = serviceProvider;
|
ServiceProvider = serviceProvider;
|
||||||
|
|
||||||
ErrorCodes = [
|
ErrorCodes = [
|
||||||
@ -71,17 +73,23 @@ namespace TwitchChatTTS.Seven.Socket.Handlers
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(Configuration.Seven?.UserId))
|
||||||
|
return;
|
||||||
|
|
||||||
var context = ServiceProvider.GetRequiredService<ReconnectContext>();
|
var context = ServiceProvider.GetRequiredService<ReconnectContext>();
|
||||||
await Task.Delay(ReconnectDelay[code]);
|
await Task.Delay(ReconnectDelay[code]);
|
||||||
|
|
||||||
Logger.LogInformation($"7tv client reconnecting.");
|
//var base_url = "@" + string.Join(",", Configuration.Seven.SevenId.Select(sub => sub.Type + "<" + string.Join(",", sub.Condition?.Select(e => e.Key + "=" + e.Value) ?? new string[0]) + ">"));
|
||||||
await sender.ConnectAsync($"{context.Protocol ?? "wss"}://{context.Url}");
|
var base_url = $"@emote_set.*<object_id={Configuration.Seven.UserId.Trim()}>";
|
||||||
if (context.SessionId is null) {
|
string url = $"{SevenApiClient.WEBSOCKET_URL}{base_url}";
|
||||||
await sender.Send(33, new object());
|
Logger.LogDebug($"7tv websocket reconnecting to {url}.");
|
||||||
|
|
||||||
|
await sender.ConnectAsync(url);
|
||||||
|
if (context.SessionId != null) {
|
||||||
|
await sender.Send(34, new ResumeMessage() { SessionId = context.SessionId });
|
||||||
|
Logger.LogInformation("Resumed connection to 7tv websocket.");
|
||||||
} else {
|
} else {
|
||||||
await sender.Send(34, new ResumeMessage() {
|
Logger.LogDebug("7tv websocket session id not available.");
|
||||||
SessionId = context.SessionId
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
using CommonSocketLibrary.Abstract;
|
using CommonSocketLibrary.Abstract;
|
||||||
using CommonSocketLibrary.Common;
|
using CommonSocketLibrary.Common;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using TwitchChatTTS.Seven.Socket.Context;
|
|
||||||
using TwitchChatTTS.Seven.Socket.Data;
|
using TwitchChatTTS.Seven.Socket.Data;
|
||||||
|
|
||||||
namespace TwitchChatTTS.Seven.Socket.Handlers
|
namespace TwitchChatTTS.Seven.Socket.Handlers
|
||||||
@ -9,12 +8,12 @@ namespace TwitchChatTTS.Seven.Socket.Handlers
|
|||||||
public class SevenHelloHandler : IWebSocketHandler
|
public class SevenHelloHandler : IWebSocketHandler
|
||||||
{
|
{
|
||||||
private ILogger Logger { get; }
|
private ILogger Logger { get; }
|
||||||
private SevenHelloContext Context { get; }
|
private Configuration Configuration { get; }
|
||||||
public int OperationCode { get; set; } = 1;
|
public int OperationCode { get; set; } = 1;
|
||||||
|
|
||||||
public SevenHelloHandler(ILogger<SevenHelloHandler> logger, SevenHelloContext context) {
|
public SevenHelloHandler(ILogger<SevenHelloHandler> logger, Configuration configuration) {
|
||||||
Logger = logger;
|
Logger = logger;
|
||||||
Context = context;
|
Configuration = configuration;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data message)
|
public async Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data message)
|
||||||
@ -27,30 +26,7 @@ namespace TwitchChatTTS.Seven.Socket.Handlers
|
|||||||
|
|
||||||
seven.Connected = true;
|
seven.Connected = true;
|
||||||
seven.ConnectionDetails = obj;
|
seven.ConnectionDetails = obj;
|
||||||
|
Logger.LogInformation("Connected to 7tv websockets.");
|
||||||
// if (Context.Subscriptions == null || !Context.Subscriptions.Any()) {
|
|
||||||
// Logger.LogWarning("No subscriptions have been set for the 7tv websocket client.");
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
//await Task.Delay(TimeSpan.FromMilliseconds(1000));
|
|
||||||
//await sender.Send(33, new IdentifyMessage());
|
|
||||||
//await Task.Delay(TimeSpan.FromMilliseconds(5000));
|
|
||||||
//await sender.SendRaw("{\"op\":35,\"d\":{\"type\":\"emote_set.*\",\"condition\":{\"object_id\":\"64505914b9fc508169ffe7cc\"}}}");
|
|
||||||
//await sender.SendRaw(File.ReadAllText("test.txt"));
|
|
||||||
|
|
||||||
// foreach (var sub in Context.Subscriptions) {
|
|
||||||
// if (string.IsNullOrWhiteSpace(sub.Type)) {
|
|
||||||
// Logger.LogWarning("Non-existent or empty subscription type found on the 7tv websocket client.");
|
|
||||||
// continue;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Logger.LogDebug($"Subscription Type: {sub.Type} | Condition: {string.Join(", ", sub.Condition?.Select(e => e.Key + "=" + e.Value) ?? new string[0])}");
|
|
||||||
// await sender.Send(35, new SubscribeMessage() {
|
|
||||||
// Type = sub.Type,
|
|
||||||
// Condition = sub.Condition
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -3,7 +3,7 @@ using CommonSocketLibrary.Socket.Manager;
|
|||||||
using CommonSocketLibrary.Common;
|
using CommonSocketLibrary.Common;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace TwitchChatTTS.Seven.Socket.Manager
|
namespace TwitchChatTTS.Seven.Socket.Managers
|
||||||
{
|
{
|
||||||
public class SevenHandlerManager : WebSocketHandlerManager
|
public class SevenHandlerManager : WebSocketHandlerManager
|
||||||
{
|
{
|
@ -4,7 +4,7 @@ using CommonSocketLibrary.Socket.Manager;
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace TwitchChatTTS.Seven.Socket.Manager
|
namespace TwitchChatTTS.Seven.Socket.Managers
|
||||||
{
|
{
|
||||||
public class SevenHandlerTypeManager : WebSocketHandlerTypeManager
|
public class SevenHandlerTypeManager : WebSocketHandlerTypeManager
|
||||||
{
|
{
|
135
Startup.cs
135
Startup.cs
@ -7,38 +7,30 @@ using CommonSocketLibrary.Abstract;
|
|||||||
using CommonSocketLibrary.Common;
|
using CommonSocketLibrary.Common;
|
||||||
using YamlDotNet.Serialization;
|
using YamlDotNet.Serialization;
|
||||||
using YamlDotNet.Serialization.NamingConventions;
|
using YamlDotNet.Serialization.NamingConventions;
|
||||||
using TwitchChatTTS.Twitch;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using TwitchChatTTS.Seven.Socket.Manager;
|
|
||||||
using TwitchChatTTS.Seven.Socket;
|
using TwitchChatTTS.Seven.Socket;
|
||||||
using TwitchChatTTS.OBS.Socket.Handlers;
|
using TwitchChatTTS.OBS.Socket.Handlers;
|
||||||
using TwitchChatTTS.Seven.Socket.Handlers;
|
using TwitchChatTTS.Seven.Socket.Handlers;
|
||||||
using TwitchChatTTS.Seven.Socket.Context;
|
using TwitchChatTTS.Seven.Socket.Context;
|
||||||
using TwitchChatTTS.Seven;
|
using TwitchChatTTS.Seven;
|
||||||
using TwitchChatTTS.OBS.Socket.Context;
|
using TwitchChatTTS.OBS.Socket.Context;
|
||||||
|
using TwitchLib.Client.Interfaces;
|
||||||
/**
|
using TwitchLib.Client;
|
||||||
Future handshake/connection procedure:
|
using TwitchLib.PubSub.Interfaces;
|
||||||
- GET all tts config data
|
using TwitchLib.PubSub;
|
||||||
- Continuous connection to server to receive commands from tom & send logs/errors (med priority, though tough task)
|
using TwitchLib.Communication.Interfaces;
|
||||||
|
using TwitchChatTTS.Seven.Socket.Managers;
|
||||||
Ideas:
|
using TwitchChatTTS.Hermes.Socket.Handlers;
|
||||||
- Filter messages by badges.
|
using TwitchChatTTS.Hermes.Socket;
|
||||||
- Speed up TTS based on message queue size?
|
using TwitchChatTTS.Hermes.Socket.Managers;
|
||||||
- Cut TTS off shortly after raid (based on size of raid)?
|
using TwitchChatTTS.Chat.Commands.Parameters;
|
||||||
- Limit duration of TTS
|
using TwitchChatTTS.Chat.Commands;
|
||||||
**/
|
using System.Text.Json;
|
||||||
|
|
||||||
// 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
|
||||||
// SE voices: https://api.streamelements.com/kappa/v2/speech?voice=brian&text=hello
|
// SE voices: https://api.streamelements.com/kappa/v2/speech?voice=brian&text=hello
|
||||||
|
|
||||||
// TODO:
|
|
||||||
// Fix OBS/7tv websocket connections when not available.
|
|
||||||
// Make it possible to do things at end of streams.
|
|
||||||
// Update emote database with twitch emotes.
|
|
||||||
// Event Subscription for emote usage?
|
|
||||||
|
|
||||||
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
|
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
|
||||||
var s = builder.Services;
|
var s = builder.Services;
|
||||||
|
|
||||||
@ -49,7 +41,7 @@ var deserializer = new DeserializerBuilder()
|
|||||||
var configContent = File.ReadAllText("tts.config.yml");
|
var configContent = File.ReadAllText("tts.config.yml");
|
||||||
var configuration = deserializer.Deserialize<Configuration>(configContent);
|
var configuration = deserializer.Deserialize<Configuration>(configContent);
|
||||||
var redeemKeys = configuration.Twitch?.Redeems?.Keys;
|
var redeemKeys = configuration.Twitch?.Redeems?.Keys;
|
||||||
if (redeemKeys is not null) {
|
if (redeemKeys != null) {
|
||||||
foreach (var key in redeemKeys) {
|
foreach (var key in redeemKeys) {
|
||||||
if (key != key.ToLower() && configuration.Twitch?.Redeems != null)
|
if (key != key.ToLower() && configuration.Twitch?.Redeems != null)
|
||||||
configuration.Twitch.Redeems.Add(key.ToLower(), configuration.Twitch.Redeems[key]);
|
configuration.Twitch.Redeems.Add(key.ToLower(), configuration.Twitch.Redeems[key]);
|
||||||
@ -58,45 +50,35 @@ if (redeemKeys is not null) {
|
|||||||
s.AddSingleton<Configuration>(configuration);
|
s.AddSingleton<Configuration>(configuration);
|
||||||
|
|
||||||
s.AddLogging();
|
s.AddLogging();
|
||||||
|
s.AddSingleton<User>(new User());
|
||||||
|
|
||||||
s.AddSingleton<TTSContext>(sp => {
|
s.AddSingleton<JsonSerializerOptions>(new JsonSerializerOptions() {
|
||||||
var context = new TTSContext();
|
PropertyNameCaseInsensitive = false,
|
||||||
var logger = sp.GetRequiredService<ILogger<TTSContext>>();
|
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
|
||||||
var hermes = sp.GetRequiredService<HermesClient>();
|
|
||||||
|
|
||||||
logger.LogInformation("Fetching TTS username filters...");
|
|
||||||
var usernameFiltersList = hermes.FetchTTSUsernameFilters();
|
|
||||||
usernameFiltersList.Wait();
|
|
||||||
context.UsernameFilters = usernameFiltersList.Result.Where(x => x.Username != null).ToDictionary(x => x.Username ?? "", x => x);
|
|
||||||
logger.LogInformation($"{context.UsernameFilters.Where(f => f.Value.Tag == "blacklisted").Count()} username(s) have been blocked.");
|
|
||||||
logger.LogInformation($"{context.UsernameFilters.Where(f => f.Value.Tag == "priority").Count()} user(s) have been prioritized.");
|
|
||||||
|
|
||||||
var enabledVoices = hermes.FetchTTSEnabledVoices();
|
|
||||||
enabledVoices.Wait();
|
|
||||||
context.EnabledVoices = enabledVoices.Result;
|
|
||||||
logger.LogInformation($"{context.EnabledVoices.Count()} TTS voices enabled.");
|
|
||||||
|
|
||||||
var wordFilters = hermes.FetchTTSWordFilters();
|
|
||||||
wordFilters.Wait();
|
|
||||||
context.WordFilters = wordFilters.Result;
|
|
||||||
logger.LogInformation($"{context.WordFilters.Count()} TTS word filters.");
|
|
||||||
|
|
||||||
var defaultVoice = hermes.FetchTTSDefaultVoice();
|
|
||||||
defaultVoice.Wait();
|
|
||||||
context.DefaultVoice = defaultVoice.Result ?? "Brian";
|
|
||||||
logger.LogInformation("Default Voice: " + context.DefaultVoice);
|
|
||||||
|
|
||||||
return context;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Command parameters
|
||||||
|
s.AddKeyedSingleton<ChatCommandParameter, TTSVoiceNameParameter>("parameter-ttsvoicename");
|
||||||
|
s.AddKeyedSingleton<ChatCommandParameter, UnvalidatedParameter>("parameter-unvalidated");
|
||||||
|
s.AddKeyedSingleton<ChatCommand, SkipAllCommand>("command-skipall");
|
||||||
|
s.AddKeyedSingleton<ChatCommand, SkipCommand>("command-skip");
|
||||||
|
s.AddKeyedSingleton<ChatCommand, VoiceCommand>("command-voice");
|
||||||
|
s.AddKeyedSingleton<ChatCommand, AddTTSVoiceCommand>("command-addttsvoice");
|
||||||
|
s.AddKeyedSingleton<ChatCommand, RemoveTTSVoiceCommand>("command-removettsvoice");
|
||||||
|
s.AddSingleton<ChatCommandManager>();
|
||||||
|
|
||||||
s.AddSingleton<TTSPlayer>();
|
s.AddSingleton<TTSPlayer>();
|
||||||
s.AddSingleton<ChatMessageHandler>();
|
s.AddSingleton<ChatMessageHandler>();
|
||||||
s.AddSingleton<HermesClient>();
|
s.AddSingleton<HermesClient>();
|
||||||
s.AddTransient<TwitchBotToken>(sp => {
|
s.AddSingleton<TwitchBotToken>(sp => {
|
||||||
var hermes = sp.GetRequiredService<HermesClient>();
|
var hermes = sp.GetRequiredService<HermesClient>();
|
||||||
var task = hermes.FetchTwitchBotToken();
|
var task = hermes.FetchTwitchBotToken();
|
||||||
task.Wait();
|
task.Wait();
|
||||||
return task.Result;
|
return task.Result;
|
||||||
});
|
});
|
||||||
|
s.AddTransient<IClient, TwitchLib.Communication.Clients.WebSocketClient>();
|
||||||
|
s.AddTransient<ITwitchClient, TwitchClient>();
|
||||||
|
s.AddTransient<ITwitchPubSub, TwitchPubSub>();
|
||||||
s.AddSingleton<TwitchApiClient>();
|
s.AddSingleton<TwitchApiClient>();
|
||||||
|
|
||||||
s.AddSingleton<SevenApiClient>();
|
s.AddSingleton<SevenApiClient>();
|
||||||
@ -106,14 +88,11 @@ s.AddSingleton<EmoteDatabase>(sp => {
|
|||||||
task.Wait();
|
task.Wait();
|
||||||
return task.Result;
|
return task.Result;
|
||||||
});
|
});
|
||||||
var emoteCounter = new EmoteCounter();
|
s.AddSingleton<EmoteCounter>(sp => {
|
||||||
if (!string.IsNullOrWhiteSpace(configuration.Emotes?.CounterFilePath) && File.Exists(configuration.Emotes.CounterFilePath.Trim())) {
|
if (!string.IsNullOrWhiteSpace(configuration.Emotes?.CounterFilePath) && File.Exists(configuration.Emotes.CounterFilePath.Trim()))
|
||||||
var d = new DeserializerBuilder()
|
return deserializer.Deserialize<EmoteCounter>(File.ReadAllText(configuration.Emotes.CounterFilePath.Trim()));
|
||||||
.WithNamingConvention(HyphenatedNamingConvention.Instance)
|
return new EmoteCounter();
|
||||||
.Build();
|
});
|
||||||
emoteCounter = deserializer.Deserialize<EmoteCounter>(File.ReadAllText(configuration.Emotes.CounterFilePath.Trim()));
|
|
||||||
}
|
|
||||||
s.AddSingleton<EmoteCounter>(emoteCounter);
|
|
||||||
|
|
||||||
// OBS websocket
|
// OBS websocket
|
||||||
s.AddSingleton<HelloContext>(sp =>
|
s.AddSingleton<HelloContext>(sp =>
|
||||||
@ -135,34 +114,16 @@ s.AddKeyedSingleton<SocketClient<WebSocketMessage>, OBSSocketClient>("obs");
|
|||||||
// 7tv websocket
|
// 7tv websocket
|
||||||
s.AddTransient(sp => {
|
s.AddTransient(sp => {
|
||||||
var logger = sp.GetRequiredService<ILogger<ReconnectContext>>();
|
var logger = sp.GetRequiredService<ILogger<ReconnectContext>>();
|
||||||
var configuration = sp.GetRequiredService<Configuration>();
|
|
||||||
var client = sp.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("7tv") as SevenSocketClient;
|
var client = sp.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("7tv") as SevenSocketClient;
|
||||||
if (client == null) {
|
if (client == null) {
|
||||||
logger.LogError("7tv client is null.");
|
logger.LogError("7tv client == null.");
|
||||||
return new ReconnectContext() {
|
return new ReconnectContext() { SessionId = null };
|
||||||
Protocol = configuration.Seven?.Protocol,
|
|
||||||
Url = configuration.Seven?.Url,
|
|
||||||
SessionId = null
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
if (client.ConnectionDetails == null) {
|
if (client.ConnectionDetails == null) {
|
||||||
logger.LogError("Connection details in 7tv client is null.");
|
logger.LogError("Connection details in 7tv client == null.");
|
||||||
return new ReconnectContext() {
|
return new ReconnectContext() { SessionId = null };
|
||||||
Protocol = configuration.Seven?.Protocol,
|
|
||||||
Url = configuration.Seven?.Url,
|
|
||||||
SessionId = null
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
return new ReconnectContext() {
|
return new ReconnectContext() { SessionId = client.ConnectionDetails.SessionId };
|
||||||
Protocol = configuration.Seven?.Protocol,
|
|
||||||
Url = configuration.Seven?.Url,
|
|
||||||
SessionId = client.ConnectionDetails.SessionId
|
|
||||||
};
|
|
||||||
});
|
|
||||||
s.AddSingleton<SevenHelloContext>(sp => {
|
|
||||||
return new SevenHelloContext() {
|
|
||||||
Subscriptions = configuration.Seven?.InitialSubscriptions
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
s.AddKeyedSingleton<IWebSocketHandler, SevenHelloHandler>("7tv-sevenhello");
|
s.AddKeyedSingleton<IWebSocketHandler, SevenHelloHandler>("7tv-sevenhello");
|
||||||
s.AddKeyedSingleton<IWebSocketHandler, HelloHandler>("7tv-hello");
|
s.AddKeyedSingleton<IWebSocketHandler, HelloHandler>("7tv-hello");
|
||||||
@ -175,9 +136,17 @@ s.AddKeyedSingleton<HandlerManager<WebSocketClient, IWebSocketHandler>, SevenHan
|
|||||||
s.AddKeyedSingleton<HandlerTypeManager<WebSocketClient, IWebSocketHandler>, SevenHandlerTypeManager>("7tv");
|
s.AddKeyedSingleton<HandlerTypeManager<WebSocketClient, IWebSocketHandler>, SevenHandlerTypeManager>("7tv");
|
||||||
s.AddKeyedSingleton<SocketClient<WebSocketMessage>, SevenSocketClient>("7tv");
|
s.AddKeyedSingleton<SocketClient<WebSocketMessage>, SevenSocketClient>("7tv");
|
||||||
|
|
||||||
|
// hermes websocket
|
||||||
|
s.AddKeyedSingleton<IWebSocketHandler, HeartbeatHandler>("hermes-heartbeat");
|
||||||
|
s.AddKeyedSingleton<IWebSocketHandler, LoginAckHandler>("hermes-loginack");
|
||||||
|
s.AddKeyedSingleton<IWebSocketHandler, RequestAckHandler>("hermes-requestack");
|
||||||
|
s.AddKeyedSingleton<IWebSocketHandler, HeartbeatHandler>("hermes-error");
|
||||||
|
|
||||||
|
s.AddKeyedSingleton<HandlerManager<WebSocketClient, IWebSocketHandler>, HermesHandlerManager>("hermes");
|
||||||
|
s.AddKeyedSingleton<HandlerTypeManager<WebSocketClient, IWebSocketHandler>, HermesHandlerTypeManager>("hermes");
|
||||||
|
s.AddKeyedSingleton<SocketClient<WebSocketMessage>, HermesSocketClient>("hermes");
|
||||||
|
|
||||||
s.AddHostedService<TTS>();
|
s.AddHostedService<TTS>();
|
||||||
|
|
||||||
using IHost host = builder.Build();
|
using IHost host = builder.Build();
|
||||||
using IServiceScope scope = host.Services.CreateAsyncScope();
|
|
||||||
IServiceProvider provider = scope.ServiceProvider;
|
|
||||||
await host.RunAsync();
|
await host.RunAsync();
|
210
TTS.cs
210
TTS.cs
@ -2,11 +2,12 @@ using System.Runtime.InteropServices;
|
|||||||
using System.Web;
|
using System.Web;
|
||||||
using CommonSocketLibrary.Abstract;
|
using CommonSocketLibrary.Abstract;
|
||||||
using CommonSocketLibrary.Common;
|
using CommonSocketLibrary.Common;
|
||||||
|
using HermesSocketLibrary.Socket.Data;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using NAudio.Wave;
|
|
||||||
using NAudio.Wave.SampleProviders;
|
using NAudio.Wave.SampleProviders;
|
||||||
|
using TwitchChatTTS.Hermes.Socket;
|
||||||
using TwitchLib.Client.Events;
|
using TwitchLib.Client.Events;
|
||||||
using YamlDotNet.Serialization;
|
using YamlDotNet.Serialization;
|
||||||
using YamlDotNet.Serialization.NamingConventions;
|
using YamlDotNet.Serialization.NamingConventions;
|
||||||
@ -15,32 +16,54 @@ namespace TwitchChatTTS
|
|||||||
{
|
{
|
||||||
public class TTS : IHostedService
|
public class TTS : IHostedService
|
||||||
{
|
{
|
||||||
private ILogger Logger { get; }
|
private readonly ILogger _logger;
|
||||||
private Configuration Configuration { get; }
|
private readonly Configuration _configuration;
|
||||||
private TTSPlayer Player { get; }
|
private readonly TTSPlayer _player;
|
||||||
private IServiceProvider ServiceProvider { get; }
|
private readonly IServiceProvider _serviceProvider;
|
||||||
private ISampleProvider? Playing { get; set; }
|
|
||||||
|
|
||||||
public TTS(ILogger<TTS> logger, Configuration configuration, TTSPlayer player, IServiceProvider serviceProvider) {
|
public TTS(ILogger<TTS> logger, Configuration configuration, TTSPlayer player, IServiceProvider serviceProvider) {
|
||||||
Logger = logger;
|
_logger = logger;
|
||||||
Configuration = configuration;
|
_configuration = configuration;
|
||||||
Player = player;
|
_player = player;
|
||||||
ServiceProvider = serviceProvider;
|
_serviceProvider = serviceProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task StartAsync(CancellationToken cancellationToken) {
|
public async Task StartAsync(CancellationToken cancellationToken) {
|
||||||
Console.Title = "TTS - Twitch Chat";
|
Console.Title = "TTS - Twitch Chat";
|
||||||
|
|
||||||
|
var user = _serviceProvider.GetRequiredService<User>();
|
||||||
|
var hermes = await InitializeHermes();
|
||||||
|
|
||||||
|
var hermesAccount = await hermes.FetchHermesAccountDetails();
|
||||||
|
user.HermesUserId = hermesAccount.Id;
|
||||||
|
user.TwitchUsername = hermesAccount.Username;
|
||||||
|
|
||||||
|
var twitchBotToken = await hermes.FetchTwitchBotToken();
|
||||||
|
user.TwitchUserId = long.Parse(twitchBotToken.BroadcasterId);
|
||||||
|
_logger.LogInformation($"Username: {user.TwitchUsername} (id: {user.TwitchUserId})");
|
||||||
|
|
||||||
|
user.DefaultTTSVoice = await hermes.FetchTTSDefaultVoice();
|
||||||
|
_logger.LogInformation("Default Voice: " + user.DefaultTTSVoice);
|
||||||
|
|
||||||
|
var wordFilters = await hermes.FetchTTSWordFilters();
|
||||||
|
user.RegexFilters = wordFilters.ToList();
|
||||||
|
_logger.LogInformation($"{user.RegexFilters.Count()} TTS word filters.");
|
||||||
|
|
||||||
|
var usernameFilters = await hermes.FetchTTSUsernameFilters();
|
||||||
|
user.ChatterFilters = usernameFilters.ToDictionary(e => e.Username, e => e);
|
||||||
|
_logger.LogInformation($"{user.ChatterFilters.Where(f => f.Value.Tag == "blacklisted").Count()} username(s) have been blocked.");
|
||||||
|
_logger.LogInformation($"{user.ChatterFilters.Where(f => f.Value.Tag == "priority").Count()} user(s) have been prioritized.");
|
||||||
|
|
||||||
|
var twitchapiclient = await InitializeTwitchApiClient(user.TwitchUsername);
|
||||||
|
|
||||||
|
await InitializeHermesWebsocket(user);
|
||||||
await InitializeSevenTv();
|
await InitializeSevenTv();
|
||||||
await InitializeObs();
|
await InitializeObs();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var hermes = await InitializeHermes();
|
|
||||||
var twitchapiclient = await InitializeTwitchApiClient(hermes);
|
|
||||||
|
|
||||||
AudioPlaybackEngine.Instance.AddOnMixerInputEnded((object? s, SampleProviderEventArgs e) => {
|
AudioPlaybackEngine.Instance.AddOnMixerInputEnded((object? s, SampleProviderEventArgs e) => {
|
||||||
if (e.SampleProvider == Playing) {
|
if (e.SampleProvider == _player.Playing) {
|
||||||
Playing = null;
|
_player.Playing = null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -48,11 +71,11 @@ namespace TwitchChatTTS
|
|||||||
while (true) {
|
while (true) {
|
||||||
try {
|
try {
|
||||||
if (cancellationToken.IsCancellationRequested) {
|
if (cancellationToken.IsCancellationRequested) {
|
||||||
Logger.LogWarning("TTS Buffer - Cancellation token was canceled.");
|
_logger.LogWarning("TTS Buffer - Cancellation token was canceled.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var m = Player.ReceiveBuffer();
|
var m = _player.ReceiveBuffer();
|
||||||
if (m == null) {
|
if (m == null) {
|
||||||
await Task.Delay(200);
|
await Task.Delay(200);
|
||||||
continue;
|
continue;
|
||||||
@ -63,14 +86,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.LogDebug("Fetched TTS audio data.");
|
_logger.LogDebug("Fetched TTS audio data.");
|
||||||
|
|
||||||
m.Audio = resampled;
|
m.Audio = resampled;
|
||||||
Player.Ready(m);
|
_player.Ready(m);
|
||||||
} catch (COMException e) {
|
} catch (COMException e) {
|
||||||
Logger.LogError(e, "Failed to send request for TTS (HResult: " + e.HResult + ").");
|
_logger.LogError(e, "Failed to send request for TTS (HResult: " + e.HResult + ").");
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Logger.LogError(e, "Failed to send request for TTS.");
|
_logger.LogError(e, "Failed to send request for TTS.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -79,40 +102,40 @@ namespace TwitchChatTTS
|
|||||||
while (true) {
|
while (true) {
|
||||||
try {
|
try {
|
||||||
if (cancellationToken.IsCancellationRequested) {
|
if (cancellationToken.IsCancellationRequested) {
|
||||||
Logger.LogWarning("TTS Queue - Cancellation token was canceled.");
|
_logger.LogWarning("TTS Queue - Cancellation token was canceled.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
while (Player.IsEmpty() || Playing != null) {
|
while (_player.IsEmpty() || _player.Playing != null) {
|
||||||
await Task.Delay(200);
|
await Task.Delay(200);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
var m = Player.ReceiveReady();
|
var m = _player.ReceiveReady();
|
||||||
if (m == null) {
|
if (m == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(m.File) && File.Exists(m.File)) {
|
if (!string.IsNullOrWhiteSpace(m.File) && File.Exists(m.File)) {
|
||||||
Logger.LogInformation("Playing message: " + m.File);
|
_logger.LogInformation("Playing message: " + m.File);
|
||||||
AudioPlaybackEngine.Instance.PlaySound(m.File);
|
AudioPlaybackEngine.Instance.PlaySound(m.File);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.LogInformation("Playing message: " + m.Message);
|
_logger.LogInformation("Playing message: " + m.Message);
|
||||||
Playing = m.Audio;
|
_player.Playing = m.Audio;
|
||||||
if (m.Audio != null)
|
if (m.Audio != null)
|
||||||
AudioPlaybackEngine.Instance.AddMixerInput(m.Audio);
|
AudioPlaybackEngine.Instance.AddMixerInput(m.Audio);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Logger.LogError(e, "Failed to play a TTS audio message");
|
_logger.LogError(e, "Failed to play a TTS audio message");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
StartSavingEmoteCounter();
|
StartSavingEmoteCounter();
|
||||||
|
|
||||||
Logger.LogInformation("Twitch API client connecting...");
|
_logger.LogInformation("Twitch API client connecting...");
|
||||||
await twitchapiclient.Connect();
|
await twitchapiclient.Connect();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Logger.LogError(e, "Failed to initialize.");
|
_logger.LogError(e, "Failed to initialize.");
|
||||||
}
|
}
|
||||||
Console.ReadLine();
|
Console.ReadLine();
|
||||||
}
|
}
|
||||||
@ -120,75 +143,100 @@ namespace TwitchChatTTS
|
|||||||
public async Task StopAsync(CancellationToken cancellationToken)
|
public async Task StopAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (cancellationToken.IsCancellationRequested)
|
if (cancellationToken.IsCancellationRequested)
|
||||||
Logger.LogWarning("Application has stopped due to cancellation token.");
|
_logger.LogWarning("Application has stopped due to cancellation token.");
|
||||||
else
|
else
|
||||||
Logger.LogWarning("Application has stopped.");
|
_logger.LogWarning("Application has stopped.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task InitializeHermesWebsocket(User user) {
|
||||||
|
if (_configuration.Hermes?.Token == null) {
|
||||||
|
_logger.LogDebug("No api token given to hermes. Skipping hermes websockets.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
_logger.LogInformation("Initializing hermes websocket client.");
|
||||||
|
var hermesClient = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("hermes") as HermesSocketClient;
|
||||||
|
var url = "wss://hermes-ws.goblincaves.com";
|
||||||
|
_logger.LogDebug($"Attempting to connect to {url}");
|
||||||
|
await hermesClient.ConnectAsync(url);
|
||||||
|
await hermesClient.Send(1, new HermesLoginMessage() {
|
||||||
|
ApiKey = _configuration.Hermes.Token
|
||||||
|
});
|
||||||
|
|
||||||
|
while (hermesClient.UserId == null)
|
||||||
|
await Task.Delay(TimeSpan.FromMilliseconds(200));
|
||||||
|
|
||||||
|
await hermesClient.Send(3, new RequestMessage() {
|
||||||
|
Type = "get_tts_voices",
|
||||||
|
Data = null
|
||||||
|
});
|
||||||
|
var token = _serviceProvider.GetRequiredService<TwitchBotToken>();
|
||||||
|
await hermesClient.Send(3, new RequestMessage() {
|
||||||
|
Type = "get_tts_users",
|
||||||
|
Data = new Dictionary<string, string>() { { "@broadcaster", token.BroadcasterId } }
|
||||||
|
});
|
||||||
|
} catch (Exception) {
|
||||||
|
_logger.LogWarning("Connecting to hermes failed. Skipping hermes websockets.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task InitializeSevenTv() {
|
private async Task InitializeSevenTv() {
|
||||||
Logger.LogInformation("Initializing 7tv client.");
|
if (_configuration.Seven?.UserId == null) {
|
||||||
var sevenClient = ServiceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("7tv");
|
_logger.LogDebug("No user id given to 7tv. Skipping 7tv websockets.");
|
||||||
if (Configuration.Seven is not null && !string.IsNullOrWhiteSpace(Configuration.Seven.Url)) {
|
return;
|
||||||
var base_url = "@" + string.Join(",", Configuration.Seven.InitialSubscriptions.Select(sub => sub.Type + "<" + string.Join(",", sub.Condition?.Select(e => e.Key + "=" + e.Value) ?? new string[0]) + ">"));
|
}
|
||||||
Logger.LogDebug($"Attempting to connect to {Configuration.Seven.Protocol?.Trim() ?? "wss"}://{Configuration.Seven.Url.Trim()}{base_url}");
|
|
||||||
await sevenClient.ConnectAsync($"{Configuration.Seven.Protocol?.Trim() ?? "wss"}://{Configuration.Seven.Url.Trim()}{base_url}");
|
try {
|
||||||
|
_logger.LogInformation("Initializing 7tv websocket client.");
|
||||||
|
var sevenClient = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("7tv");
|
||||||
|
//var base_url = "@" + string.Join(",", Configuration.Seven.InitialSubscriptions.Select(sub => sub.Type + "<" + string.Join(",", sub.Condition?.Select(e => e.Key + "=" + e.Value) ?? new string[0]) + ">"));
|
||||||
|
var url = $"{SevenApiClient.WEBSOCKET_URL}@emote_set.*<object_id={_configuration.Seven.UserId.Trim()}>";
|
||||||
|
_logger.LogDebug($"Attempting to connect to {url}");
|
||||||
|
await sevenClient.ConnectAsync($"{url}");
|
||||||
|
} catch (Exception) {
|
||||||
|
_logger.LogWarning("Connecting to 7tv failed. Skipping 7tv websockets.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task InitializeObs() {
|
private async Task InitializeObs() {
|
||||||
Logger.LogInformation("Initializing obs client.");
|
if (_configuration.Obs == null || string.IsNullOrWhiteSpace(_configuration.Obs.Host) || !_configuration.Obs.Port.HasValue || _configuration.Obs.Port.Value < 0) {
|
||||||
var obsClient = ServiceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("obs");
|
_logger.LogDebug("Lacking obs connection info. Skipping obs websockets.");
|
||||||
if (Configuration.Obs is not null && !string.IsNullOrWhiteSpace(Configuration.Obs.Host) && Configuration.Obs.Port.HasValue && Configuration.Obs.Port.Value >= 0) {
|
return;
|
||||||
Logger.LogDebug($"Attempting to connect to ws://{Configuration.Obs.Host.Trim()}:{Configuration.Obs.Port}");
|
}
|
||||||
await obsClient.ConnectAsync($"ws://{Configuration.Obs.Host.Trim()}:{Configuration.Obs.Port}");
|
|
||||||
await Task.Delay(500);
|
try {
|
||||||
|
_logger.LogInformation("Initializing obs websocket client.");
|
||||||
|
var obsClient = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("obs");
|
||||||
|
var url = $"ws://{_configuration.Obs.Host.Trim()}:{_configuration.Obs.Port}";
|
||||||
|
_logger.LogDebug($"Attempting to connect to {url}");
|
||||||
|
await obsClient.ConnectAsync(url);
|
||||||
|
} catch (Exception) {
|
||||||
|
_logger.LogWarning("Connecting to obs failed. Skipping obs websockets.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<HermesClient> InitializeHermes() {
|
private async Task<HermesClient> InitializeHermes() {
|
||||||
// Fetch id and username based on api key given.
|
// Fetch id and username based on api key given.
|
||||||
Logger.LogInformation("Initializing hermes client.");
|
_logger.LogInformation("Initializing hermes client.");
|
||||||
var hermes = ServiceProvider.GetRequiredService<HermesClient>();
|
var hermes = _serviceProvider.GetRequiredService<HermesClient>();
|
||||||
await hermes.FetchHermesAccountDetails();
|
await hermes.FetchHermesAccountDetails();
|
||||||
|
|
||||||
if (hermes.Username == null)
|
|
||||||
throw new Exception("Username fetched from Hermes is invalid.");
|
|
||||||
|
|
||||||
Logger.LogInformation("Username: " + hermes.Username);
|
|
||||||
return hermes;
|
return hermes;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<TwitchApiClient> InitializeTwitchApiClient(HermesClient hermes) {
|
private async Task<TwitchApiClient> InitializeTwitchApiClient(string username) {
|
||||||
Logger.LogInformation("Initializing twitch client.");
|
_logger.LogInformation("Initializing twitch client.");
|
||||||
var twitchapiclient = ServiceProvider.GetRequiredService<TwitchApiClient>();
|
var twitchapiclient = _serviceProvider.GetRequiredService<TwitchApiClient>();
|
||||||
await twitchapiclient.Authorize();
|
await twitchapiclient.Authorize();
|
||||||
|
|
||||||
var channels = Configuration.Twitch?.Channels ?? [hermes.Username];
|
var channels = _configuration.Twitch.Channels ?? [username];
|
||||||
Logger.LogInformation("Twitch channels: " + string.Join(", ", channels));
|
_logger.LogInformation("Twitch channels: " + string.Join(", ", channels));
|
||||||
twitchapiclient.InitializeClient(hermes, channels);
|
twitchapiclient.InitializeClient(username, channels);
|
||||||
twitchapiclient.InitializePublisher();
|
twitchapiclient.InitializePublisher();
|
||||||
|
|
||||||
var handler = ServiceProvider.GetRequiredService<ChatMessageHandler>();
|
var handler = _serviceProvider.GetRequiredService<ChatMessageHandler>();
|
||||||
twitchapiclient.AddOnNewMessageReceived(async Task (object? s, OnMessageReceivedArgs e) => {
|
twitchapiclient.AddOnNewMessageReceived(async Task (object? s, OnMessageReceivedArgs e) => {
|
||||||
var result = handler.Handle(e);
|
var result = await handler.Handle(e);
|
||||||
|
|
||||||
switch (result) {
|
|
||||||
case MessageResult.Skip:
|
|
||||||
if (Playing != null) {
|
|
||||||
AudioPlaybackEngine.Instance.RemoveMixerInput(Playing);
|
|
||||||
Playing = null;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case MessageResult.SkipAll:
|
|
||||||
Player.RemoveAll();
|
|
||||||
if (Playing != null) {
|
|
||||||
AudioPlaybackEngine.Instance.RemoveMixerInput(Playing);
|
|
||||||
Playing = null;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return twitchapiclient;
|
return twitchapiclient;
|
||||||
@ -204,13 +252,13 @@ namespace TwitchChatTTS
|
|||||||
.WithNamingConvention(HyphenatedNamingConvention.Instance)
|
.WithNamingConvention(HyphenatedNamingConvention.Instance)
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
var chathandler = ServiceProvider.GetRequiredService<ChatMessageHandler>();
|
var chathandler = _serviceProvider.GetRequiredService<ChatMessageHandler>();
|
||||||
using (TextWriter writer = File.CreateText(Configuration.Emotes.CounterFilePath.Trim()))
|
using (TextWriter writer = File.CreateText(_configuration.Emotes.CounterFilePath.Trim()))
|
||||||
{
|
{
|
||||||
await writer.WriteAsync(serializer.Serialize(chathandler.EmoteCounter));
|
await writer.WriteAsync(serializer.Serialize(chathandler._emoteCounter));
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Logger.LogError(e, "Failed to save the emote counter.");
|
_logger.LogError(e, "Failed to save the emote counter.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -1,12 +1,29 @@
|
|||||||
using TwitchChatTTS.Hermes;
|
// using System.Text.RegularExpressions;
|
||||||
|
// using HermesSocketLibrary.Request.Message;
|
||||||
|
// using TwitchChatTTS.Hermes;
|
||||||
|
|
||||||
namespace TwitchChatTTS.Twitch
|
// namespace TwitchChatTTS.Twitch
|
||||||
{
|
// {
|
||||||
public class TTSContext
|
// public class TTSContext
|
||||||
{
|
// {
|
||||||
public string DefaultVoice;
|
// public string DefaultVoice;
|
||||||
public IEnumerable<TTSVoice>? EnabledVoices;
|
// public IEnumerable<TTSVoice>? EnabledVoices;
|
||||||
public IDictionary<string, TTSUsernameFilter>? UsernameFilters;
|
// public IDictionary<string, TTSUsernameFilter>? UsernameFilters;
|
||||||
public IEnumerable<TTSWordFilter>? WordFilters;
|
// public IEnumerable<TTSWordFilter>? WordFilters;
|
||||||
}
|
// public IList<VoiceDetails>? AvailableVoices { get => _availableVoices; set { _availableVoices = value; EnabledVoicesRegex = GenerateEnabledVoicesRegex(); } }
|
||||||
}
|
// public IDictionary<long, string>? SelectedVoices;
|
||||||
|
// public Regex? EnabledVoicesRegex;
|
||||||
|
|
||||||
|
// private IList<VoiceDetails>? _availableVoices;
|
||||||
|
|
||||||
|
|
||||||
|
// private Regex? GenerateEnabledVoicesRegex() {
|
||||||
|
// if (AvailableVoices == null || AvailableVoices.Count() <= 0) {
|
||||||
|
// return null;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// var enabledVoicesString = string.Join("|", AvailableVoices.Select(v => v.Name));
|
||||||
|
// return new Regex($@"\b({enabledVoicesString})\:(.*?)(?=\Z|\b(?:{enabledVoicesString})\:)", RegexOptions.IgnoreCase);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
@ -3,137 +3,160 @@ using TwitchChatTTS.Helpers;
|
|||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using TwitchChatTTS;
|
using TwitchChatTTS;
|
||||||
using TwitchLib.Api.Core.Exceptions;
|
using TwitchLib.Api.Core.Exceptions;
|
||||||
using TwitchLib.Client;
|
|
||||||
using TwitchLib.Client.Events;
|
using TwitchLib.Client.Events;
|
||||||
using TwitchLib.Client.Models;
|
using TwitchLib.Client.Models;
|
||||||
using TwitchLib.Communication.Clients;
|
|
||||||
using TwitchLib.Communication.Events;
|
using TwitchLib.Communication.Events;
|
||||||
using TwitchLib.PubSub;
|
|
||||||
using static TwitchChatTTS.Configuration;
|
using static TwitchChatTTS.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using CommonSocketLibrary.Abstract;
|
||||||
|
using CommonSocketLibrary.Common;
|
||||||
|
using TwitchLib.PubSub.Interfaces;
|
||||||
|
using TwitchLib.Client.Interfaces;
|
||||||
|
using TwitchChatTTS.OBS.Socket;
|
||||||
|
|
||||||
public class TwitchApiClient {
|
public class TwitchApiClient {
|
||||||
private TwitchBotToken Token { get; }
|
private readonly Configuration _configuration;
|
||||||
private TwitchClient Client { get; }
|
private readonly ILogger<TwitchApiClient> _logger;
|
||||||
private TwitchPubSub Publisher { get; }
|
private readonly TwitchBotToken _token;
|
||||||
private WebClientWrap Web { get; }
|
private readonly ITwitchClient _client;
|
||||||
private Configuration Configuration { get; }
|
private readonly ITwitchPubSub _publisher;
|
||||||
private ILogger<TwitchApiClient> Logger { get; }
|
private readonly WebClientWrap Web;
|
||||||
private bool Initialized { get; set; }
|
private readonly IServiceProvider _serviceProvider;
|
||||||
|
private bool Initialized;
|
||||||
|
|
||||||
|
|
||||||
public TwitchApiClient(Configuration configuration, ILogger<TwitchApiClient> logger, TwitchBotToken token) {
|
public TwitchApiClient(
|
||||||
Configuration = configuration;
|
Configuration configuration,
|
||||||
Logger = logger;
|
ILogger<TwitchApiClient> logger,
|
||||||
Client = new TwitchClient(new WebSocketClient());
|
TwitchBotToken token,
|
||||||
Publisher = new TwitchPubSub();
|
ITwitchClient twitchClient,
|
||||||
|
ITwitchPubSub twitchPublisher,
|
||||||
|
IServiceProvider serviceProvider
|
||||||
|
) {
|
||||||
|
_configuration = configuration;
|
||||||
|
_logger = logger;
|
||||||
|
_token = token;
|
||||||
|
_client = twitchClient;
|
||||||
|
_publisher = twitchPublisher;
|
||||||
|
_serviceProvider = serviceProvider;
|
||||||
Initialized = false;
|
Initialized = false;
|
||||||
Token = token;
|
|
||||||
|
|
||||||
Web = new WebClientWrap(new JsonSerializerOptions() {
|
Web = new WebClientWrap(new JsonSerializerOptions() {
|
||||||
PropertyNameCaseInsensitive = false,
|
PropertyNameCaseInsensitive = false,
|
||||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
|
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
|
||||||
});
|
});
|
||||||
if (!string.IsNullOrWhiteSpace(Configuration.Hermes?.Token))
|
if (!string.IsNullOrWhiteSpace(_configuration.Hermes?.Token))
|
||||||
Web.AddHeader("x-api-key", Configuration.Hermes?.Token);
|
Web.AddHeader("x-api-key", _configuration.Hermes.Token.Trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Authorize() {
|
public async Task Authorize() {
|
||||||
try {
|
try {
|
||||||
var authorize = await Web.GetJson<TwitchBotAuth>("https://hermes.goblincaves.com/api/account/reauthorize");
|
var authorize = await Web.GetJson<TwitchBotAuth>("https://hermes.goblincaves.com/api/account/reauthorize");
|
||||||
if (authorize != null && Token.BroadcasterId == authorize.BroadcasterId) {
|
if (authorize != null && _token.BroadcasterId == authorize.BroadcasterId) {
|
||||||
Token.AccessToken = authorize.AccessToken;
|
_token.AccessToken = authorize.AccessToken;
|
||||||
Token.RefreshToken = authorize.RefreshToken;
|
_token.RefreshToken = authorize.RefreshToken;
|
||||||
Logger.LogInformation("Updated Twitch API tokens.");
|
_logger.LogInformation("Updated Twitch API tokens.");
|
||||||
} else if (authorize != null) {
|
} else if (authorize != null) {
|
||||||
Logger.LogError("Twitch API Authorization failed.");
|
_logger.LogError("Twitch API Authorization failed.");
|
||||||
}
|
}
|
||||||
} catch (HttpResponseException e) {
|
} catch (HttpResponseException e) {
|
||||||
if (string.IsNullOrWhiteSpace(Configuration.Hermes?.Token))
|
if (string.IsNullOrWhiteSpace(_configuration.Hermes?.Token))
|
||||||
Logger.LogError("No Hermes API key found. Enter it into the configuration file.");
|
_logger.LogError("No Hermes API key found. Enter it into the configuration file.");
|
||||||
else
|
else
|
||||||
Logger.LogError("Invalid Hermes API key. Double check the token. HTTP Error Code: " + e.HttpResponse.StatusCode);
|
_logger.LogError("Invalid Hermes API key. Double check the token. HTTP Error Code: " + e.HttpResponse.StatusCode);
|
||||||
} catch (JsonException) {
|
} catch (JsonException) {
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Logger.LogError(e, "Failed to authorize to Twitch API.");
|
_logger.LogError(e, "Failed to authorize to Twitch API.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Connect() {
|
public async Task Connect() {
|
||||||
Client.Connect();
|
_client.Connect();
|
||||||
await Publisher.ConnectAsync();
|
await _publisher.ConnectAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void InitializeClient(HermesClient hermes, IEnumerable<string> channels) {
|
public void InitializeClient(string username, IEnumerable<string> channels) {
|
||||||
ConnectionCredentials credentials = new ConnectionCredentials(hermes.Username, Token?.AccessToken);
|
ConnectionCredentials credentials = new ConnectionCredentials(username, _token?.AccessToken);
|
||||||
Client.Initialize(credentials, channels.Distinct().ToList());
|
_client.Initialize(credentials, channels.Distinct().ToList());
|
||||||
|
|
||||||
if (Initialized) {
|
if (Initialized) {
|
||||||
Logger.LogDebug("Twitch API client has already been initialized.");
|
_logger.LogDebug("Twitch API client has already been initialized.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Initialized = true;
|
Initialized = true;
|
||||||
|
|
||||||
Client.OnJoinedChannel += async Task (object? s, OnJoinedChannelArgs e) => {
|
_client.OnJoinedChannel += async Task (object? s, OnJoinedChannelArgs e) => {
|
||||||
Logger.LogInformation("Joined channel: " + e.Channel);
|
_logger.LogInformation("Joined channel: " + e.Channel);
|
||||||
};
|
};
|
||||||
|
|
||||||
Client.OnConnected += async Task (object? s, OnConnectedArgs e) => {
|
_client.OnConnected += async Task (object? s, OnConnectedArgs e) => {
|
||||||
Logger.LogInformation("-----------------------------------------------------------");
|
_logger.LogInformation("-----------------------------------------------------------");
|
||||||
};
|
};
|
||||||
|
|
||||||
Client.OnIncorrectLogin += async Task (object? s, OnIncorrectLoginArgs e) => {
|
_client.OnIncorrectLogin += async Task (object? s, OnIncorrectLoginArgs e) => {
|
||||||
Logger.LogError(e.Exception, "Incorrect Login on Twitch API client.");
|
_logger.LogError(e.Exception, "Incorrect Login on Twitch API client.");
|
||||||
|
|
||||||
Logger.LogInformation("Attempting to re-authorize.");
|
_logger.LogInformation("Attempting to re-authorize.");
|
||||||
await Authorize();
|
await Authorize();
|
||||||
};
|
};
|
||||||
|
|
||||||
Client.OnConnectionError += async Task (object? s, OnConnectionErrorArgs e) => {
|
_client.OnConnectionError += async Task (object? s, OnConnectionErrorArgs e) => {
|
||||||
Logger.LogError("Connection Error: " + e.Error.Message + " (" + e.Error.GetType().Name + ")");
|
_logger.LogError("Connection Error: " + e.Error.Message + " (" + e.Error.GetType().Name + ")");
|
||||||
|
|
||||||
|
_logger.LogInformation("Attempting to re-authorize.");
|
||||||
|
await Authorize();
|
||||||
};
|
};
|
||||||
|
|
||||||
Client.OnError += async Task (object? s, OnErrorEventArgs e) => {
|
_client.OnError += async Task (object? s, OnErrorEventArgs e) => {
|
||||||
Logger.LogError(e.Exception, "Twitch API client error.");
|
_logger.LogError(e.Exception, "Twitch API client error.");
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public void InitializePublisher() {
|
public void InitializePublisher() {
|
||||||
Publisher.OnPubSubServiceConnected += async (s, e) => {
|
_publisher.OnPubSubServiceConnected += async (s, e) => {
|
||||||
Publisher.ListenToChannelPoints(Token.BroadcasterId);
|
_publisher.ListenToChannelPoints(_token.BroadcasterId);
|
||||||
Publisher.ListenToFollows(Token.BroadcasterId);
|
_publisher.ListenToFollows(_token.BroadcasterId);
|
||||||
|
|
||||||
await Publisher.SendTopicsAsync(Token.AccessToken);
|
await _publisher.SendTopicsAsync(_token.AccessToken);
|
||||||
Logger.LogInformation("Twitch PubSub has been connected.");
|
_logger.LogInformation("Twitch PubSub has been connected.");
|
||||||
};
|
};
|
||||||
|
|
||||||
Publisher.OnFollow += (s, e) => {
|
_publisher.OnFollow += (s, e) => {
|
||||||
Logger.LogInformation("Follow: " + e.DisplayName);
|
var client = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("obs") as OBSSocketClient;
|
||||||
|
if (_configuration.Twitch?.TtsWhenOffline != true && client?.Live == false)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_logger.LogInformation("Follow: " + e.DisplayName);
|
||||||
};
|
};
|
||||||
|
|
||||||
Publisher.OnChannelPointsRewardRedeemed += (s, e) => {
|
_publisher.OnChannelPointsRewardRedeemed += (s, e) => {
|
||||||
Logger.LogInformation($"Channel Point Reward Redeemed: {e.RewardRedeemed.Redemption.Reward.Title} (id: {e.RewardRedeemed.Redemption.Id})");
|
var client = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("obs") as OBSSocketClient;
|
||||||
|
if (_configuration.Twitch?.TtsWhenOffline != true && client?.Live == false)
|
||||||
|
return;
|
||||||
|
|
||||||
if (Configuration.Twitch?.Redeems is null) {
|
_logger.LogInformation($"Channel Point Reward Redeemed: {e.RewardRedeemed.Redemption.Reward.Title} (id: {e.RewardRedeemed.Redemption.Id})");
|
||||||
Logger.LogDebug("No redeems found in the configuration.");
|
|
||||||
|
if (_configuration.Twitch?.Redeems == null) {
|
||||||
|
_logger.LogDebug("No redeems found in the configuration.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var redeemName = e.RewardRedeemed.Redemption.Reward.Title.ToLower().Trim().Replace(" ", "-");
|
var redeemName = e.RewardRedeemed.Redemption.Reward.Title.ToLower().Trim().Replace(" ", "-");
|
||||||
if (!Configuration.Twitch.Redeems.TryGetValue(redeemName, out RedeemConfiguration? redeem))
|
if (!_configuration.Twitch.Redeems.TryGetValue(redeemName, out RedeemConfiguration? redeem))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (redeem is null)
|
if (redeem == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Write or append to file if needed.
|
// Write or append to file if needed.
|
||||||
var outputFile = string.IsNullOrWhiteSpace(redeem.OutputFilePath) ? null : redeem.OutputFilePath.Trim();
|
var outputFile = string.IsNullOrWhiteSpace(redeem.OutputFilePath) ? null : redeem.OutputFilePath.Trim();
|
||||||
if (outputFile is null) {
|
if (outputFile == null) {
|
||||||
Logger.LogDebug($"No output file was provided for redeem '{e.RewardRedeemed.Redemption.Reward.Title}'.");
|
_logger.LogDebug($"No output file was provided for redeem '{e.RewardRedeemed.Redemption.Reward.Title}'.");
|
||||||
} else {
|
} else {
|
||||||
var outputContent = string.IsNullOrWhiteSpace(redeem.OutputContent) ? null : redeem.OutputContent.Trim().Replace("%USER%", e.RewardRedeemed.Redemption.User.DisplayName).Replace("\\n", "\n");
|
var outputContent = string.IsNullOrWhiteSpace(redeem.OutputContent) ? null : redeem.OutputContent.Trim().Replace("%USER%", e.RewardRedeemed.Redemption.User.DisplayName).Replace("\\n", "\n");
|
||||||
if (outputContent is null) {
|
if (outputContent == null) {
|
||||||
Logger.LogWarning($"No output content was provided for redeem '{e.RewardRedeemed.Redemption.Reward.Title}'.");
|
_logger.LogWarning($"No output content was provided for redeem '{e.RewardRedeemed.Redemption.Reward.Title}'.");
|
||||||
} else {
|
} else {
|
||||||
if (redeem.OutputAppend == true) {
|
if (redeem.OutputAppend == true) {
|
||||||
File.AppendAllText(outputFile, outputContent + "\n");
|
File.AppendAllText(outputFile, outputContent + "\n");
|
||||||
@ -145,12 +168,12 @@ public class TwitchApiClient {
|
|||||||
|
|
||||||
// Play audio file if needed.
|
// Play audio file if needed.
|
||||||
var audioFile = string.IsNullOrWhiteSpace(redeem.AudioFilePath) ? null : redeem.AudioFilePath.Trim();
|
var audioFile = string.IsNullOrWhiteSpace(redeem.AudioFilePath) ? null : redeem.AudioFilePath.Trim();
|
||||||
if (audioFile is null) {
|
if (audioFile == null) {
|
||||||
Logger.LogDebug($"No audio file was provided for redeem '{e.RewardRedeemed.Redemption.Reward.Title}'.");
|
_logger.LogDebug($"No audio file was provided for redeem '{e.RewardRedeemed.Redemption.Reward.Title}'.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!File.Exists(audioFile)) {
|
if (!File.Exists(audioFile)) {
|
||||||
Logger.LogWarning($"Cannot find audio file @ {audioFile} for redeem '{e.RewardRedeemed.Redemption.Reward.Title}'.");
|
_logger.LogWarning($"Cannot find audio file @ {audioFile} for redeem '{e.RewardRedeemed.Redemption.Reward.Title}'.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -179,6 +202,6 @@ public class TwitchApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void AddOnNewMessageReceived(AsyncEventHandler<OnMessageReceivedArgs> handler) {
|
public void AddOnNewMessageReceived(AsyncEventHandler<OnMessageReceivedArgs> handler) {
|
||||||
Client.OnMessageReceived += handler;
|
_client.OnMessageReceived += handler;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -35,5 +35,6 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\CommonSocketLibrary\CommonSocketLibrary.csproj" />
|
<ProjectReference Include="..\CommonSocketLibrary\CommonSocketLibrary.csproj" />
|
||||||
|
<ProjectReference Include="..\HermesSocketLibrary\HermesSocketLibrary.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
36
User.cs
Normal file
36
User.cs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using TwitchChatTTS.Hermes;
|
||||||
|
|
||||||
|
namespace TwitchChatTTS
|
||||||
|
{
|
||||||
|
public class User
|
||||||
|
{
|
||||||
|
// Hermes user id
|
||||||
|
public string HermesUserId { get; set; }
|
||||||
|
public long TwitchUserId { get; set; }
|
||||||
|
public string TwitchUsername { get; set; }
|
||||||
|
|
||||||
|
public string DefaultTTSVoice { get; set; }
|
||||||
|
// voice id -> voice name
|
||||||
|
public IDictionary<string, string> VoicesAvailable { get; set; }
|
||||||
|
// chatter/twitch id -> voice name
|
||||||
|
public IDictionary<long, string> VoicesSelected { get; set; }
|
||||||
|
public HashSet<string> VoicesEnabled { get; set; }
|
||||||
|
|
||||||
|
public IDictionary<string, TTSUsernameFilter> ChatterFilters { get; set; }
|
||||||
|
public IList<TTSWordFilter> RegexFilters { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
public User() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public Regex? GenerateEnabledVoicesRegex() {
|
||||||
|
if (VoicesAvailable == null || VoicesAvailable.Count() <= 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var enabledVoicesString = string.Join("|", VoicesAvailable.Select(v => v.Value));
|
||||||
|
return new Regex($@"\b({enabledVoicesString})\:(.*?)(?=\Z|\b(?:{enabledVoicesString})\:)", RegexOptions.IgnoreCase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user