Using Serilog. Added partial OBS batch request support. Added update checking. Added more commands. Added enabled/disabled TTS voices. And more.
This commit is contained in:
parent
d4004d6230
commit
706cd06930
@ -3,94 +3,99 @@ using TwitchLib.Client.Events;
|
||||
using TwitchChatTTS.OBS.Socket;
|
||||
using CommonSocketLibrary.Abstract;
|
||||
using CommonSocketLibrary.Common;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Serilog;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using TwitchChatTTS;
|
||||
using TwitchChatTTS.Seven;
|
||||
using TwitchChatTTS.Chat.Commands;
|
||||
using TwitchChatTTS.Hermes.Socket;
|
||||
using HermesSocketLibrary.Socket.Data;
|
||||
|
||||
|
||||
public class ChatMessageHandler {
|
||||
private ILogger<ChatMessageHandler> _logger { get; }
|
||||
public class ChatMessageHandler
|
||||
{
|
||||
private ILogger _logger { get; }
|
||||
private Configuration _configuration { get; }
|
||||
public EmoteCounter _emoteCounter { get; }
|
||||
private EmoteDatabase _emotes { get; }
|
||||
private TTSPlayer _player { get; }
|
||||
private ChatCommandManager _commands { get; }
|
||||
private OBSSocketClient? _obsClient { get; }
|
||||
private HermesSocketClient? _hermesClient { get; }
|
||||
private IServiceProvider _serviceProvider { get; }
|
||||
|
||||
private Regex sfxRegex;
|
||||
private HashSet<long> _chatters;
|
||||
|
||||
public HashSet<long> Chatters { get => _chatters; set => _chatters = value; }
|
||||
|
||||
|
||||
public ChatMessageHandler(
|
||||
ILogger<ChatMessageHandler> logger,
|
||||
Configuration configuration,
|
||||
EmoteCounter emoteCounter,
|
||||
EmoteDatabase emotes,
|
||||
TTSPlayer player,
|
||||
ChatCommandManager commands,
|
||||
[FromKeyedServices("obs")] SocketClient<WebSocketMessage> client,
|
||||
IServiceProvider serviceProvider
|
||||
) {
|
||||
_logger = logger;
|
||||
_configuration = configuration;
|
||||
_emoteCounter = emoteCounter;
|
||||
_emotes = emotes;
|
||||
EmoteDatabase emotes,
|
||||
[FromKeyedServices("obs")] SocketClient<WebSocketMessage> obsClient,
|
||||
[FromKeyedServices("hermes")] SocketClient<WebSocketMessage> hermesClient,
|
||||
Configuration configuration,
|
||||
IServiceProvider serviceProvider,
|
||||
ILogger logger
|
||||
)
|
||||
{
|
||||
_player = player;
|
||||
_commands = commands;
|
||||
_obsClient = client as OBSSocketClient;
|
||||
_emotes = emotes;
|
||||
_obsClient = obsClient as OBSSocketClient;
|
||||
_hermesClient = hermesClient as HermesSocketClient;
|
||||
_configuration = configuration;
|
||||
_serviceProvider = serviceProvider;
|
||||
_logger = logger;
|
||||
|
||||
_chatters = null;
|
||||
sfxRegex = new Regex(@"\(([A-Za-z0-9_-]+)\)");
|
||||
}
|
||||
|
||||
|
||||
public async Task<MessageResult> Handle(OnMessageReceivedArgs e) {
|
||||
if (_configuration.Twitch?.TtsWhenOffline != true && _obsClient?.Live == false)
|
||||
return MessageResult.Blocked;
|
||||
|
||||
public async Task<MessageResult> Handle(OnMessageReceivedArgs e)
|
||||
{
|
||||
if (_obsClient == null || _hermesClient == null || _obsClient.Connected && _chatters == null)
|
||||
return new MessageResult(MessageStatus.NotReady, -1, -1);
|
||||
if (_configuration.Twitch?.TtsWhenOffline != true && _obsClient.Live == false)
|
||||
return new MessageResult(MessageStatus.NotReady, -1, -1);
|
||||
|
||||
var user = _serviceProvider.GetRequiredService<User>();
|
||||
var m = e.ChatMessage;
|
||||
var msg = e.ChatMessage.Message;
|
||||
var chatterId = long.Parse(m.UserId);
|
||||
var tasks = new List<Task>();
|
||||
|
||||
var blocked = user.ChatterFilters.TryGetValue(m.Username, out TTSUsernameFilter? filter) && filter.Tag == "blacklisted";
|
||||
|
||||
if (!blocked || m.IsBroadcaster) {
|
||||
try {
|
||||
if (!blocked || m.IsBroadcaster)
|
||||
{
|
||||
try
|
||||
{
|
||||
var commandResult = await _commands.Execute(msg, m);
|
||||
if (commandResult != ChatCommandResult.Unknown) {
|
||||
return MessageResult.Command;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
_logger.LogError(ex, "Failed at executing command.");
|
||||
if (commandResult != ChatCommandResult.Unknown)
|
||||
return new MessageResult(MessageStatus.Command, -1, -1);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Failed at executing command.");
|
||||
}
|
||||
}
|
||||
|
||||
if (blocked) {
|
||||
_logger.LogTrace($"Blocked message by {m.Username}: {msg}");
|
||||
return MessageResult.Blocked;
|
||||
if (blocked)
|
||||
{
|
||||
_logger.Debug($"Blocked message by {m.Username}: {msg}");
|
||||
return new MessageResult(MessageStatus.Blocked, -1, -1);
|
||||
}
|
||||
|
||||
// Replace filtered words.
|
||||
if (user.RegexFilters != null) {
|
||||
foreach (var wf in user.RegexFilters) {
|
||||
if (wf.Search == null || wf.Replace == null)
|
||||
continue;
|
||||
|
||||
if (wf.IsRegex) {
|
||||
try {
|
||||
var regex = new Regex(wf.Search);
|
||||
msg = regex.Replace(msg, wf.Replace);
|
||||
continue;
|
||||
} catch (Exception) {
|
||||
wf.IsRegex = false;
|
||||
}
|
||||
}
|
||||
|
||||
msg = msg.Replace(wf.Search, wf.Replace);
|
||||
}
|
||||
if (_obsClient.Connected && !_chatters.Contains(chatterId))
|
||||
{
|
||||
tasks.Add(_hermesClient.Send(6, new ChatterMessage()
|
||||
{
|
||||
Id = chatterId,
|
||||
Name = m.Username
|
||||
}));
|
||||
_chatters.Add(chatterId);
|
||||
}
|
||||
|
||||
// Filter highly repetitive words (like emotes) from the message.
|
||||
@ -99,17 +104,30 @@ public class ChatMessageHandler {
|
||||
var words = msg.Split(" ");
|
||||
var wordCounter = new Dictionary<string, int>();
|
||||
string filteredMsg = string.Empty;
|
||||
foreach (var w in words) {
|
||||
if (wordCounter.ContainsKey(w)) {
|
||||
var newEmotes = new Dictionary<string, string>();
|
||||
foreach (var w in words)
|
||||
{
|
||||
if (wordCounter.ContainsKey(w))
|
||||
{
|
||||
wordCounter[w]++;
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
wordCounter.Add(w, 1);
|
||||
}
|
||||
|
||||
var emoteId = _emotes?.Get(w);
|
||||
var emoteId = _emotes.Get(w);
|
||||
if (emoteId == null)
|
||||
{
|
||||
emoteId = m.EmoteSet.Emotes.FirstOrDefault(e => e.Name == w)?.Id;
|
||||
if (emoteId != null) {
|
||||
if (emoteId != null)
|
||||
{
|
||||
newEmotes.Add(emoteId, w);
|
||||
_emotes.Add(w, emoteId);
|
||||
}
|
||||
}
|
||||
if (emoteId != null)
|
||||
{
|
||||
emotesUsed.Add(emoteId);
|
||||
totalEmoteUsed++;
|
||||
}
|
||||
@ -117,55 +135,89 @@ public class ChatMessageHandler {
|
||||
if (wordCounter[w] <= 4 && (emoteId == null || totalEmoteUsed <= 5))
|
||||
filteredMsg += w + " ";
|
||||
}
|
||||
if (_obsClient.Connected && newEmotes.Any())
|
||||
tasks.Add(_hermesClient.Send(7, new EmoteDetailsMessage()
|
||||
{
|
||||
Emotes = newEmotes
|
||||
}));
|
||||
msg = filteredMsg;
|
||||
|
||||
// Adding twitch emotes to the counter.
|
||||
foreach (var emote in e.ChatMessage.EmoteSet.Emotes) {
|
||||
_logger.LogTrace("Twitch emote name used: " + emote.Name);
|
||||
emotesUsed.Add(emote.Id);
|
||||
// Replace filtered words.
|
||||
if (user.RegexFilters != null)
|
||||
{
|
||||
foreach (var wf in user.RegexFilters)
|
||||
{
|
||||
if (wf.Search == null || wf.Replace == null)
|
||||
continue;
|
||||
|
||||
if (wf.IsRegex)
|
||||
{
|
||||
try
|
||||
{
|
||||
var regex = new Regex(wf.Search);
|
||||
msg = regex.Replace(msg, wf.Replace);
|
||||
continue;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
wf.IsRegex = false;
|
||||
}
|
||||
}
|
||||
|
||||
msg = msg.Replace(wf.Search, wf.Replace);
|
||||
}
|
||||
}
|
||||
|
||||
if (long.TryParse(e.ChatMessage.UserId, out long userId))
|
||||
_emoteCounter.Add(userId, emotesUsed);
|
||||
if (emotesUsed.Any())
|
||||
_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;
|
||||
if (m.IsStaff) {
|
||||
if (m.IsStaff)
|
||||
{
|
||||
priority = int.MinValue;
|
||||
} else if (filter?.Tag == "priority") {
|
||||
}
|
||||
else if (filter?.Tag == "priority")
|
||||
{
|
||||
priority = int.MinValue + 1;
|
||||
} else if (m.IsModerator) {
|
||||
}
|
||||
else if (m.IsModerator)
|
||||
{
|
||||
priority = -100;
|
||||
} else if (m.IsVip) {
|
||||
}
|
||||
else if (m.IsVip)
|
||||
{
|
||||
priority = -10;
|
||||
} else if (m.IsPartner) {
|
||||
}
|
||||
else if (m.IsPartner)
|
||||
{
|
||||
priority = -5;
|
||||
} else if (m.IsHighlighted) {
|
||||
}
|
||||
else if (m.IsHighlighted)
|
||||
{
|
||||
priority = -1;
|
||||
}
|
||||
priority = Math.Min(priority, -m.SubscribedMonthCount * (m.IsSubscriber ? 2 : 1));
|
||||
|
||||
// Determine voice selected.
|
||||
string voiceSelected = user.DefaultTTSVoice;
|
||||
if (user.VoicesSelected?.ContainsKey(userId) == true) {
|
||||
if (long.TryParse(e.ChatMessage.UserId, out long userId) && user.VoicesSelected?.ContainsKey(userId) == true)
|
||||
{
|
||||
var voiceId = user.VoicesSelected[userId];
|
||||
if (user.VoicesAvailable.TryGetValue(voiceId, out string? voiceName) && voiceName != null) {
|
||||
if (user.VoicesAvailable.TryGetValue(voiceId, out string? voiceName) && voiceName != null)
|
||||
{
|
||||
voiceSelected = voiceName;
|
||||
}
|
||||
}
|
||||
|
||||
// Determine additional voices used
|
||||
var voicesRegex = user.GenerateEnabledVoicesRegex();
|
||||
var matches = voicesRegex?.Matches(msg).ToArray();
|
||||
if (matches == null || matches.FirstOrDefault() == null || matches.FirstOrDefault().Index == 0) {
|
||||
var matches = user.WordFilterRegex?.Matches(msg).ToArray();
|
||||
if (matches == null || matches.FirstOrDefault() == null || matches.First().Index < 0)
|
||||
{
|
||||
HandlePartialMessage(priority, voiceSelected, msg.Trim(), e);
|
||||
return MessageResult.None;
|
||||
return new MessageResult(MessageStatus.None, user.TwitchUserId, chatterId, emotesUsed);
|
||||
}
|
||||
|
||||
HandlePartialMessage(priority, voiceSelected, msg.Substring(0, matches.FirstOrDefault().Index).Trim(), e);
|
||||
foreach (Match match in matches) {
|
||||
HandlePartialMessage(priority, voiceSelected, msg.Substring(0, matches.First().Index).Trim(), e);
|
||||
foreach (Match match in matches)
|
||||
{
|
||||
var message = match.Groups[2].ToString();
|
||||
if (string.IsNullOrWhiteSpace(message))
|
||||
continue;
|
||||
@ -175,21 +227,28 @@ public class ChatMessageHandler {
|
||||
HandlePartialMessage(priority, voice, message.Trim(), e);
|
||||
}
|
||||
|
||||
return MessageResult.None;
|
||||
if (tasks.Any())
|
||||
await Task.WhenAll(tasks);
|
||||
|
||||
return new MessageResult(MessageStatus.None, user.TwitchUserId, chatterId, emotesUsed);
|
||||
}
|
||||
|
||||
private void HandlePartialMessage(int priority, string voice, string message, OnMessageReceivedArgs e) {
|
||||
if (string.IsNullOrWhiteSpace(message)) {
|
||||
private void HandlePartialMessage(int priority, string voice, string message, OnMessageReceivedArgs e)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(message))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var m = e.ChatMessage;
|
||||
var parts = sfxRegex.Split(message);
|
||||
var badgesString = string.Join(", ", e.ChatMessage.Badges.Select(b => b.Key + " = " + b.Value));
|
||||
|
||||
if (parts.Length == 1) {
|
||||
_logger.LogInformation($"Voice: {voice}; Priority: {priority}; Message: {message}; Month: {m.SubscribedMonthCount}; {badgesString}");
|
||||
_player.Add(new TTSMessage() {
|
||||
|
||||
if (parts.Length == 1)
|
||||
{
|
||||
_logger.Information($"Username: {m.Username}; User ID: {m.UserId}; Voice: {voice}; Priority: {priority}; Message: {message}; Month: {m.SubscribedMonthCount}; {badgesString}");
|
||||
_player.Add(new TTSMessage()
|
||||
{
|
||||
Voice = voice,
|
||||
Message = message,
|
||||
Moderator = m.IsModerator,
|
||||
@ -205,18 +264,22 @@ public class ChatMessageHandler {
|
||||
var sfxMatches = sfxRegex.Matches(message);
|
||||
var sfxStart = sfxMatches.FirstOrDefault()?.Index ?? message.Length;
|
||||
|
||||
for (var i = 0; i < sfxMatches.Count; i++) {
|
||||
for (var i = 0; i < sfxMatches.Count; i++)
|
||||
{
|
||||
var sfxMatch = sfxMatches[i];
|
||||
var sfxName = sfxMatch.Groups[1]?.ToString()?.ToLower();
|
||||
|
||||
if (!File.Exists("sfx/" + sfxName + ".mp3")) {
|
||||
if (!File.Exists("sfx/" + sfxName + ".mp3"))
|
||||
{
|
||||
parts[i * 2 + 2] = parts[i * 2] + " (" + parts[i * 2 + 1] + ")" + parts[i * 2 + 2];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(parts[i * 2])) {
|
||||
_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() {
|
||||
if (!string.IsNullOrWhiteSpace(parts[i * 2]))
|
||||
{
|
||||
_logger.Information($"Username: {m.Username}; User ID: {m.UserId}; Voice: {voice}; Priority: {priority}; Message: {parts[i * 2]}; Month: {m.SubscribedMonthCount}; {badgesString}");
|
||||
_player.Add(new TTSMessage()
|
||||
{
|
||||
Voice = voice,
|
||||
Message = parts[i * 2],
|
||||
Moderator = m.IsModerator,
|
||||
@ -228,8 +291,9 @@ public class ChatMessageHandler {
|
||||
});
|
||||
}
|
||||
|
||||
_logger.LogInformation($"Username: {m.Username}; User ID: {m.UserId}; Voice: {voice}; Priority: {priority}; SFX: {sfxName}; Month: {m.SubscribedMonthCount}; {badgesString}");
|
||||
_player.Add(new TTSMessage() {
|
||||
_logger.Information($"Username: {m.Username}; User ID: {m.UserId}; Voice: {voice}; Priority: {priority}; SFX: {sfxName}; Month: {m.SubscribedMonthCount}; {badgesString}");
|
||||
_player.Add(new TTSMessage()
|
||||
{
|
||||
Voice = voice,
|
||||
Message = sfxName,
|
||||
File = $"sfx/{sfxName}.mp3",
|
||||
@ -242,9 +306,11 @@ public class ChatMessageHandler {
|
||||
});
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(parts.Last())) {
|
||||
_logger.LogInformation($"Username: {m.Username}; User ID: {m.UserId}; Voice: {voice}; Priority: {priority}; Message: {parts.Last()}; Month: {m.SubscribedMonthCount}; {badgesString}");
|
||||
_player.Add(new TTSMessage() {
|
||||
if (!string.IsNullOrWhiteSpace(parts.Last()))
|
||||
{
|
||||
_logger.Information($"Username: {m.Username}; User ID: {m.UserId}; Voice: {voice}; Priority: {priority}; Message: {parts.Last()}; Month: {m.SubscribedMonthCount}; {badgesString}");
|
||||
_player.Add(new TTSMessage()
|
||||
{
|
||||
Voice = voice,
|
||||
Message = parts.Last(),
|
||||
Moderator = m.IsModerator,
|
||||
|
@ -2,7 +2,7 @@ using CommonSocketLibrary.Abstract;
|
||||
using CommonSocketLibrary.Common;
|
||||
using HermesSocketLibrary.Socket.Data;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Serilog;
|
||||
using TwitchChatTTS.Chat.Commands.Parameters;
|
||||
using TwitchLib.Client.Models;
|
||||
|
||||
@ -11,13 +11,14 @@ namespace TwitchChatTTS.Chat.Commands
|
||||
public class AddTTSVoiceCommand : ChatCommand
|
||||
{
|
||||
private IServiceProvider _serviceProvider;
|
||||
private ILogger<AddTTSVoiceCommand> _logger;
|
||||
private ILogger _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.") {
|
||||
ILogger logger
|
||||
) : base("addttsvoice", "Select a TTS voice as the default for that user.")
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
_logger = logger;
|
||||
|
||||
@ -26,7 +27,7 @@ namespace TwitchChatTTS.Chat.Commands
|
||||
|
||||
public override async Task<bool> CheckPermissions(ChatMessage message, long broadcasterId)
|
||||
{
|
||||
return message.IsModerator || message.IsBroadcaster || message.UserId == "126224566";
|
||||
return message.IsModerator || message.IsBroadcaster;
|
||||
}
|
||||
|
||||
public override async Task Execute(IList<string> args, ChatMessage message, long broadcasterId)
|
||||
@ -43,12 +44,13 @@ namespace TwitchChatTTS.Chat.Commands
|
||||
var exists = context.VoicesAvailable.Any(v => v.Value.ToLower() == voiceNameLower);
|
||||
if (exists)
|
||||
return;
|
||||
|
||||
await client.Send(3, new RequestMessage() {
|
||||
|
||||
await client.Send(3, new RequestMessage()
|
||||
{
|
||||
Type = "create_tts_voice",
|
||||
Data = new Dictionary<string, string>() { { "@voice", voiceName } }
|
||||
Data = new Dictionary<string, object>() { { "voice", voiceName } }
|
||||
});
|
||||
_logger.LogInformation($"Added a new TTS voice by {message.Username} (id: {message.UserId}): {voiceName}.");
|
||||
_logger.Information($"Added a new TTS voice by {message.Username} (id: {message.UserId}): {voiceName}.");
|
||||
}
|
||||
}
|
||||
}
|
@ -10,15 +10,18 @@ namespace TwitchChatTTS.Chat.Commands
|
||||
public IList<ChatCommandParameter> Parameters { get => _parameters.AsReadOnly(); }
|
||||
private IList<ChatCommandParameter> _parameters;
|
||||
|
||||
public ChatCommand(string name, string description) {
|
||||
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);
|
||||
protected void AddParameter(ChatCommandParameter parameter, bool optional = false)
|
||||
{
|
||||
if (parameter != null && parameter.Clone() is ChatCommandParameter p) {
|
||||
_parameters.Add(optional ? p.Permissive() : p);
|
||||
}
|
||||
}
|
||||
|
||||
public abstract Task<bool> CheckPermissions(ChatMessage message, long broadcasterId);
|
||||
|
@ -1,5 +1,5 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Serilog;
|
||||
using TwitchLib.Client.Models;
|
||||
|
||||
namespace TwitchChatTTS.Chat.Commands
|
||||
@ -7,13 +7,14 @@ namespace TwitchChatTTS.Chat.Commands
|
||||
public class ChatCommandManager
|
||||
{
|
||||
private IDictionary<string, ChatCommand> _commands;
|
||||
private TwitchBotToken _token;
|
||||
private TwitchBotAuth _token;
|
||||
private IServiceProvider _serviceProvider;
|
||||
private ILogger<ChatCommandManager> _logger;
|
||||
private ILogger _logger;
|
||||
private string CommandStartSign { get; } = "!";
|
||||
|
||||
|
||||
public ChatCommandManager(TwitchBotToken token, IServiceProvider serviceProvider, ILogger<ChatCommandManager> logger) {
|
||||
public ChatCommandManager(TwitchBotAuth token, IServiceProvider serviceProvider, ILogger logger)
|
||||
{
|
||||
_token = token;
|
||||
_serviceProvider = serviceProvider;
|
||||
_logger = logger;
|
||||
@ -22,33 +23,38 @@ namespace TwitchChatTTS.Chat.Commands
|
||||
GenerateCommands();
|
||||
}
|
||||
|
||||
private void Add(ChatCommand command) {
|
||||
private void Add(ChatCommand command)
|
||||
{
|
||||
_commands.Add(command.Name.ToLower(), command);
|
||||
}
|
||||
|
||||
private void GenerateCommands() {
|
||||
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) {
|
||||
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);
|
||||
if (command == null)
|
||||
{
|
||||
_logger.Error("Failed to add command: " + type.AssemblyQualifiedName);
|
||||
continue;
|
||||
}
|
||||
|
||||
_logger.LogDebug($"Added command {type.AssemblyQualifiedName}.");
|
||||
|
||||
_logger.Debug($"Added command {type.AssemblyQualifiedName}.");
|
||||
Add(command);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ChatCommandResult> Execute(string arg, ChatMessage message) {
|
||||
public async Task<ChatCommandResult> Execute(string arg, ChatMessage message)
|
||||
{
|
||||
if (_token.BroadcasterId == null)
|
||||
return ChatCommandResult.Unknown;
|
||||
if (string.IsNullOrWhiteSpace(arg))
|
||||
@ -64,36 +70,44 @@ namespace TwitchChatTTS.Chat.Commands
|
||||
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}'.");
|
||||
if (!_commands.TryGetValue(com, out ChatCommand? command) || command == null)
|
||||
{
|
||||
_logger.Debug($"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}'.");
|
||||
if (!await command.CheckPermissions(message, broadcasterId) && message.UserId != "126224566" && !message.IsStaff)
|
||||
{
|
||||
_logger.Warning($"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)}");
|
||||
if (command.Parameters.Count(p => !p.Optional) > args.Length)
|
||||
{
|
||||
_logger.Warning($"Command syntax issue when executing command named '{com}' with the following args: {string.Join(" ", args)}");
|
||||
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]}");
|
||||
for (int i = 0; i < Math.Min(args.Length, command.Parameters.Count); i++)
|
||||
{
|
||||
if (!command.Parameters[i].Validate(args[i]))
|
||||
{
|
||||
_logger.Warning($"Commmand '{com}' failed because of the #{i + 1} argument. Invalid value: {args[i]}");
|
||||
return ChatCommandResult.Syntax;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
try
|
||||
{
|
||||
await command.Execute(args, message, broadcasterId);
|
||||
} catch (Exception e) {
|
||||
_logger.LogError(e, $"Command '{arg}' failed.");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Error(e, $"Command '{arg}' failed.");
|
||||
return ChatCommandResult.Fail;
|
||||
}
|
||||
|
||||
_logger.LogInformation($"Execute the {com} command with the following args: " + string.Join(" ", args));
|
||||
_logger.Information($"Executed the {com} command with the following args: " + string.Join(" ", args));
|
||||
return ChatCommandResult.Success;
|
||||
}
|
||||
}
|
||||
|
81
Chat/Commands/OBSCommand.cs
Normal file
81
Chat/Commands/OBSCommand.cs
Normal file
@ -0,0 +1,81 @@
|
||||
using CommonSocketLibrary.Abstract;
|
||||
using CommonSocketLibrary.Common;
|
||||
using HermesSocketLibrary.Socket.Data;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Serilog;
|
||||
using TwitchChatTTS.Chat.Commands.Parameters;
|
||||
using TwitchLib.Client.Models;
|
||||
|
||||
namespace TwitchChatTTS.Chat.Commands
|
||||
{
|
||||
public class OBSCommand : ChatCommand
|
||||
{
|
||||
private IServiceProvider _serviceProvider;
|
||||
private ILogger _logger;
|
||||
|
||||
public OBSCommand(
|
||||
[FromKeyedServices("parameter-unvalidated")] ChatCommandParameter unvalidatedParameter,
|
||||
IServiceProvider serviceProvider,
|
||||
ILogger logger
|
||||
) : base("obs", "Various obs commands.")
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
_logger = logger;
|
||||
|
||||
AddParameter(unvalidatedParameter);
|
||||
}
|
||||
|
||||
public override async Task<bool> CheckPermissions(ChatMessage message, long broadcasterId)
|
||||
{
|
||||
return message.IsModerator || message.IsBroadcaster;
|
||||
}
|
||||
|
||||
public override async Task Execute(IList<string> args, ChatMessage message, long broadcasterId)
|
||||
{
|
||||
var client = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("obs");
|
||||
if (client == null)
|
||||
return;
|
||||
var context = _serviceProvider.GetRequiredService<User>();
|
||||
if (context == null || context.VoicesAvailable == null)
|
||||
return;
|
||||
|
||||
var voiceName = args[0].ToLower();
|
||||
var voiceId = context.VoicesAvailable.FirstOrDefault(v => v.Value.ToLower() == voiceName).Key;
|
||||
var action = args[1].ToLower();
|
||||
|
||||
switch (action) {
|
||||
case "sleep":
|
||||
await client.Send(8, new RequestMessage()
|
||||
{
|
||||
Type = "Sleep",
|
||||
Data = new Dictionary<string, object>() { { "requestId", "siduhsidasd" }, { "sleepMillis", 10000 } }
|
||||
});
|
||||
break;
|
||||
case "get_scene_item_id":
|
||||
await client.Send(6, new RequestMessage()
|
||||
{
|
||||
Type = "GetSceneItemId",
|
||||
Data = new Dictionary<string, object>() { { "sceneName", "Generic" }, { "sourceName", "ABCDEF" }, { "rotation", 90 } }
|
||||
});
|
||||
break;
|
||||
case "transform":
|
||||
await client.Send(6, new RequestMessage()
|
||||
{
|
||||
Type = "Transform",
|
||||
Data = new Dictionary<string, object>() { { "sceneName", "Generic" }, { "sceneItemId", 90 }, { "rotation", 90 } }
|
||||
});
|
||||
break;
|
||||
case "remove":
|
||||
await client.Send(3, new RequestMessage()
|
||||
{
|
||||
Type = "delete_tts_voice",
|
||||
Data = new Dictionary<string, object>() { { "voice", voiceId } }
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
_logger.Information($"Added a new TTS voice by {message.Username} (id: {message.UserId}): {voiceName}.");
|
||||
}
|
||||
}
|
||||
}
|
@ -1,17 +1,27 @@
|
||||
namespace TwitchChatTTS.Chat.Commands.Parameters
|
||||
{
|
||||
public abstract class ChatCommandParameter
|
||||
public abstract class ChatCommandParameter : ICloneable
|
||||
{
|
||||
public string Name { get; }
|
||||
public string Description { get; }
|
||||
public bool Optional { get; }
|
||||
public bool Optional { get; private set; }
|
||||
|
||||
public ChatCommandParameter(string name, string description, bool optional = false) {
|
||||
public ChatCommandParameter(string name, string description, bool optional = false)
|
||||
{
|
||||
Name = name;
|
||||
Description = description;
|
||||
Optional = optional;
|
||||
}
|
||||
|
||||
public abstract bool Validate(string value);
|
||||
|
||||
public object Clone() {
|
||||
return (ChatCommandParameter) MemberwiseClone();
|
||||
}
|
||||
|
||||
public ChatCommandParameter Permissive() {
|
||||
Optional = true;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
61
Chat/Commands/RefreshTTSDataCommand.cs
Normal file
61
Chat/Commands/RefreshTTSDataCommand.cs
Normal file
@ -0,0 +1,61 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Serilog;
|
||||
using TwitchLib.Client.Models;
|
||||
|
||||
namespace TwitchChatTTS.Chat.Commands
|
||||
{
|
||||
public class RefreshTTSDataCommand : ChatCommand
|
||||
{
|
||||
private IServiceProvider _serviceProvider;
|
||||
private ILogger _logger;
|
||||
|
||||
public RefreshTTSDataCommand(IServiceProvider serviceProvider, ILogger logger)
|
||||
: base("refresh", "Refreshes certain TTS related data on the client.")
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public override async Task<bool> CheckPermissions(ChatMessage message, long broadcasterId)
|
||||
{
|
||||
return message.IsModerator || message.IsBroadcaster;
|
||||
}
|
||||
|
||||
public override async Task Execute(IList<string> args, ChatMessage message, long broadcasterId)
|
||||
{
|
||||
var user = _serviceProvider.GetRequiredService<User>();
|
||||
var service = args.FirstOrDefault();
|
||||
if (service == null)
|
||||
return;
|
||||
|
||||
var hermes = _serviceProvider.GetRequiredService<HermesApiClient>();
|
||||
|
||||
switch (service)
|
||||
{
|
||||
case "tts_voice_enabled":
|
||||
var voicesEnabled = await hermes.FetchTTSEnabledVoices();
|
||||
if (voicesEnabled == null || !voicesEnabled.Any())
|
||||
user.VoicesEnabled = new HashSet<string>(new string[] { "Brian" });
|
||||
else
|
||||
user.VoicesEnabled = new HashSet<string>(voicesEnabled.Select(v => v));
|
||||
_logger.Information($"{user.VoicesEnabled.Count} TTS voices have been enabled.");
|
||||
break;
|
||||
case "word_filters":
|
||||
var wordFilters = await hermes.FetchTTSWordFilters();
|
||||
user.RegexFilters = wordFilters.ToList();
|
||||
_logger.Information($"{user.RegexFilters.Count()} TTS word filters.");
|
||||
break;
|
||||
case "username_filters":
|
||||
var usernameFilters = await hermes.FetchTTSUsernameFilters();
|
||||
user.ChatterFilters = usernameFilters.ToDictionary(e => e.Username, e => e);
|
||||
_logger.Information($"{user.ChatterFilters.Where(f => f.Value.Tag == "blacklisted").Count()} username(s) have been blocked.");
|
||||
_logger.Information($"{user.ChatterFilters.Where(f => f.Value.Tag == "priority").Count()} user(s) have been prioritized.");
|
||||
break;
|
||||
case "default_voice":
|
||||
user.DefaultTTSVoice = await hermes.FetchTTSDefaultVoice();
|
||||
_logger.Information("Default Voice: " + user.DefaultTTSVoice);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -2,7 +2,7 @@ using CommonSocketLibrary.Abstract;
|
||||
using CommonSocketLibrary.Common;
|
||||
using HermesSocketLibrary.Socket.Data;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Serilog;
|
||||
using TwitchChatTTS.Chat.Commands.Parameters;
|
||||
using TwitchLib.Client.Models;
|
||||
|
||||
@ -11,13 +11,14 @@ namespace TwitchChatTTS.Chat.Commands
|
||||
public class RemoveTTSVoiceCommand : ChatCommand
|
||||
{
|
||||
private IServiceProvider _serviceProvider;
|
||||
private ILogger<RemoveTTSVoiceCommand> _logger;
|
||||
private ILogger _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.") {
|
||||
ILogger logger
|
||||
) : base("removettsvoice", "Select a TTS voice as the default for that user.")
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
_logger = logger;
|
||||
|
||||
@ -26,7 +27,7 @@ namespace TwitchChatTTS.Chat.Commands
|
||||
|
||||
public override async Task<bool> CheckPermissions(ChatMessage message, long broadcasterId)
|
||||
{
|
||||
return message.IsModerator || message.IsBroadcaster || message.UserId == "126224566";
|
||||
return message.IsModerator || message.IsBroadcaster;
|
||||
}
|
||||
|
||||
public override async Task Execute(IList<string> args, ChatMessage message, long broadcasterId)
|
||||
@ -42,13 +43,14 @@ namespace TwitchChatTTS.Chat.Commands
|
||||
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() {
|
||||
|
||||
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 } }
|
||||
Data = new Dictionary<string, object>() { { "voice", voiceId } }
|
||||
});
|
||||
_logger.LogInformation($"Deleted a TTS voice by {message.Username} (id: {message.UserId}): {voiceName}.");
|
||||
_logger.Information($"Deleted a TTS voice by {message.Username} (id: {message.UserId}): {voiceName}.");
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Serilog;
|
||||
using TwitchLib.Client.Models;
|
||||
|
||||
namespace TwitchChatTTS.Chat.Commands
|
||||
@ -7,10 +7,11 @@ namespace TwitchChatTTS.Chat.Commands
|
||||
public class SkipAllCommand : ChatCommand
|
||||
{
|
||||
private IServiceProvider _serviceProvider;
|
||||
private ILogger<SkipAllCommand> _logger;
|
||||
private ILogger _logger;
|
||||
|
||||
public SkipAllCommand(IServiceProvider serviceProvider, ILogger<SkipAllCommand> logger)
|
||||
: base("skipall", "Skips all text to speech messages in queue and playing.") {
|
||||
public SkipAllCommand(IServiceProvider serviceProvider, ILogger logger)
|
||||
: base("skipall", "Skips all text to speech messages in queue and playing.")
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
_logger = logger;
|
||||
}
|
||||
@ -27,11 +28,11 @@ namespace TwitchChatTTS.Chat.Commands
|
||||
|
||||
if (player.Playing == null)
|
||||
return;
|
||||
|
||||
|
||||
AudioPlaybackEngine.Instance.RemoveMixerInput(player.Playing);
|
||||
player.Playing = null;
|
||||
|
||||
_logger.LogInformation("Skipped all queued and playing tts.");
|
||||
_logger.Information("Skipped all queued and playing tts.");
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Serilog;
|
||||
using TwitchLib.Client.Models;
|
||||
|
||||
namespace TwitchChatTTS.Chat.Commands
|
||||
@ -7,10 +7,11 @@ namespace TwitchChatTTS.Chat.Commands
|
||||
public class SkipCommand : ChatCommand
|
||||
{
|
||||
private IServiceProvider _serviceProvider;
|
||||
private ILogger<SkipCommand> _logger;
|
||||
private ILogger _logger;
|
||||
|
||||
public SkipCommand(IServiceProvider serviceProvider, ILogger<SkipCommand> logger)
|
||||
: base("skip", "Skips the current text to speech message.") {
|
||||
public SkipCommand(IServiceProvider serviceProvider, ILogger logger)
|
||||
: base("skip", "Skips the current text to speech message.")
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
_logger = logger;
|
||||
}
|
||||
@ -25,11 +26,11 @@ namespace TwitchChatTTS.Chat.Commands
|
||||
var player = _serviceProvider.GetRequiredService<TTSPlayer>();
|
||||
if (player.Playing == null)
|
||||
return;
|
||||
|
||||
|
||||
AudioPlaybackEngine.Instance.RemoveMixerInput(player.Playing);
|
||||
player.Playing = null;
|
||||
|
||||
_logger.LogInformation("Skipped current tts.");
|
||||
_logger.Information("Skipped current tts.");
|
||||
}
|
||||
}
|
||||
}
|
76
Chat/Commands/TTSCommand.cs
Normal file
76
Chat/Commands/TTSCommand.cs
Normal file
@ -0,0 +1,76 @@
|
||||
using CommonSocketLibrary.Abstract;
|
||||
using CommonSocketLibrary.Common;
|
||||
using HermesSocketLibrary.Socket.Data;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Serilog;
|
||||
using TwitchChatTTS.Chat.Commands.Parameters;
|
||||
using TwitchLib.Client.Models;
|
||||
|
||||
namespace TwitchChatTTS.Chat.Commands
|
||||
{
|
||||
public class TTSCommand : ChatCommand
|
||||
{
|
||||
private IServiceProvider _serviceProvider;
|
||||
private ILogger _logger;
|
||||
|
||||
public TTSCommand(
|
||||
[FromKeyedServices("parameter-ttsvoicename")] ChatCommandParameter ttsVoiceParameter,
|
||||
[FromKeyedServices("parameter-unvalidated")] ChatCommandParameter unvalidatedParameter,
|
||||
IServiceProvider serviceProvider,
|
||||
ILogger logger
|
||||
) : base("tts", "Various tts commands.")
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
_logger = logger;
|
||||
|
||||
AddParameter(ttsVoiceParameter);
|
||||
AddParameter(unvalidatedParameter);
|
||||
}
|
||||
|
||||
public override async Task<bool> CheckPermissions(ChatMessage message, long broadcasterId)
|
||||
{
|
||||
return message.IsModerator || message.IsBroadcaster;
|
||||
}
|
||||
|
||||
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[0].ToLower();
|
||||
var voiceId = context.VoicesAvailable.FirstOrDefault(v => v.Value.ToLower() == voiceName).Key;
|
||||
var action = args[1].ToLower();
|
||||
|
||||
switch (action) {
|
||||
case "enable":
|
||||
await client.Send(3, new RequestMessage()
|
||||
{
|
||||
Type = "update_tts_voice_state",
|
||||
Data = new Dictionary<string, object>() { { "voice", voiceId }, { "state", true } }
|
||||
});
|
||||
break;
|
||||
case "disable":
|
||||
await client.Send(3, new RequestMessage()
|
||||
{
|
||||
Type = "update_tts_voice_state",
|
||||
Data = new Dictionary<string, object>() { { "voice", voiceId }, { "state", false } }
|
||||
});
|
||||
break;
|
||||
case "remove":
|
||||
await client.Send(3, new RequestMessage()
|
||||
{
|
||||
Type = "delete_tts_voice",
|
||||
Data = new Dictionary<string, object>() { { "voice", voiceId } }
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
_logger.Information($"Added a new TTS voice by {message.Username} (id: {message.UserId}): {voiceName}.");
|
||||
}
|
||||
}
|
||||
}
|
26
Chat/Commands/VersionCommand.cs
Normal file
26
Chat/Commands/VersionCommand.cs
Normal file
@ -0,0 +1,26 @@
|
||||
using Serilog;
|
||||
using TwitchLib.Client.Models;
|
||||
|
||||
namespace TwitchChatTTS.Chat.Commands
|
||||
{
|
||||
public class VersionCommand : ChatCommand
|
||||
{
|
||||
private ILogger _logger;
|
||||
|
||||
public VersionCommand(ILogger logger)
|
||||
: base("version", "Does nothing.")
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public override async Task<bool> CheckPermissions(ChatMessage message, long broadcasterId)
|
||||
{
|
||||
return message.IsBroadcaster;
|
||||
}
|
||||
|
||||
public override async Task Execute(IList<string> args, ChatMessage message, long broadcasterId)
|
||||
{
|
||||
_logger.Information($"Version: {TTS.MAJOR_VERSION}.{TTS.MINOR_VERSION}");
|
||||
}
|
||||
}
|
||||
}
|
@ -2,7 +2,7 @@ using CommonSocketLibrary.Abstract;
|
||||
using CommonSocketLibrary.Common;
|
||||
using HermesSocketLibrary.Socket.Data;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Serilog;
|
||||
using TwitchChatTTS.Chat.Commands.Parameters;
|
||||
using TwitchLib.Client.Models;
|
||||
|
||||
@ -11,13 +11,14 @@ namespace TwitchChatTTS.Chat.Commands
|
||||
public class VoiceCommand : ChatCommand
|
||||
{
|
||||
private IServiceProvider _serviceProvider;
|
||||
private ILogger<VoiceCommand> _logger;
|
||||
private ILogger _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.") {
|
||||
ILogger logger
|
||||
) : base("voice", "Select a TTS voice as the default for that user.")
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
_logger = logger;
|
||||
|
||||
@ -26,7 +27,7 @@ namespace TwitchChatTTS.Chat.Commands
|
||||
|
||||
public override async Task<bool> CheckPermissions(ChatMessage message, long broadcasterId)
|
||||
{
|
||||
return message.IsModerator || message.IsBroadcaster || message.IsSubscriber || message.Bits >= 100 || message.UserId == "126224566";
|
||||
return message.IsModerator || message.IsBroadcaster || message.IsSubscriber || message.Bits >= 100;
|
||||
}
|
||||
|
||||
public override async Task Execute(IList<string> args, ChatMessage message, long broadcasterId)
|
||||
@ -42,19 +43,12 @@ namespace TwitchChatTTS.Chat.Commands
|
||||
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}).");
|
||||
}
|
||||
await client.Send(3, new RequestMessage()
|
||||
{
|
||||
Type = context.VoicesSelected.ContainsKey(chatterId) ? "update_tts_user" : "create_tts_user",
|
||||
Data = new Dictionary<string, object>() { { "chatter", chatterId }, { "voice", voice.Key } }
|
||||
});
|
||||
_logger.Information($"Updated {message.Username}'s [id: {chatterId}] tts voice to {voice.Value} (id: {voice.Key}).");
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,22 @@
|
||||
public enum MessageResult {
|
||||
public class MessageResult
|
||||
{
|
||||
public MessageStatus Status;
|
||||
public long BroadcasterId;
|
||||
public long ChatterId;
|
||||
public HashSet<string> Emotes;
|
||||
|
||||
|
||||
public MessageResult(MessageStatus status, long broadcasterId, long chatterId, HashSet<string>? emotes = null)
|
||||
{
|
||||
Status = status;
|
||||
BroadcasterId = broadcasterId;
|
||||
ChatterId = chatterId;
|
||||
Emotes = emotes ?? new HashSet<string>();
|
||||
}
|
||||
}
|
||||
|
||||
public enum MessageStatus
|
||||
{
|
||||
None = 0,
|
||||
NotReady = 1,
|
||||
Blocked = 2,
|
||||
|
@ -14,7 +14,7 @@ public class AudioPlaybackEngine : IDisposable
|
||||
{
|
||||
SampleRate = sampleRate;
|
||||
outputDevice = new WaveOutEvent();
|
||||
|
||||
|
||||
mixer = new MixingSampleProvider(WaveFormat.CreateIeeeFloatWaveFormat(sampleRate, channelCount));
|
||||
mixer.ReadFully = true;
|
||||
|
||||
@ -49,25 +49,41 @@ public class AudioPlaybackEngine : IDisposable
|
||||
AddMixerInput(new CachedWavProvider(sound));
|
||||
}
|
||||
|
||||
public ISampleProvider ConvertSound(IWaveProvider provider) {
|
||||
public ISampleProvider ConvertSound(IWaveProvider provider)
|
||||
{
|
||||
ISampleProvider? converted = null;
|
||||
if (provider.WaveFormat.Encoding == WaveFormatEncoding.Pcm) {
|
||||
if (provider.WaveFormat.BitsPerSample == 8) {
|
||||
if (provider.WaveFormat.Encoding == WaveFormatEncoding.Pcm)
|
||||
{
|
||||
if (provider.WaveFormat.BitsPerSample == 8)
|
||||
{
|
||||
converted = new Pcm8BitToSampleProvider(provider);
|
||||
} else if (provider.WaveFormat.BitsPerSample == 16) {
|
||||
}
|
||||
else if (provider.WaveFormat.BitsPerSample == 16)
|
||||
{
|
||||
converted = new Pcm16BitToSampleProvider(provider);
|
||||
} else if (provider.WaveFormat.BitsPerSample == 24) {
|
||||
}
|
||||
else if (provider.WaveFormat.BitsPerSample == 24)
|
||||
{
|
||||
converted = new Pcm24BitToSampleProvider(provider);
|
||||
} else if (provider.WaveFormat.BitsPerSample == 32) {
|
||||
}
|
||||
else if (provider.WaveFormat.BitsPerSample == 32)
|
||||
{
|
||||
converted = new Pcm32BitToSampleProvider(provider);
|
||||
}
|
||||
} else if (provider.WaveFormat.Encoding == WaveFormatEncoding.IeeeFloat) {
|
||||
if (provider.WaveFormat.BitsPerSample == 64) {
|
||||
}
|
||||
else if (provider.WaveFormat.Encoding == WaveFormatEncoding.IeeeFloat)
|
||||
{
|
||||
if (provider.WaveFormat.BitsPerSample == 64)
|
||||
{
|
||||
converted = new WaveToSampleProvider64(provider);
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
converted = new WaveToSampleProvider(provider);
|
||||
}
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException("Unsupported source encoding while adding to mixer.");
|
||||
}
|
||||
return ConvertToRightChannelCount(converted);
|
||||
@ -83,15 +99,18 @@ public class AudioPlaybackEngine : IDisposable
|
||||
mixer.AddMixerInput(input);
|
||||
}
|
||||
|
||||
public void RemoveMixerInput(ISampleProvider sound) {
|
||||
public void RemoveMixerInput(ISampleProvider sound)
|
||||
{
|
||||
mixer.RemoveMixerInput(sound);
|
||||
}
|
||||
|
||||
public void AddOnMixerInputEnded(EventHandler<SampleProviderEventArgs> e) {
|
||||
public void AddOnMixerInputEnded(EventHandler<SampleProviderEventArgs> e)
|
||||
{
|
||||
mixer.MixerInputEnded += e;
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
public void Dispose()
|
||||
{
|
||||
outputDevice.Dispose();
|
||||
}
|
||||
}
|
@ -7,12 +7,14 @@ public class NetworkWavSound
|
||||
|
||||
public NetworkWavSound(string uri)
|
||||
{
|
||||
using (var mfr = new MediaFoundationReader(uri)) {
|
||||
using (var mfr = new MediaFoundationReader(uri))
|
||||
{
|
||||
WaveFormat = mfr.WaveFormat;
|
||||
|
||||
byte[] buffer = new byte[4096];
|
||||
int read = 0;
|
||||
using (var ms = new MemoryStream()) {
|
||||
using (var ms = new MemoryStream())
|
||||
{
|
||||
while ((read = mfr.Read(buffer, 0, buffer.Length)) > 0)
|
||||
ms.Write(buffer, 0, read);
|
||||
AudioData = ms.ToArray();
|
||||
|
@ -1,6 +1,7 @@
|
||||
using NAudio.Wave;
|
||||
|
||||
public class TTSPlayer {
|
||||
public class TTSPlayer
|
||||
{
|
||||
private PriorityQueue<TTSMessage, int> _messages; // ready to play
|
||||
private PriorityQueue<TTSMessage, int> _buffer;
|
||||
private Mutex _mutex;
|
||||
@ -8,77 +9,105 @@ public class TTSPlayer {
|
||||
|
||||
public ISampleProvider? Playing { get; set; }
|
||||
|
||||
public TTSPlayer() {
|
||||
public TTSPlayer()
|
||||
{
|
||||
_messages = new PriorityQueue<TTSMessage, int>();
|
||||
_buffer = new PriorityQueue<TTSMessage, int>();
|
||||
_mutex = new Mutex();
|
||||
_mutex2 = new Mutex();
|
||||
}
|
||||
|
||||
public void Add(TTSMessage message) {
|
||||
try {
|
||||
public void Add(TTSMessage message)
|
||||
{
|
||||
try
|
||||
{
|
||||
_mutex2.WaitOne();
|
||||
_buffer.Enqueue(message, message.Priority);
|
||||
} finally {
|
||||
}
|
||||
finally
|
||||
{
|
||||
_mutex2.ReleaseMutex();
|
||||
}
|
||||
}
|
||||
|
||||
public TTSMessage? ReceiveReady() {
|
||||
try {
|
||||
public TTSMessage? ReceiveReady()
|
||||
{
|
||||
try
|
||||
{
|
||||
_mutex.WaitOne();
|
||||
if (_messages.TryDequeue(out TTSMessage? message, out int _)) {
|
||||
if (_messages.TryDequeue(out TTSMessage? message, out int _))
|
||||
{
|
||||
return message;
|
||||
}
|
||||
return null;
|
||||
} finally {
|
||||
}
|
||||
finally
|
||||
{
|
||||
_mutex.ReleaseMutex();
|
||||
}
|
||||
}
|
||||
|
||||
public TTSMessage? ReceiveBuffer() {
|
||||
try {
|
||||
public TTSMessage? ReceiveBuffer()
|
||||
{
|
||||
try
|
||||
{
|
||||
_mutex2.WaitOne();
|
||||
if (_buffer.TryDequeue(out TTSMessage? message, out int _)) {
|
||||
if (_buffer.TryDequeue(out TTSMessage? message, out int _))
|
||||
{
|
||||
return message;
|
||||
}
|
||||
return null;
|
||||
} finally {
|
||||
}
|
||||
finally
|
||||
{
|
||||
_mutex2.ReleaseMutex();
|
||||
}
|
||||
}
|
||||
|
||||
public void Ready(TTSMessage message) {
|
||||
try {
|
||||
public void Ready(TTSMessage message)
|
||||
{
|
||||
try
|
||||
{
|
||||
_mutex.WaitOne();
|
||||
_messages.Enqueue(message, message.Priority);
|
||||
} finally {
|
||||
}
|
||||
finally
|
||||
{
|
||||
_mutex.ReleaseMutex();
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveAll() {
|
||||
try {
|
||||
public void RemoveAll()
|
||||
{
|
||||
try
|
||||
{
|
||||
_mutex2.WaitOne();
|
||||
_buffer.Clear();
|
||||
} finally {
|
||||
}
|
||||
finally
|
||||
{
|
||||
_mutex2.ReleaseMutex();
|
||||
}
|
||||
|
||||
try {
|
||||
try
|
||||
{
|
||||
_mutex.WaitOne();
|
||||
_messages.Clear();
|
||||
} finally {
|
||||
}
|
||||
finally
|
||||
{
|
||||
_mutex.ReleaseMutex();
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsEmpty() {
|
||||
public bool IsEmpty()
|
||||
{
|
||||
return _messages.Count == 0;
|
||||
}
|
||||
}
|
||||
|
||||
public class TTSMessage {
|
||||
public class TTSMessage
|
||||
{
|
||||
public string? Voice { get; set; }
|
||||
public string? Channel { get; set; }
|
||||
public string? Username { get; set; }
|
||||
|
@ -2,11 +2,11 @@ namespace TwitchChatTTS
|
||||
{
|
||||
public class Configuration
|
||||
{
|
||||
public string Environment = "PROD";
|
||||
|
||||
public HermesConfiguration? Hermes;
|
||||
public TwitchConfiguration? Twitch;
|
||||
public EmotesConfiguration? Emotes;
|
||||
public OBSConfiguration? Obs;
|
||||
public SevenConfiguration? Seven;
|
||||
|
||||
|
||||
public class HermesConfiguration {
|
||||
@ -26,18 +26,10 @@ namespace TwitchChatTTS
|
||||
public bool? OutputAppend;
|
||||
}
|
||||
|
||||
public class EmotesConfiguration {
|
||||
public string? CounterFilePath;
|
||||
}
|
||||
|
||||
public class OBSConfiguration {
|
||||
public string? Host;
|
||||
public short? Port;
|
||||
public string? Password;
|
||||
}
|
||||
|
||||
public class SevenConfiguration {
|
||||
public string? UserId;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,38 +1,46 @@
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace TwitchChatTTS.Helpers {
|
||||
public class WebClientWrap {
|
||||
namespace TwitchChatTTS.Helpers
|
||||
{
|
||||
public class WebClientWrap
|
||||
{
|
||||
private HttpClient _client;
|
||||
private JsonSerializerOptions _options;
|
||||
|
||||
|
||||
public WebClientWrap(JsonSerializerOptions options) {
|
||||
public WebClientWrap(JsonSerializerOptions options)
|
||||
{
|
||||
_client = new HttpClient();
|
||||
_options = options;
|
||||
}
|
||||
|
||||
|
||||
public void AddHeader(string key, string? value) {
|
||||
public void AddHeader(string key, string? value)
|
||||
{
|
||||
if (_client.DefaultRequestHeaders.Contains(key))
|
||||
_client.DefaultRequestHeaders.Remove(key);
|
||||
_client.DefaultRequestHeaders.Add(key, value);
|
||||
}
|
||||
|
||||
public async Task<T?> GetJson<T>(string uri) {
|
||||
public async Task<T?> GetJson<T>(string uri)
|
||||
{
|
||||
var response = await _client.GetAsync(uri);
|
||||
return JsonSerializer.Deserialize<T>(await response.Content.ReadAsStreamAsync(), _options);
|
||||
}
|
||||
|
||||
public async Task<HttpResponseMessage> Get(string uri) {
|
||||
public async Task<HttpResponseMessage> Get(string uri)
|
||||
{
|
||||
return await _client.GetAsync(uri);
|
||||
}
|
||||
|
||||
public async Task<HttpResponseMessage> Post<T>(string uri, T data) {
|
||||
public async Task<HttpResponseMessage> Post<T>(string uri, T data)
|
||||
{
|
||||
return await _client.PostAsJsonAsync(uri, data);
|
||||
}
|
||||
|
||||
public async Task<HttpResponseMessage> Post(string uri) {
|
||||
public async Task<HttpResponseMessage> Post(string uri)
|
||||
{
|
||||
return await _client.PostAsJsonAsync(uri, new object());
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,4 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
[Serializable]
|
||||
public class Account {
|
||||
[AllowNull]
|
||||
public string Id { get; set; }
|
||||
[AllowNull]
|
||||
public string Username { get; set; }
|
||||
}
|
@ -1,31 +1,44 @@
|
||||
using TwitchChatTTS.Helpers;
|
||||
using TwitchChatTTS;
|
||||
using TwitchChatTTS.Hermes;
|
||||
using System.Text.Json;
|
||||
using HermesSocketLibrary.Requests.Messages;
|
||||
using TwitchChatTTS.Hermes;
|
||||
|
||||
public class HermesClient {
|
||||
public class HermesApiClient
|
||||
{
|
||||
private WebClientWrap _web;
|
||||
|
||||
public HermesClient(Configuration configuration) {
|
||||
if (string.IsNullOrWhiteSpace(configuration.Hermes?.Token)) {
|
||||
public HermesApiClient(Configuration configuration)
|
||||
{
|
||||
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.");
|
||||
}
|
||||
|
||||
_web = new WebClientWrap(new JsonSerializerOptions() {
|
||||
_web = new WebClientWrap(new JsonSerializerOptions()
|
||||
{
|
||||
PropertyNameCaseInsensitive = false,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
|
||||
});
|
||||
_web.AddHeader("x-api-key", configuration.Hermes.Token);
|
||||
}
|
||||
|
||||
public async Task<Account> FetchHermesAccountDetails() {
|
||||
public async Task<TTSVersion> GetTTSVersion()
|
||||
{
|
||||
var version = await _web.GetJson<TTSVersion>("https://hermes.goblincaves.com/api/info/version");
|
||||
return version;
|
||||
}
|
||||
|
||||
public async Task<Account> FetchHermesAccountDetails()
|
||||
{
|
||||
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");
|
||||
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.");
|
||||
@ -33,7 +46,8 @@ public class HermesClient {
|
||||
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");
|
||||
if (filters == null)
|
||||
throw new Exception("Failed to fetch TTS username filters from Hermes.");
|
||||
@ -41,23 +55,35 @@ public class HermesClient {
|
||||
return filters;
|
||||
}
|
||||
|
||||
public async Task<string> FetchTTSDefaultVoice() {
|
||||
var data = await _web.GetJson<TTSVoice>("https://hermes.goblincaves.com/api/settings/tts/default");
|
||||
public async Task<string> FetchTTSDefaultVoice()
|
||||
{
|
||||
var data = await _web.GetJson<string>("https://hermes.goblincaves.com/api/settings/tts/default");
|
||||
if (data == null)
|
||||
throw new Exception("Failed to fetch TTS default voice from Hermes.");
|
||||
|
||||
return data.Label;
|
||||
return data;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<TTSVoice>> FetchTTSEnabledVoices() {
|
||||
var voices = await _web.GetJson<IEnumerable<TTSVoice>>("https://hermes.goblincaves.com/api/settings/tts");
|
||||
public async Task<IEnumerable<TTSChatterSelectedVoice>> FetchTTSChatterSelectedVoices()
|
||||
{
|
||||
var voices = await _web.GetJson<IEnumerable<TTSChatterSelectedVoice>>("https://hermes.goblincaves.com/api/settings/tts/selected");
|
||||
if (voices == null)
|
||||
throw new Exception("Failed to fetch TTS chatter selected voices from Hermes.");
|
||||
|
||||
return voices;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<string>> FetchTTSEnabledVoices()
|
||||
{
|
||||
var voices = await _web.GetJson<IEnumerable<string>>("https://hermes.goblincaves.com/api/settings/tts");
|
||||
if (voices == null)
|
||||
throw new Exception("Failed to fetch TTS enabled voices from Hermes.");
|
||||
|
||||
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");
|
||||
if (filters == null)
|
||||
throw new Exception("Failed to fetch TTS word filters from Hermes.");
|
||||
|
@ -1,7 +1,7 @@
|
||||
using CommonSocketLibrary.Abstract;
|
||||
using CommonSocketLibrary.Common;
|
||||
using HermesSocketLibrary.Socket.Data;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Serilog;
|
||||
|
||||
namespace TwitchChatTTS.Hermes.Socket.Handlers
|
||||
{
|
||||
@ -10,7 +10,8 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
|
||||
private ILogger _logger { get; }
|
||||
public int OperationCode { get; set; } = 0;
|
||||
|
||||
public HeartbeatHandler(ILogger<HeartbeatHandler> logger) {
|
||||
public HeartbeatHandler(ILogger logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@ -18,18 +19,22 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
|
||||
{
|
||||
if (message is not HeartbeatMessage obj || obj == null)
|
||||
return;
|
||||
|
||||
if (sender is not HermesSocketClient client) {
|
||||
|
||||
if (sender is not HermesSocketClient client)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogTrace("Received heartbeat.");
|
||||
_logger.Verbose("Received heartbeat.");
|
||||
|
||||
client.LastHeartbeat = DateTime.UtcNow;
|
||||
client.LastHeartbeatReceived = DateTime.UtcNow;
|
||||
|
||||
await sender.Send(0, new HeartbeatMessage() {
|
||||
DateTime = DateTime.UtcNow
|
||||
});
|
||||
if (obj.Respond)
|
||||
await sender.Send(0, new HeartbeatMessage()
|
||||
{
|
||||
DateTime = DateTime.UtcNow,
|
||||
Respond = false
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -1,16 +1,20 @@
|
||||
using CommonSocketLibrary.Abstract;
|
||||
using CommonSocketLibrary.Common;
|
||||
using HermesSocketLibrary.Socket.Data;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Serilog;
|
||||
|
||||
namespace TwitchChatTTS.Hermes.Socket.Handlers
|
||||
{
|
||||
public class LoginAckHandler : IWebSocketHandler
|
||||
{
|
||||
private ILogger _logger { get; }
|
||||
private IServiceProvider _serviceProvider;
|
||||
private ILogger _logger;
|
||||
public int OperationCode { get; set; } = 2;
|
||||
|
||||
public LoginAckHandler(ILogger<LoginAckHandler> logger) {
|
||||
public LoginAckHandler(IServiceProvider serviceProvider, ILogger logger)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@ -18,17 +22,45 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
|
||||
{
|
||||
if (message is not LoginAckMessage obj || obj == null)
|
||||
return;
|
||||
|
||||
if (sender is not HermesSocketClient client) {
|
||||
|
||||
if (sender is not HermesSocketClient client)
|
||||
return;
|
||||
|
||||
if (obj.AnotherClient)
|
||||
{
|
||||
_logger.Warning("Another client has connected to the same account.");
|
||||
}
|
||||
else
|
||||
{
|
||||
var user = _serviceProvider.GetRequiredService<User>();
|
||||
client.UserId = obj.UserId;
|
||||
_logger.Information($"Logged in as {user.TwitchUsername} (id: {client.UserId}).");
|
||||
}
|
||||
|
||||
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}.");
|
||||
}
|
||||
await client.Send(3, new RequestMessage()
|
||||
{
|
||||
Type = "get_tts_voices",
|
||||
Data = null
|
||||
});
|
||||
|
||||
var token = _serviceProvider.GetRequiredService<User>();
|
||||
await client.Send(3, new RequestMessage()
|
||||
{
|
||||
Type = "get_tts_users",
|
||||
Data = new Dictionary<string, object>() { { "user", token.HermesUserId } }
|
||||
});
|
||||
|
||||
await client.Send(3, new RequestMessage()
|
||||
{
|
||||
Type = "get_chatter_ids",
|
||||
Data = null
|
||||
});
|
||||
|
||||
await client.Send(3, new RequestMessage()
|
||||
{
|
||||
Type = "get_emotes",
|
||||
Data = null
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -5,7 +5,8 @@ using CommonSocketLibrary.Common;
|
||||
using HermesSocketLibrary.Requests.Messages;
|
||||
using HermesSocketLibrary.Socket.Data;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Serilog;
|
||||
using TwitchChatTTS.Seven;
|
||||
|
||||
namespace TwitchChatTTS.Hermes.Socket.Handlers
|
||||
{
|
||||
@ -14,9 +15,13 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly JsonSerializerOptions _options;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
private readonly object _voicesAvailableLock = new object();
|
||||
|
||||
public int OperationCode { get; set; } = 4;
|
||||
|
||||
public RequestAckHandler(IServiceProvider serviceProvider, JsonSerializerOptions options, ILogger<RequestAckHandler> logger) {
|
||||
public RequestAckHandler(IServiceProvider serviceProvider, JsonSerializerOptions options, ILogger logger)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
_options = options;
|
||||
_logger = logger;
|
||||
@ -32,74 +37,144 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
|
||||
if (context == null)
|
||||
return;
|
||||
|
||||
if (obj.Request.Type == "get_tts_voices") {
|
||||
_logger.LogDebug("Updating all available voices.");
|
||||
if (obj.Request.Type == "get_tts_voices")
|
||||
{
|
||||
_logger.Verbose("Updating all available voices for TTS.");
|
||||
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))
|
||||
|
||||
lock (_voicesAvailableLock)
|
||||
{
|
||||
context.VoicesAvailable = voices.ToDictionary(e => e.Id, e => e.Name);
|
||||
}
|
||||
_logger.Information("Updated all available voices for TTS.");
|
||||
}
|
||||
else if (obj.Request.Type == "create_tts_user")
|
||||
{
|
||||
_logger.Verbose("Adding new tts voice for user.");
|
||||
if (!long.TryParse(obj.Request.Data["user"].ToString(), out long chatterId))
|
||||
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))
|
||||
string userId = obj.Request.Data["user"].ToString();
|
||||
string voice = obj.Request.Data["voice"].ToString();
|
||||
|
||||
context.VoicesSelected.Add(chatterId, voice);
|
||||
_logger.Information($"Added new TTS voice [voice: {voice}] for user [user id: {userId}]");
|
||||
}
|
||||
else if (obj.Request.Type == "update_tts_user")
|
||||
{
|
||||
_logger.Verbose("Updating user's voice");
|
||||
if (!long.TryParse(obj.Request.Data["chatter"].ToString(), out long chatterId))
|
||||
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 userId = obj.Request.Data["user"].ToString();
|
||||
string voice = obj.Request.Data["voice"].ToString();
|
||||
|
||||
context.VoicesSelected[chatterId] = voice;
|
||||
_logger.Information($"Updated TTS voice [voice: {voice}] for user [user id: {userId}]");
|
||||
}
|
||||
else if (obj.Request.Type == "create_tts_voice")
|
||||
{
|
||||
_logger.Verbose("Creating new tts voice.");
|
||||
string? voice = obj.Request.Data["voice"].ToString();
|
||||
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.");
|
||||
lock (_voicesAvailableLock)
|
||||
{
|
||||
var list = context.VoicesAvailable.ToDictionary(k => k.Key, v => v.Value);
|
||||
list.Add(voiceId, voice);
|
||||
context.VoicesAvailable = list;
|
||||
}
|
||||
_logger.Information($"Created new tts voice [voice: {voice}][id: {voiceId}].");
|
||||
}
|
||||
else if (obj.Request.Type == "delete_tts_voice")
|
||||
{
|
||||
_logger.Verbose("Deleting tts voice.");
|
||||
var voice = obj.Request.Data["voice"].ToString();
|
||||
if (!context.VoicesAvailable.TryGetValue(voice, out string voiceName) || voiceName == null)
|
||||
return;
|
||||
|
||||
var voice = obj.Request.Data["@voice"];
|
||||
if (!context.VoicesAvailable.TryGetValue(voice, out string voiceName) || voiceName == null) {
|
||||
return;
|
||||
lock (_voicesAvailableLock)
|
||||
{
|
||||
var dict = context.VoicesAvailable.ToDictionary(k => k.Key, v => v.Value);
|
||||
dict.Remove(voice);
|
||||
context.VoicesAvailable.Remove(voice);
|
||||
}
|
||||
|
||||
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) {
|
||||
_logger.Information($"Deleted a voice [voice: {voiceName}]");
|
||||
}
|
||||
else if (obj.Request.Type == "update_tts_voice")
|
||||
{
|
||||
_logger.Verbose("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.");
|
||||
_logger.Information($"Updated TTS voice [voice: {voice}][id: {voiceId}]");
|
||||
}
|
||||
else if (obj.Request.Type == "get_tts_users")
|
||||
{
|
||||
_logger.Verbose("Updating 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.");
|
||||
_logger.Information($"Updated {temp.Count()} chatters' selected voice.");
|
||||
}
|
||||
else if (obj.Request.Type == "get_chatter_ids")
|
||||
{
|
||||
_logger.Verbose("Fetching all chatters' id.");
|
||||
var chatters = JsonSerializer.Deserialize<IEnumerable<long>>(obj.Data.ToString(), _options);
|
||||
if (chatters == null)
|
||||
return;
|
||||
|
||||
var client = _serviceProvider.GetRequiredService<ChatMessageHandler>();
|
||||
client.Chatters = [.. chatters];
|
||||
_logger.Information($"Fetched {chatters.Count()} chatters' id.");
|
||||
}
|
||||
else if (obj.Request.Type == "get_emotes")
|
||||
{
|
||||
_logger.Verbose("Updating emotes.");
|
||||
var emotes = JsonSerializer.Deserialize<IEnumerable<EmoteInfo>>(obj.Data.ToString(), _options);
|
||||
if (emotes == null)
|
||||
return;
|
||||
|
||||
var emoteDb = _serviceProvider.GetRequiredService<EmoteDatabase>();
|
||||
var count = 0;
|
||||
foreach (var emote in emotes)
|
||||
{
|
||||
if (emoteDb.Get(emote.Name) == null)
|
||||
{
|
||||
emoteDb.Add(emote.Name, emote.Id);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
_logger.Information($"Fetched {count} emotes from various sources.");
|
||||
}
|
||||
else if (obj.Request.Type == "update_tts_voice_state")
|
||||
{
|
||||
_logger.Verbose("Updating TTS voice states.");
|
||||
string voiceId = obj.Request.Data["voice"].ToString();
|
||||
bool state = obj.Request.Data["state"].ToString() == "true";
|
||||
|
||||
if (!context.VoicesAvailable.TryGetValue(voiceId, out string voiceName) || voiceName == null)
|
||||
{
|
||||
_logger.Warning($"Failed to find voice [id: {voiceId}]");
|
||||
return;
|
||||
}
|
||||
|
||||
if (state)
|
||||
context.VoicesEnabled.Add(voiceId);
|
||||
else
|
||||
context.VoicesEnabled.Remove(voiceId);
|
||||
_logger.Information($"Updated voice state [voice: {voiceName}][new state: {(state ? "enabled" : "disabled")}]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,24 +1,108 @@
|
||||
using System.Text.Json;
|
||||
using System.Timers;
|
||||
using CommonSocketLibrary.Abstract;
|
||||
using CommonSocketLibrary.Common;
|
||||
using HermesSocketLibrary.Socket.Data;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Serilog;
|
||||
|
||||
namespace TwitchChatTTS.Hermes.Socket
|
||||
{
|
||||
public class HermesSocketClient : WebSocketClient {
|
||||
public DateTime LastHeartbeat { get; set; }
|
||||
public string? UserId { get; set; }
|
||||
public class HermesSocketClient : WebSocketClient
|
||||
{
|
||||
private Configuration _configuration;
|
||||
public DateTime LastHeartbeatReceived { get; set; }
|
||||
public DateTime LastHeartbeatSent { get; set; }
|
||||
public string? UserId { get; set; }
|
||||
private System.Timers.Timer _heartbeatTimer;
|
||||
private System.Timers.Timer _reconnectTimer;
|
||||
|
||||
public HermesSocketClient(
|
||||
ILogger<HermesSocketClient> logger,
|
||||
Configuration configuration,
|
||||
[FromKeyedServices("hermes")] HandlerManager<WebSocketClient, IWebSocketHandler> handlerManager,
|
||||
[FromKeyedServices("hermes")] HandlerTypeManager<WebSocketClient, IWebSocketHandler> typeManager
|
||||
) : base(logger, handlerManager, typeManager, new JsonSerializerOptions() {
|
||||
[FromKeyedServices("hermes")] HandlerTypeManager<WebSocketClient, IWebSocketHandler> typeManager,
|
||||
ILogger logger
|
||||
) : base(logger, handlerManager, typeManager, new JsonSerializerOptions()
|
||||
{
|
||||
PropertyNameCaseInsensitive = false,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
|
||||
}) {
|
||||
|
||||
})
|
||||
{
|
||||
_configuration = configuration;
|
||||
|
||||
_heartbeatTimer = new System.Timers.Timer(TimeSpan.FromSeconds(15));
|
||||
_heartbeatTimer.Elapsed += async (sender, e) => await HandleHeartbeat(e);
|
||||
|
||||
_reconnectTimer = new System.Timers.Timer(TimeSpan.FromSeconds(15));
|
||||
_reconnectTimer.Elapsed += async (sender, e) => await Reconnect(e);
|
||||
|
||||
LastHeartbeatReceived = LastHeartbeatSent = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
protected override async Task OnConnection()
|
||||
{
|
||||
_heartbeatTimer.Enabled = true;
|
||||
}
|
||||
|
||||
private async Task HandleHeartbeat(ElapsedEventArgs e)
|
||||
{
|
||||
var signalTime = e.SignalTime.ToUniversalTime();
|
||||
|
||||
if (signalTime - LastHeartbeatReceived > TimeSpan.FromSeconds(60))
|
||||
{
|
||||
if (LastHeartbeatReceived > LastHeartbeatSent)
|
||||
{
|
||||
_logger.Verbose("Sending heartbeat...");
|
||||
LastHeartbeatSent = DateTime.UtcNow;
|
||||
try
|
||||
{
|
||||
await Send(0, new HeartbeatMessage() { DateTime = LastHeartbeatSent });
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
}
|
||||
else if (signalTime - LastHeartbeatReceived > TimeSpan.FromSeconds(120))
|
||||
{
|
||||
try
|
||||
{
|
||||
await DisconnectAsync();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
UserId = null;
|
||||
_heartbeatTimer.Enabled = false;
|
||||
|
||||
_logger.Information("Logged off due to disconnection. Attempting to reconnect...");
|
||||
_reconnectTimer.Enabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task Reconnect(ElapsedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
await ConnectAsync($"wss://hermes-ws.goblincaves.com");
|
||||
Connected = true;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (Connected)
|
||||
{
|
||||
_logger.Information("Reconnected.");
|
||||
_reconnectTimer.Enabled = false;
|
||||
_heartbeatTimer.Enabled = true;
|
||||
LastHeartbeatReceived = DateTime.UtcNow;
|
||||
|
||||
if (_configuration.Hermes?.Token != null)
|
||||
await Send(1, new HermesLoginMessage() { ApiKey = _configuration.Hermes.Token });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,35 +1,41 @@
|
||||
using CommonSocketLibrary.Common;
|
||||
using CommonSocketLibrary.Socket.Manager;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Serilog;
|
||||
|
||||
namespace TwitchChatTTS.Hermes.Socket.Managers
|
||||
{
|
||||
public class HermesHandlerManager : WebSocketHandlerManager
|
||||
{
|
||||
public HermesHandlerManager(ILogger<HermesHandlerManager> logger, IServiceProvider provider) : base(logger) {
|
||||
public HermesHandlerManager(ILogger logger, IServiceProvider provider) : base(logger)
|
||||
{
|
||||
//Add(provider.GetRequiredService<HeartbeatHandler>());
|
||||
try {
|
||||
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) {
|
||||
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);
|
||||
if (handler == null)
|
||||
{
|
||||
logger.Error("Failed to find hermes websocket handler: " + type.AssemblyQualifiedName);
|
||||
continue;
|
||||
}
|
||||
|
||||
Logger.LogDebug($"Linked type {type.AssemblyQualifiedName} to hermes websocket handlers.");
|
||||
|
||||
Logger.Debug($"Linked type {type.AssemblyQualifiedName} to hermes websocket handlers.");
|
||||
Add(handler);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Logger.LogError(e, "Failed to load hermes websocket handler types.");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error(e, "Failed to load hermes websocket handler types.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,14 +3,14 @@ using CommonSocketLibrary.Abstract;
|
||||
using CommonSocketLibrary.Common;
|
||||
using CommonSocketLibrary.Socket.Manager;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Serilog;
|
||||
|
||||
namespace TwitchChatTTS.Hermes.Socket.Managers
|
||||
{
|
||||
public class HermesHandlerTypeManager : WebSocketHandlerTypeManager
|
||||
{
|
||||
public HermesHandlerTypeManager(
|
||||
ILogger<HermesHandlerTypeManager> factory,
|
||||
ILogger factory,
|
||||
[FromKeyedServices("hermes")] HandlerManager<WebSocketClient, IWebSocketHandler> handlers
|
||||
) : base(factory, handlers)
|
||||
{
|
||||
@ -20,12 +20,12 @@ namespace TwitchChatTTS.Hermes.Socket.Managers
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
10
Hermes/TTSVersion.cs
Normal file
10
Hermes/TTSVersion.cs
Normal file
@ -0,0 +1,10 @@
|
||||
namespace TwitchChatTTS.Hermes
|
||||
{
|
||||
public class TTSVersion
|
||||
{
|
||||
public int MajorVersion { get; set; }
|
||||
public int MinorVersion { get; set; }
|
||||
public string Download { get; set; }
|
||||
public string Changelog { get; set; }
|
||||
}
|
||||
}
|
@ -1,6 +1,13 @@
|
||||
public class TTSVoice {
|
||||
public string Label { get; set; }
|
||||
public int Value { get; set; }
|
||||
public string? Gender { get; set; }
|
||||
public string? Language { get; set; }
|
||||
public class TTSVoice
|
||||
{
|
||||
public string Label { get; set; }
|
||||
public int Value { get; set; }
|
||||
public string? Gender { get; set; }
|
||||
public string? Language { get; set; }
|
||||
}
|
||||
|
||||
public class TTSChatterSelectedVoice
|
||||
{
|
||||
public long ChatterId { get; set; }
|
||||
public string Voice { get; set; }
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
namespace TwitchChatTTS.Hermes
|
||||
{
|
||||
public class TTSWordFilter
|
||||
{
|
||||
public string? Id { get; set; }
|
||||
public string? Search { get; set; }
|
||||
public string? Replace { get; set; }
|
||||
public string? UserId { get; set; }
|
||||
|
||||
public bool IsRegex { get; set; }
|
||||
|
||||
|
||||
public TTSWordFilter() {
|
||||
IsRegex = true;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
[Serializable]
|
||||
public class TwitchBotAuth {
|
||||
public string? UserId { get; set; }
|
||||
public string? AccessToken { get; set; }
|
||||
public string? RefreshToken { get; set; }
|
||||
public string? BroadcasterId { get; set; }
|
||||
public string? UserId { get; set; }
|
||||
public string? AccessToken { get; set; }
|
||||
public string? RefreshToken { get; set; }
|
||||
public string? BroadcasterId { get; set; }
|
||||
}
|
@ -1,8 +1,7 @@
|
||||
[Serializable]
|
||||
public class TwitchBotToken {
|
||||
public string? ClientId { get; set; }
|
||||
public string? ClientSecret { get; set; }
|
||||
public string? AccessToken { get; set; }
|
||||
public string? RefreshToken { get; set; }
|
||||
public string? BroadcasterId { get; set; }
|
||||
public string? ClientId { get; set; }
|
||||
public string? ClientSecret { get; set; }
|
||||
public string? AccessToken { get; set; }
|
||||
public string? RefreshToken { get; set; }
|
||||
public string? BroadcasterId { get; set; }
|
||||
}
|
@ -1,8 +1,7 @@
|
||||
[Serializable]
|
||||
public class TwitchConnection {
|
||||
public string? Id { get; set; }
|
||||
public string? Secret { get; set; }
|
||||
public string? BroadcasterId { get; set; }
|
||||
public string? Username { get; set; }
|
||||
public string? UserId { get; set; }
|
||||
public string? Id { get; set; }
|
||||
public string? Secret { get; set; }
|
||||
public string? BroadcasterId { get; set; }
|
||||
public string? Username { get; set; }
|
||||
public string? UserId { get; set; }
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
namespace TwitchChatTTS.OBS.Socket.Context
|
||||
{
|
||||
[Serializable]
|
||||
public class HelloContext
|
||||
{
|
||||
public string? Host { get; set; }
|
||||
|
@ -1,6 +1,5 @@
|
||||
namespace TwitchChatTTS.OBS.Socket.Data
|
||||
{
|
||||
[Serializable]
|
||||
public class EventMessage
|
||||
{
|
||||
public string EventType { get; set; }
|
||||
|
@ -1,6 +1,5 @@
|
||||
namespace TwitchChatTTS.OBS.Socket.Data
|
||||
{
|
||||
[Serializable]
|
||||
public class HelloMessage
|
||||
{
|
||||
public string ObsWebSocketVersion { get; set; }
|
||||
|
@ -1,6 +1,5 @@
|
||||
namespace TwitchChatTTS.OBS.Socket.Data
|
||||
{
|
||||
[Serializable]
|
||||
public class IdentifiedMessage
|
||||
{
|
||||
public int NegotiatedRpcVersion { get; set; }
|
||||
|
@ -1,13 +1,13 @@
|
||||
namespace TwitchChatTTS.OBS.Socket.Data
|
||||
{
|
||||
[Serializable]
|
||||
public class IdentifyMessage
|
||||
{
|
||||
public int RpcVersion { get; set; }
|
||||
public string? Authentication { 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;
|
||||
Authentication = auth;
|
||||
EventSubscriptions = subscriptions;
|
||||
|
25
OBS/Socket/Data/RequestBatchMessage.cs
Normal file
25
OBS/Socket/Data/RequestBatchMessage.cs
Normal file
@ -0,0 +1,25 @@
|
||||
namespace TwitchChatTTS.OBS.Socket.Data
|
||||
{
|
||||
public class RequestBatchMessage
|
||||
{
|
||||
public string RequestId { get; set; }
|
||||
public bool HaltOnFailure { get; set; }
|
||||
public RequestBatchExecutionType ExecutionType { get; set; }
|
||||
public IEnumerable<object> Requests { get; set;}
|
||||
|
||||
public RequestBatchMessage(string id, IEnumerable<object> requests, bool haltOnFailure = false, RequestBatchExecutionType executionType = RequestBatchExecutionType.SerialRealtime)
|
||||
{
|
||||
RequestId = id;
|
||||
Requests = requests;
|
||||
HaltOnFailure = haltOnFailure;
|
||||
ExecutionType = executionType;
|
||||
}
|
||||
}
|
||||
|
||||
public enum RequestBatchExecutionType {
|
||||
None = -1,
|
||||
SerialRealtime = 0,
|
||||
SerialFrame = 1,
|
||||
Parallel = 2
|
||||
}
|
||||
}
|
8
OBS/Socket/Data/RequestBatchResponseMessage.cs
Normal file
8
OBS/Socket/Data/RequestBatchResponseMessage.cs
Normal file
@ -0,0 +1,8 @@
|
||||
namespace TwitchChatTTS.OBS.Socket.Data
|
||||
{
|
||||
public class RequestBatchResponseMessage
|
||||
{
|
||||
public string RequestId { get; set; }
|
||||
public IEnumerable<object> Results { get; set; }
|
||||
}
|
||||
}
|
@ -1,13 +1,13 @@
|
||||
namespace TwitchChatTTS.OBS.Socket.Data
|
||||
{
|
||||
[Serializable]
|
||||
public class RequestMessage
|
||||
{
|
||||
public string RequestType { get; set; }
|
||||
public string RequestId { 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;
|
||||
RequestId = id;
|
||||
RequestData = data;
|
||||
|
@ -1,6 +1,5 @@
|
||||
namespace TwitchChatTTS.OBS.Socket.Data
|
||||
{
|
||||
[Serializable]
|
||||
public class RequestResponseMessage
|
||||
{
|
||||
public string RequestType { get; set; }
|
||||
|
@ -1,19 +1,20 @@
|
||||
using CommonSocketLibrary.Abstract;
|
||||
using CommonSocketLibrary.Common;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Serilog;
|
||||
using TwitchChatTTS.OBS.Socket.Data;
|
||||
|
||||
namespace TwitchChatTTS.OBS.Socket.Handlers
|
||||
{
|
||||
public class EventMessageHandler : IWebSocketHandler
|
||||
{
|
||||
private ILogger Logger { get; }
|
||||
private IServiceProvider ServiceProvider { get; }
|
||||
private ILogger _logger { get; }
|
||||
private IServiceProvider _serviceProvider { get; }
|
||||
public int OperationCode { get; set; } = 5;
|
||||
|
||||
public EventMessageHandler(ILogger<EventMessageHandler> logger, IServiceProvider serviceProvider) {
|
||||
Logger = logger;
|
||||
ServiceProvider = serviceProvider;
|
||||
public EventMessageHandler(ILogger logger, IServiceProvider serviceProvider)
|
||||
{
|
||||
_logger = logger;
|
||||
_serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
public async Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data message)
|
||||
@ -21,28 +22,31 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
|
||||
if (message is not EventMessage obj || obj == null)
|
||||
return;
|
||||
|
||||
switch (obj.EventType) {
|
||||
switch (obj.EventType)
|
||||
{
|
||||
case "StreamStateChanged":
|
||||
case "RecordStateChanged":
|
||||
if (sender is not OBSSocketClient client)
|
||||
return;
|
||||
|
||||
|
||||
string? raw_state = obj.EventData["outputState"].ToString();
|
||||
string? state = raw_state?.Substring(21).ToLower();
|
||||
client.Live = obj.EventData["outputActive"].ToString() == "True";
|
||||
Logger.LogWarning("Stream " + (state != null && state.EndsWith("ing") ? "is " : "has ") + state + ".");
|
||||
_logger.Warning("Stream " + (state != null && state.EndsWith("ing") ? "is " : "has ") + state + ".");
|
||||
|
||||
if (client.Live == false && state != null && !state.EndsWith("ing")) {
|
||||
if (client.Live == false && state != null && !state.EndsWith("ing"))
|
||||
{
|
||||
OnStreamEnd();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
Logger.LogDebug(obj.EventType + " EVENT: " + string.Join(" | ", obj.EventData?.Select(x => x.Key + "=" + x.Value?.ToString()) ?? new string[0]));
|
||||
_logger.Debug(obj.EventType + " EVENT: " + string.Join(" | ", obj.EventData?.Select(x => x.Key + "=" + x.Value?.ToString()) ?? new string[0]));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnStreamEnd() {
|
||||
private void OnStreamEnd()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -2,7 +2,7 @@ using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using CommonSocketLibrary.Abstract;
|
||||
using CommonSocketLibrary.Common;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Serilog;
|
||||
using TwitchChatTTS.OBS.Socket.Data;
|
||||
using TwitchChatTTS.OBS.Socket.Context;
|
||||
|
||||
@ -10,13 +10,14 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
|
||||
{
|
||||
public class HelloHandler : IWebSocketHandler
|
||||
{
|
||||
private ILogger Logger { get; }
|
||||
private ILogger _logger { get; }
|
||||
public int OperationCode { get; set; } = 0;
|
||||
private HelloContext Context { get; }
|
||||
private HelloContext _context { get; }
|
||||
|
||||
public HelloHandler(ILogger<HelloHandler> logger, HelloContext context) {
|
||||
Logger = logger;
|
||||
Context = context;
|
||||
public HelloHandler(ILogger logger, HelloContext context)
|
||||
{
|
||||
_logger = logger;
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data message)
|
||||
@ -24,19 +25,23 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
|
||||
if (message is not HelloMessage obj || obj == null)
|
||||
return;
|
||||
|
||||
Logger.LogTrace("OBS websocket password: " + Context.Password);
|
||||
if (obj.Authentication == null || Context.Password == null) // TODO: send re-identify message.
|
||||
_logger.Verbose("OBS websocket password: " + _context.Password);
|
||||
if (obj.Authentication == null || string.IsNullOrWhiteSpace(_context.Password))
|
||||
{
|
||||
await sender.Send(1, new IdentifyMessage(obj.RpcVersion, string.Empty, 1023 | 262144));
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
var salt = obj.Authentication.Salt;
|
||||
var challenge = obj.Authentication.Challenge;
|
||||
Logger.LogTrace("Salt: " + salt);
|
||||
Logger.LogTrace("Challenge: " + challenge);
|
||||
|
||||
string secret = Context.Password + salt;
|
||||
_logger.Verbose("Salt: " + salt);
|
||||
_logger.Verbose("Challenge: " + challenge);
|
||||
|
||||
string secret = _context.Password + salt;
|
||||
byte[] bytes = Encoding.UTF8.GetBytes(secret);
|
||||
string hash = null;
|
||||
using (var sha = SHA256.Create()) {
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
bytes = sha.ComputeHash(bytes);
|
||||
hash = Convert.ToBase64String(bytes);
|
||||
|
||||
@ -46,7 +51,7 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
|
||||
hash = Convert.ToBase64String(bytes);
|
||||
}
|
||||
|
||||
Logger.LogTrace("Final hash: " + hash);
|
||||
_logger.Verbose("Final hash: " + hash);
|
||||
await sender.Send(1, new IdentifyMessage(obj.RpcVersion, hash, 1023 | 262144));
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
using CommonSocketLibrary.Abstract;
|
||||
using CommonSocketLibrary.Common;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Serilog;
|
||||
using TwitchChatTTS.OBS.Socket.Data;
|
||||
|
||||
namespace TwitchChatTTS.OBS.Socket.Handlers
|
||||
@ -10,7 +10,8 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
|
||||
private ILogger Logger { get; }
|
||||
public int OperationCode { get; set; } = 2;
|
||||
|
||||
public IdentifiedHandler(ILogger<IdentifiedHandler> logger) {
|
||||
public IdentifiedHandler(ILogger logger)
|
||||
{
|
||||
Logger = logger;
|
||||
}
|
||||
|
||||
@ -18,9 +19,9 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
|
||||
{
|
||||
if (message is not IdentifiedMessage obj || obj == null)
|
||||
return;
|
||||
|
||||
|
||||
sender.Connected = true;
|
||||
Logger.LogInformation("Connected to OBS via rpc version " + obj.NegotiatedRpcVersion + ".");
|
||||
Logger.Information("Connected to OBS via rpc version " + obj.NegotiatedRpcVersion + ".");
|
||||
}
|
||||
}
|
||||
}
|
85
OBS/Socket/Handlers/RequestBatchResponseHandler.cs
Normal file
85
OBS/Socket/Handlers/RequestBatchResponseHandler.cs
Normal file
@ -0,0 +1,85 @@
|
||||
using System.Text.Json;
|
||||
using CommonSocketLibrary.Abstract;
|
||||
using CommonSocketLibrary.Common;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Serilog;
|
||||
using Serilog.Context;
|
||||
using TwitchChatTTS.OBS.Socket.Data;
|
||||
using TwitchChatTTS.OBS.Socket.Manager;
|
||||
|
||||
namespace TwitchChatTTS.OBS.Socket.Handlers
|
||||
{
|
||||
public class RequestBatchResponseHandler : IWebSocketHandler
|
||||
{
|
||||
private OBSRequestBatchManager _manager { get; }
|
||||
private IServiceProvider _serviceProvider { get; }
|
||||
private ILogger _logger { get; }
|
||||
private JsonSerializerOptions _options;
|
||||
public int OperationCode { get; set; } = 9;
|
||||
|
||||
public RequestBatchResponseHandler(OBSRequestBatchManager manager, JsonSerializerOptions options, IServiceProvider serviceProvider, ILogger logger)
|
||||
{
|
||||
_manager = manager;
|
||||
_serviceProvider = serviceProvider;
|
||||
_logger = logger;
|
||||
_options = options;
|
||||
}
|
||||
|
||||
public async Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data data)
|
||||
{
|
||||
if (data is not RequestBatchResponseMessage message || message == null)
|
||||
return;
|
||||
|
||||
using (LogContext.PushProperty("obsrid", message.RequestId))
|
||||
{
|
||||
|
||||
var results = message.Results.ToList();
|
||||
_logger.Debug($"Received request batch response of {results.Count} messages.");
|
||||
|
||||
var requestData = _manager.Take(message.RequestId);
|
||||
if (requestData == null || !results.Any())
|
||||
{
|
||||
_logger.Verbose($"Received request batch response of {results.Count} messages.");
|
||||
return;
|
||||
}
|
||||
|
||||
IList<Task> tasks = new List<Task>();
|
||||
int count = Math.Min(results.Count, requestData.RequestTypes.Count);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
Type type = requestData.RequestTypes[i];
|
||||
|
||||
using (LogContext.PushProperty("type", type.Name))
|
||||
{
|
||||
try
|
||||
{
|
||||
var handler = GetResponseHandlerForRequestType(type);
|
||||
_logger.Verbose($"Request handled by {handler.GetType().Name}.");
|
||||
tasks.Add(handler.Execute(sender, results[i]));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Failed to process an item in a request batch message.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_logger.Verbose($"Waiting for processing to complete.");
|
||||
await Task.WhenAll(tasks);
|
||||
|
||||
_logger.Debug($"Finished processing all request in this batch.");
|
||||
}
|
||||
}
|
||||
|
||||
private IWebSocketHandler? GetResponseHandlerForRequestType(Type type)
|
||||
{
|
||||
if (type == typeof(RequestMessage))
|
||||
return _serviceProvider.GetRequiredKeyedService<IWebSocketHandler>("obs-requestresponse");
|
||||
else if (type == typeof(RequestBatchMessage))
|
||||
return _serviceProvider.GetRequiredKeyedService<IWebSocketHandler>("obs-requestbatcresponse");
|
||||
else if (type == typeof(IdentifyMessage))
|
||||
return _serviceProvider.GetRequiredKeyedService<IWebSocketHandler>("obs-identified");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
using CommonSocketLibrary.Abstract;
|
||||
using CommonSocketLibrary.Common;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Serilog;
|
||||
using TwitchChatTTS.OBS.Socket.Data;
|
||||
|
||||
namespace TwitchChatTTS.OBS.Socket.Handlers
|
||||
@ -10,7 +10,8 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
|
||||
private ILogger Logger { get; }
|
||||
public int OperationCode { get; set; } = 7;
|
||||
|
||||
public RequestResponseHandler(ILogger<RequestResponseHandler> logger) {
|
||||
public RequestResponseHandler(ILogger logger)
|
||||
{
|
||||
Logger = logger;
|
||||
}
|
||||
|
||||
@ -18,15 +19,17 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
|
||||
{
|
||||
if (message is not RequestResponseMessage obj || obj == null)
|
||||
return;
|
||||
|
||||
switch (obj.RequestType) {
|
||||
|
||||
switch (obj.RequestType)
|
||||
{
|
||||
case "GetOutputStatus":
|
||||
if (sender is not OBSSocketClient client)
|
||||
return;
|
||||
|
||||
if (obj.RequestId == "stream") {
|
||||
|
||||
if (obj.RequestId == "stream")
|
||||
{
|
||||
client.Live = obj.ResponseData["outputActive"].ToString() == "True";
|
||||
Logger.LogWarning("Updated stream's live status to " + client.Live);
|
||||
Logger.Warning("Updated stream's live status to " + client.Live);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
60
OBS/Socket/Manager/OBSBatchRequestManager.cs
Normal file
60
OBS/Socket/Manager/OBSBatchRequestManager.cs
Normal file
@ -0,0 +1,60 @@
|
||||
using CommonSocketLibrary.Abstract;
|
||||
using CommonSocketLibrary.Common;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Serilog;
|
||||
using TwitchChatTTS.OBS.Socket.Data;
|
||||
|
||||
namespace TwitchChatTTS.OBS.Socket.Manager
|
||||
{
|
||||
public class OBSRequestBatchManager
|
||||
{
|
||||
private IDictionary<string, OBSRequestBatchData> _requests;
|
||||
private IServiceProvider _serviceProvider;
|
||||
private ILogger _logger;
|
||||
|
||||
public OBSRequestBatchManager(IServiceProvider serviceProvider, ILogger logger)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
|
||||
public async Task Send(long broadcasterId, IEnumerable<WebSocketMessage> messages) {
|
||||
string uid = GenerateUniqueIdentifier();
|
||||
var data = new OBSRequestBatchData(broadcasterId, uid, new List<Type>());
|
||||
_logger.Debug($"Sending request batch of {messages.Count()} messages.");
|
||||
|
||||
foreach (WebSocketMessage message in messages)
|
||||
data.RequestTypes.Add(message.GetType());
|
||||
|
||||
var client = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("obs");
|
||||
await client.Send(8, new RequestBatchMessage(uid, messages));
|
||||
}
|
||||
|
||||
public OBSRequestBatchData? Take(string id) {
|
||||
if (_requests.TryGetValue(id, out var request)) {
|
||||
_requests.Remove(id);
|
||||
return request;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private string GenerateUniqueIdentifier()
|
||||
{
|
||||
return Guid.NewGuid().ToString("X");
|
||||
}
|
||||
}
|
||||
|
||||
public class OBSRequestBatchData
|
||||
{
|
||||
public long BroadcasterId { get; }
|
||||
public string RequestId { get; }
|
||||
public IList<Type> RequestTypes { get; }
|
||||
|
||||
public OBSRequestBatchData(long bid, string rid, IList<Type> types) {
|
||||
BroadcasterId = bid;
|
||||
RequestId = rid;
|
||||
RequestTypes = types;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Serilog;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using CommonSocketLibrary.Socket.Manager;
|
||||
using CommonSocketLibrary.Common;
|
||||
@ -7,23 +7,26 @@ namespace TwitchChatTTS.OBS.Socket.Manager
|
||||
{
|
||||
public class OBSHandlerManager : WebSocketHandlerManager
|
||||
{
|
||||
public OBSHandlerManager(ILogger<OBSHandlerManager> logger, IServiceProvider provider) : base(logger) {
|
||||
public OBSHandlerManager(ILogger logger, IServiceProvider provider) : base(logger)
|
||||
{
|
||||
var basetype = typeof(IWebSocketHandler);
|
||||
var assembly = GetType().Assembly;
|
||||
var types = assembly.GetTypes().Where(t => t.IsClass && basetype.IsAssignableFrom(t) && t.AssemblyQualifiedName?.Contains(".OBS.") == true);
|
||||
|
||||
foreach (var type in types) {
|
||||
foreach (var type in types)
|
||||
{
|
||||
var key = "obs-" + 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 obs websocket handler: " + type.AssemblyQualifiedName);
|
||||
if (handler == null)
|
||||
{
|
||||
logger.Error("Failed to find obs websocket handler: " + type.AssemblyQualifiedName);
|
||||
continue;
|
||||
}
|
||||
|
||||
Logger.LogDebug($"Linked type {type.AssemblyQualifiedName} to obs websocket handler {handler.GetType().AssemblyQualifiedName}.");
|
||||
|
||||
Logger.Debug($"Linked type {type.AssemblyQualifiedName} to obs websocket handler {handler.GetType().AssemblyQualifiedName}.");
|
||||
Add(handler);
|
||||
}
|
||||
}
|
||||
|
@ -2,14 +2,14 @@ using CommonSocketLibrary.Abstract;
|
||||
using CommonSocketLibrary.Common;
|
||||
using CommonSocketLibrary.Socket.Manager;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Serilog;
|
||||
|
||||
namespace TwitchChatTTS.OBS.Socket.Manager
|
||||
{
|
||||
public class OBSHandlerTypeManager : WebSocketHandlerTypeManager
|
||||
{
|
||||
public OBSHandlerTypeManager(
|
||||
ILogger<OBSHandlerTypeManager> factory,
|
||||
ILogger factory,
|
||||
[FromKeyedServices("obs")] HandlerManager<WebSocketClient, IWebSocketHandler> handlers
|
||||
) : base(factory, handlers)
|
||||
{
|
||||
|
@ -1,29 +1,34 @@
|
||||
using CommonSocketLibrary.Common;
|
||||
using CommonSocketLibrary.Abstract;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Serilog;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace TwitchChatTTS.OBS.Socket
|
||||
{
|
||||
public class OBSSocketClient : WebSocketClient {
|
||||
public class OBSSocketClient : WebSocketClient
|
||||
{
|
||||
private bool _live;
|
||||
public bool? Live {
|
||||
public bool? Live
|
||||
{
|
||||
get => Connected ? _live : null;
|
||||
set {
|
||||
set
|
||||
{
|
||||
if (value.HasValue)
|
||||
_live = value.Value;
|
||||
}
|
||||
}
|
||||
|
||||
public OBSSocketClient(
|
||||
ILogger<OBSSocketClient> logger,
|
||||
ILogger logger,
|
||||
[FromKeyedServices("obs")] HandlerManager<WebSocketClient, IWebSocketHandler> handlerManager,
|
||||
[FromKeyedServices("obs")] HandlerTypeManager<WebSocketClient, IWebSocketHandler> typeManager
|
||||
) : base(logger, handlerManager, typeManager, new JsonSerializerOptions() {
|
||||
) : base(logger, handlerManager, typeManager, new JsonSerializerOptions()
|
||||
{
|
||||
PropertyNameCaseInsensitive = false,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
}) {
|
||||
})
|
||||
{
|
||||
_live = false;
|
||||
}
|
||||
}
|
||||
|
@ -1,70 +1,42 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace TwitchChatTTS.Seven
|
||||
{
|
||||
public class EmoteCounter {
|
||||
public IDictionary<string, IDictionary<long, int>> Counters { get; set; }
|
||||
public class EmoteDatabase
|
||||
{
|
||||
private readonly IDictionary<string, string> _emotes;
|
||||
public IDictionary<string, string> Emotes { get => _emotes.AsReadOnly(); }
|
||||
|
||||
public EmoteCounter() {
|
||||
Counters = new ConcurrentDictionary<string, IDictionary<long, int>>();
|
||||
public EmoteDatabase()
|
||||
{
|
||||
_emotes = new Dictionary<string, string>();
|
||||
}
|
||||
|
||||
public void Add(long userId, IEnumerable<string> emoteIds) {
|
||||
foreach (var emote in emoteIds) {
|
||||
if (Counters.TryGetValue(emote, out IDictionary<long, int>? subcounters)) {
|
||||
if (subcounters.TryGetValue(userId, out int counter))
|
||||
subcounters[userId] = counter + 1;
|
||||
else
|
||||
subcounters.Add(userId, 1);
|
||||
} else {
|
||||
Counters.Add(emote, new ConcurrentDictionary<long, int>());
|
||||
Counters[emote].Add(userId, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear() {
|
||||
Counters.Clear();
|
||||
}
|
||||
|
||||
public int Get(long userId, string emoteId) {
|
||||
if (Counters.TryGetValue(emoteId, out IDictionary<long, int>? subcounters)) {
|
||||
if (subcounters.TryGetValue(userId, out int counter))
|
||||
return counter;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
public class EmoteDatabase {
|
||||
private IDictionary<string, string> Emotes { get; }
|
||||
|
||||
public EmoteDatabase() {
|
||||
Emotes = new Dictionary<string, string>();
|
||||
}
|
||||
|
||||
public void Add(string emoteName, string emoteId) {
|
||||
if (Emotes.ContainsKey(emoteName))
|
||||
Emotes[emoteName] = emoteId;
|
||||
public void Add(string emoteName, string emoteId)
|
||||
{
|
||||
if (_emotes.ContainsKey(emoteName))
|
||||
_emotes[emoteName] = emoteId;
|
||||
else
|
||||
Emotes.Add(emoteName, emoteId);
|
||||
_emotes.Add(emoteName, emoteId);
|
||||
}
|
||||
|
||||
public void Clear() {
|
||||
Emotes.Clear();
|
||||
public void Clear()
|
||||
{
|
||||
_emotes.Clear();
|
||||
}
|
||||
|
||||
public string? Get(string emoteName) {
|
||||
return Emotes.TryGetValue(emoteName, out string? emoteId) ? emoteId : null;
|
||||
public string? Get(string emoteName)
|
||||
{
|
||||
return _emotes.TryGetValue(emoteName, out string? emoteId) ? emoteId : null;
|
||||
}
|
||||
|
||||
public void Remove(string emoteName) {
|
||||
if (Emotes.ContainsKey(emoteName))
|
||||
Emotes.Remove(emoteName);
|
||||
public void Remove(string emoteName)
|
||||
{
|
||||
if (_emotes.ContainsKey(emoteName))
|
||||
_emotes.Remove(emoteName);
|
||||
}
|
||||
}
|
||||
|
||||
public class EmoteSet {
|
||||
public class EmoteSet
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public int Flags { get; set; }
|
||||
@ -75,7 +47,8 @@ namespace TwitchChatTTS.Seven
|
||||
public int Capacity { get; set; }
|
||||
}
|
||||
|
||||
public class Emote {
|
||||
public class Emote
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public int Flags { get; set; }
|
||||
|
@ -1,46 +1,59 @@
|
||||
using System.Text.Json;
|
||||
using TwitchChatTTS.Helpers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Serilog;
|
||||
using TwitchChatTTS.Seven;
|
||||
using TwitchChatTTS;
|
||||
|
||||
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 ILogger<SevenApiClient> Logger { get; }
|
||||
private long? Id { get; }
|
||||
private ILogger Logger { get; }
|
||||
|
||||
|
||||
public SevenApiClient(ILogger<SevenApiClient> logger, TwitchBotToken token) {
|
||||
public SevenApiClient(ILogger logger)
|
||||
{
|
||||
Logger = logger;
|
||||
Id = long.TryParse(token?.BroadcasterId, out long id) ? id : -1;
|
||||
|
||||
Web = new WebClientWrap(new JsonSerializerOptions() {
|
||||
Web = new WebClientWrap(new JsonSerializerOptions()
|
||||
{
|
||||
PropertyNameCaseInsensitive = false,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<EmoteDatabase?> GetSevenEmotes() {
|
||||
if (Id == null)
|
||||
throw new NullReferenceException(nameof(Id));
|
||||
|
||||
try {
|
||||
var details = await Web.GetJson<UserDetails>($"{API_URL}/users/twitch/" + Id);
|
||||
if (details == null)
|
||||
return null;
|
||||
|
||||
var emotes = new EmoteDatabase();
|
||||
if (details.EmoteSet != null)
|
||||
foreach (var emote in details.EmoteSet.Emotes)
|
||||
emotes.Add(emote.Name, emote.Id);
|
||||
Logger.LogInformation($"Loaded {details.EmoteSet?.Emotes.Count() ?? 0} emotes from 7tv.");
|
||||
return emotes;
|
||||
} catch (JsonException e) {
|
||||
Logger.LogError(e, "Failed to fetch emotes from 7tv. 2");
|
||||
} catch (Exception e) {
|
||||
Logger.LogError(e, "Failed to fetch emotes from 7tv.");
|
||||
public async Task<EmoteSet?> FetchChannelEmoteSet(string twitchId) {
|
||||
try
|
||||
{
|
||||
var details = await Web.GetJson<UserDetails>($"{API_URL}/users/twitch/" + twitchId);
|
||||
return details?.EmoteSet;
|
||||
}
|
||||
catch (JsonException e)
|
||||
{
|
||||
Logger.Error(e, "Failed to fetch emotes from 7tv due to improper JSON.");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error(e, "Failed to fetch emotes from 7tv.");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Emote>?> FetchGlobalSevenEmotes()
|
||||
{
|
||||
try
|
||||
{
|
||||
var emoteSet = await Web.GetJson<EmoteSet>($"{API_URL}/emote-sets/6353512c802a0e34bac96dd2");
|
||||
return emoteSet?.Emotes;
|
||||
}
|
||||
catch (JsonException e)
|
||||
{
|
||||
Logger.Error(e, "Failed to fetch emotes from 7tv due to improper JSON.");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error(e, "Failed to fetch emotes from 7tv.");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ using System.Text.Json;
|
||||
using CommonSocketLibrary.Abstract;
|
||||
using CommonSocketLibrary.Common;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Serilog;
|
||||
using TwitchChatTTS.Seven.Socket.Data;
|
||||
|
||||
namespace TwitchChatTTS.Seven.Socket.Handlers
|
||||
@ -11,9 +11,11 @@ namespace TwitchChatTTS.Seven.Socket.Handlers
|
||||
{
|
||||
private ILogger Logger { get; }
|
||||
private EmoteDatabase Emotes { get; }
|
||||
private object _lock = new object();
|
||||
public int OperationCode { get; set; } = 0;
|
||||
|
||||
public DispatchHandler(ILogger<DispatchHandler> logger, EmoteDatabase emotes) {
|
||||
public DispatchHandler(ILogger logger, EmoteDatabase emotes)
|
||||
{
|
||||
Logger = logger;
|
||||
Emotes = emotes;
|
||||
}
|
||||
@ -22,33 +24,82 @@ namespace TwitchChatTTS.Seven.Socket.Handlers
|
||||
{
|
||||
if (message is not DispatchMessage obj || obj == null)
|
||||
return;
|
||||
|
||||
|
||||
ApplyChanges(obj?.Body?.Pulled, cf => cf.OldValue, true);
|
||||
ApplyChanges(obj?.Body?.Pushed, cf => cf.Value, false);
|
||||
ApplyChanges(obj?.Body?.Removed, cf => cf.OldValue, true);
|
||||
ApplyChanges(obj?.Body?.Updated, cf => cf.OldValue, false, cf => cf.Value);
|
||||
}
|
||||
|
||||
private void ApplyChanges(IEnumerable<ChangeField>? fields, Func<ChangeField, object> getter, bool removing) {
|
||||
if (fields == null)
|
||||
private void ApplyChanges(IEnumerable<ChangeField>? fields, Func<ChangeField, object> getter, bool removing, Func<ChangeField, object>? updater = null)
|
||||
{
|
||||
if (fields == null || !fields.Any() || removing && updater != null)
|
||||
return;
|
||||
|
||||
foreach (var val in fields) {
|
||||
|
||||
foreach (var val in fields)
|
||||
{
|
||||
var value = getter(val);
|
||||
if (value == null)
|
||||
continue;
|
||||
|
||||
var o = JsonSerializer.Deserialize<EmoteField>(value.ToString(), new JsonSerializerOptions() {
|
||||
|
||||
var o = JsonSerializer.Deserialize<EmoteField>(value.ToString(), new JsonSerializerOptions()
|
||||
{
|
||||
PropertyNameCaseInsensitive = false,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
|
||||
});
|
||||
if (o == null)
|
||||
continue;
|
||||
|
||||
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})");
|
||||
lock (_lock)
|
||||
{
|
||||
if (removing)
|
||||
{
|
||||
RemoveEmoteById(o.Id);
|
||||
Logger.Information($"Removed 7tv emote: {o.Name} (id: {o.Id})");
|
||||
}
|
||||
else if (updater != null)
|
||||
{
|
||||
RemoveEmoteById(o.Id);
|
||||
var update = updater(val);
|
||||
|
||||
var u = JsonSerializer.Deserialize<EmoteField>(update.ToString(), new JsonSerializerOptions()
|
||||
{
|
||||
PropertyNameCaseInsensitive = false,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
|
||||
});
|
||||
|
||||
if (u != null)
|
||||
{
|
||||
Emotes.Add(u.Name, u.Id);
|
||||
Logger.Information($"Updated 7tv emote: from '{o.Name}' to '{u.Name}' (id: {u.Id})");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Warning("Failed to update 7tv emote.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Emotes.Add(o.Name, o.Id);
|
||||
Logger.Information($"Added 7tv emote: {o.Name} (id: {o.Id})");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveEmoteById(string id)
|
||||
{
|
||||
string? key = null;
|
||||
foreach (var e in Emotes.Emotes)
|
||||
{
|
||||
if (e.Value == id)
|
||||
{
|
||||
key = e.Key;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (key != null)
|
||||
Emotes.Remove(key);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
using CommonSocketLibrary.Abstract;
|
||||
using CommonSocketLibrary.Common;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Serilog;
|
||||
using TwitchChatTTS.Seven.Socket.Context;
|
||||
using TwitchChatTTS.Seven.Socket.Data;
|
||||
|
||||
@ -10,17 +10,18 @@ namespace TwitchChatTTS.Seven.Socket.Handlers
|
||||
public class EndOfStreamHandler : IWebSocketHandler
|
||||
{
|
||||
private ILogger Logger { get; }
|
||||
private Configuration Configuration { get; }
|
||||
private User User { get; }
|
||||
private IServiceProvider ServiceProvider { get; }
|
||||
private string[] ErrorCodes { get; }
|
||||
private int[] ReconnectDelay { get; }
|
||||
|
||||
public int OperationCode { get; set; } = 7;
|
||||
|
||||
|
||||
public EndOfStreamHandler(ILogger<EndOfStreamHandler> logger, Configuration configuration, IServiceProvider serviceProvider) {
|
||||
|
||||
public EndOfStreamHandler(ILogger logger, User user, IServiceProvider serviceProvider)
|
||||
{
|
||||
Logger = logger;
|
||||
Configuration = configuration;
|
||||
User = user;
|
||||
ServiceProvider = serviceProvider;
|
||||
|
||||
ErrorCodes = [
|
||||
@ -59,37 +60,40 @@ namespace TwitchChatTTS.Seven.Socket.Handlers
|
||||
{
|
||||
if (message is not EndOfStreamMessage obj || obj == null)
|
||||
return;
|
||||
|
||||
|
||||
var code = obj.Code - 4000;
|
||||
if (code >= 0 && code < ErrorCodes.Length)
|
||||
Logger.LogWarning($"Received end of stream message (reason: {ErrorCodes[code]}, code: {obj.Code}, message: {obj.Message}).");
|
||||
Logger.Warning($"Received end of stream message (reason: {ErrorCodes[code]}, code: {obj.Code}, message: {obj.Message}).");
|
||||
else
|
||||
Logger.LogWarning($"Received end of stream message (code: {obj.Code}, message: {obj.Message}).");
|
||||
|
||||
Logger.Warning($"Received end of stream message (code: {obj.Code}, message: {obj.Message}).");
|
||||
|
||||
await sender.DisconnectAsync();
|
||||
|
||||
if (code >= 0 && code < ReconnectDelay.Length && ReconnectDelay[code] < 0) {
|
||||
Logger.LogError($"7tv client will remain disconnected due to a bad client implementation.");
|
||||
if (code >= 0 && code < ReconnectDelay.Length && ReconnectDelay[code] < 0)
|
||||
{
|
||||
Logger.Error($"7tv client will remain disconnected due to a bad client implementation.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(Configuration.Seven?.UserId))
|
||||
if (string.IsNullOrWhiteSpace(User.SevenEmoteSetId))
|
||||
return;
|
||||
|
||||
var context = ServiceProvider.GetRequiredService<ReconnectContext>();
|
||||
await Task.Delay(ReconnectDelay[code]);
|
||||
|
||||
//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]) + ">"));
|
||||
var base_url = $"@emote_set.*<object_id={Configuration.Seven.UserId.Trim()}>";
|
||||
var base_url = $"@emote_set.*<object_id={User.SevenEmoteSetId}>";
|
||||
string url = $"{SevenApiClient.WEBSOCKET_URL}{base_url}";
|
||||
Logger.LogDebug($"7tv websocket reconnecting to {url}.");
|
||||
Logger.Debug($"7tv websocket reconnecting to {url}.");
|
||||
|
||||
await sender.ConnectAsync(url);
|
||||
if (context.SessionId != null) {
|
||||
if (context.SessionId != null)
|
||||
{
|
||||
await sender.Send(34, new ResumeMessage() { SessionId = context.SessionId });
|
||||
Logger.LogInformation("Resumed connection to 7tv websocket.");
|
||||
} else {
|
||||
Logger.LogDebug("7tv websocket session id not available.");
|
||||
Logger.Information("Resumed connection to 7tv websocket.");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Information("Resumed connection to 7tv websocket on a different session.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
using CommonSocketLibrary.Abstract;
|
||||
using CommonSocketLibrary.Common;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Serilog;
|
||||
using TwitchChatTTS.Seven.Socket.Data;
|
||||
|
||||
namespace TwitchChatTTS.Seven.Socket.Handlers
|
||||
@ -10,7 +10,8 @@ namespace TwitchChatTTS.Seven.Socket.Handlers
|
||||
private ILogger Logger { get; }
|
||||
public int OperationCode { get; set; } = 6;
|
||||
|
||||
public ErrorHandler(ILogger<ErrorHandler> logger) {
|
||||
public ErrorHandler(ILogger logger)
|
||||
{
|
||||
Logger = logger;
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
using CommonSocketLibrary.Abstract;
|
||||
using CommonSocketLibrary.Common;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Serilog;
|
||||
using TwitchChatTTS.Seven.Socket.Data;
|
||||
|
||||
namespace TwitchChatTTS.Seven.Socket.Handlers
|
||||
@ -10,7 +10,8 @@ namespace TwitchChatTTS.Seven.Socket.Handlers
|
||||
private ILogger Logger { get; }
|
||||
public int OperationCode { get; set; } = 4;
|
||||
|
||||
public ReconnectHandler(ILogger<ReconnectHandler> logger) {
|
||||
public ReconnectHandler(ILogger logger)
|
||||
{
|
||||
Logger = logger;
|
||||
}
|
||||
|
||||
@ -19,7 +20,7 @@ namespace TwitchChatTTS.Seven.Socket.Handlers
|
||||
if (message is not ReconnectMessage obj || obj == null)
|
||||
return;
|
||||
|
||||
Logger.LogInformation($"7tv server wants us to reconnect (reason: {obj.Reason}).");
|
||||
Logger.Information($"7tv server wants us to reconnect (reason: {obj.Reason}).");
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
using CommonSocketLibrary.Abstract;
|
||||
using CommonSocketLibrary.Common;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Serilog;
|
||||
using TwitchChatTTS.Seven.Socket.Data;
|
||||
|
||||
namespace TwitchChatTTS.Seven.Socket.Handlers
|
||||
@ -11,7 +11,8 @@ namespace TwitchChatTTS.Seven.Socket.Handlers
|
||||
private Configuration Configuration { get; }
|
||||
public int OperationCode { get; set; } = 1;
|
||||
|
||||
public SevenHelloHandler(ILogger<SevenHelloHandler> logger, Configuration configuration) {
|
||||
public SevenHelloHandler(ILogger logger, Configuration configuration)
|
||||
{
|
||||
Logger = logger;
|
||||
Configuration = configuration;
|
||||
}
|
||||
@ -23,10 +24,10 @@ namespace TwitchChatTTS.Seven.Socket.Handlers
|
||||
|
||||
if (sender is not SevenSocketClient seven || seven == null)
|
||||
return;
|
||||
|
||||
|
||||
seven.Connected = true;
|
||||
seven.ConnectionDetails = obj;
|
||||
Logger.LogInformation("Connected to 7tv websockets.");
|
||||
Logger.Information("Connected to 7tv websockets.");
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Serilog;
|
||||
using CommonSocketLibrary.Socket.Manager;
|
||||
using CommonSocketLibrary.Common;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@ -7,28 +7,34 @@ namespace TwitchChatTTS.Seven.Socket.Managers
|
||||
{
|
||||
public class SevenHandlerManager : WebSocketHandlerManager
|
||||
{
|
||||
public SevenHandlerManager(ILogger<SevenHandlerManager> logger, IServiceProvider provider) : base(logger) {
|
||||
try {
|
||||
public SevenHandlerManager(ILogger logger, IServiceProvider provider) : base(logger)
|
||||
{
|
||||
try
|
||||
{
|
||||
var basetype = typeof(IWebSocketHandler);
|
||||
var assembly = GetType().Assembly;
|
||||
var types = assembly.GetTypes().Where(t => t.IsClass && basetype.IsAssignableFrom(t) && t.AssemblyQualifiedName?.Contains(".Seven.") == true);
|
||||
|
||||
foreach (var type in types) {
|
||||
foreach (var type in types)
|
||||
{
|
||||
var key = "7tv-" + 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 7tv websocket handler: " + type.AssemblyQualifiedName);
|
||||
if (handler == null)
|
||||
{
|
||||
logger.Error("Failed to find 7tv websocket handler: " + type.AssemblyQualifiedName);
|
||||
continue;
|
||||
}
|
||||
|
||||
Logger.LogDebug($"Linked type {type.AssemblyQualifiedName} to 7tv websocket handler {handler.GetType().AssemblyQualifiedName}.");
|
||||
|
||||
Logger.Debug($"Linked type {type.AssemblyQualifiedName} to 7tv websocket handler {handler.GetType().AssemblyQualifiedName}.");
|
||||
Add(handler);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Logger.LogError(e, "Failed to load 7tv websocket handler types.");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error(e, "Failed to load 7tv websocket handler types.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,14 +2,14 @@ using CommonSocketLibrary.Abstract;
|
||||
using CommonSocketLibrary.Common;
|
||||
using CommonSocketLibrary.Socket.Manager;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Serilog;
|
||||
|
||||
namespace TwitchChatTTS.Seven.Socket.Managers
|
||||
{
|
||||
public class SevenHandlerTypeManager : WebSocketHandlerTypeManager
|
||||
{
|
||||
public SevenHandlerTypeManager(
|
||||
ILogger<SevenHandlerTypeManager> factory,
|
||||
ILogger factory,
|
||||
[FromKeyedServices("7tv")] HandlerManager<WebSocketClient,
|
||||
IWebSocketHandler> handlers
|
||||
) : base(factory, handlers)
|
||||
|
@ -1,23 +1,26 @@
|
||||
using CommonSocketLibrary.Common;
|
||||
using CommonSocketLibrary.Abstract;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Serilog;
|
||||
using TwitchChatTTS.Seven.Socket.Data;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace TwitchChatTTS.Seven.Socket
|
||||
{
|
||||
public class SevenSocketClient : WebSocketClient {
|
||||
public class SevenSocketClient : WebSocketClient
|
||||
{
|
||||
public SevenHelloMessage? ConnectionDetails { get; set; }
|
||||
|
||||
public SevenSocketClient(
|
||||
ILogger<SevenSocketClient> logger,
|
||||
ILogger logger,
|
||||
[FromKeyedServices("7tv")] HandlerManager<WebSocketClient, IWebSocketHandler> handlerManager,
|
||||
[FromKeyedServices("7tv")] HandlerTypeManager<WebSocketClient, IWebSocketHandler> typeManager
|
||||
) : base(logger, handlerManager, typeManager, new JsonSerializerOptions() {
|
||||
) : base(logger, handlerManager, typeManager, new JsonSerializerOptions()
|
||||
{
|
||||
PropertyNameCaseInsensitive = false,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
|
||||
}) {
|
||||
})
|
||||
{
|
||||
ConnectionDetails = null;
|
||||
}
|
||||
}
|
||||
|
@ -8,5 +8,12 @@ namespace TwitchChatTTS.Seven
|
||||
public int EmoteCapacity { get; set; }
|
||||
public int? EmoteSetId { get; set; }
|
||||
public EmoteSet EmoteSet { get; set; }
|
||||
public SevenUser User { get; set; }
|
||||
}
|
||||
|
||||
public class SevenUser
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string Username { get; set; }
|
||||
}
|
||||
}
|
72
Startup.cs
72
Startup.cs
@ -7,7 +7,6 @@ using CommonSocketLibrary.Abstract;
|
||||
using CommonSocketLibrary.Common;
|
||||
using YamlDotNet.Serialization;
|
||||
using YamlDotNet.Serialization.NamingConventions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TwitchChatTTS.Seven.Socket;
|
||||
using TwitchChatTTS.OBS.Socket.Handlers;
|
||||
using TwitchChatTTS.Seven.Socket.Handlers;
|
||||
@ -26,6 +25,8 @@ using TwitchChatTTS.Hermes.Socket.Managers;
|
||||
using TwitchChatTTS.Chat.Commands.Parameters;
|
||||
using TwitchChatTTS.Chat.Commands;
|
||||
using System.Text.Json;
|
||||
using Serilog;
|
||||
using Serilog.Events;
|
||||
|
||||
// dotnet publish -r linux-x64 -p:PublishSingleFile=true --self-contained true
|
||||
// dotnet publish -r win-x64 -p:PublishSingleFile=true --self-contained true
|
||||
@ -41,18 +42,31 @@ var deserializer = new DeserializerBuilder()
|
||||
var configContent = File.ReadAllText("tts.config.yml");
|
||||
var configuration = deserializer.Deserialize<Configuration>(configContent);
|
||||
var redeemKeys = configuration.Twitch?.Redeems?.Keys;
|
||||
if (redeemKeys != null) {
|
||||
foreach (var key in redeemKeys) {
|
||||
if (key != key.ToLower() && configuration.Twitch?.Redeems != null)
|
||||
if (redeemKeys != null && redeemKeys.Any())
|
||||
{
|
||||
foreach (var key in redeemKeys)
|
||||
{
|
||||
if (key != key.ToLower())
|
||||
configuration.Twitch.Redeems.Add(key.ToLower(), configuration.Twitch.Redeems[key]);
|
||||
}
|
||||
}
|
||||
s.AddSingleton<Configuration>(configuration);
|
||||
|
||||
s.AddLogging();
|
||||
var logger = new LoggerConfiguration()
|
||||
#if DEBUG
|
||||
.MinimumLevel.Debug()
|
||||
#else
|
||||
.MinimumLevel.Information()
|
||||
#endif
|
||||
.WriteTo.File("logs/log.log", rollingInterval: RollingInterval.Day, retainedFileCountLimit: 7)
|
||||
.WriteTo.Console(restrictedToMinimumLevel: LogEventLevel.Information)
|
||||
.CreateLogger();
|
||||
|
||||
s.AddSerilog(logger);
|
||||
s.AddSingleton<User>(new User());
|
||||
|
||||
s.AddSingleton<JsonSerializerOptions>(new JsonSerializerOptions() {
|
||||
s.AddSingleton<JsonSerializerOptions>(new JsonSerializerOptions()
|
||||
{
|
||||
PropertyNameCaseInsensitive = false,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
|
||||
});
|
||||
@ -65,46 +79,38 @@ 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.AddKeyedSingleton<ChatCommand, RefreshTTSDataCommand>("command-refreshttsdata");
|
||||
s.AddKeyedSingleton<ChatCommand, OBSCommand>("command-obs");
|
||||
s.AddKeyedSingleton<ChatCommand, TTSCommand>("command-tts");
|
||||
s.AddKeyedSingleton<ChatCommand, VersionCommand>("command-version");
|
||||
s.AddSingleton<ChatCommandManager>();
|
||||
|
||||
s.AddSingleton<TTSPlayer>();
|
||||
s.AddSingleton<ChatMessageHandler>();
|
||||
s.AddSingleton<HermesClient>();
|
||||
s.AddSingleton<TwitchBotToken>(sp => {
|
||||
var hermes = sp.GetRequiredService<HermesClient>();
|
||||
var task = hermes.FetchTwitchBotToken();
|
||||
task.Wait();
|
||||
return task.Result;
|
||||
});
|
||||
s.AddSingleton<HermesApiClient>();
|
||||
s.AddSingleton<TwitchBotAuth>(new TwitchBotAuth());
|
||||
s.AddTransient<IClient, TwitchLib.Communication.Clients.WebSocketClient>();
|
||||
s.AddTransient<ITwitchClient, TwitchClient>();
|
||||
s.AddTransient<ITwitchPubSub, TwitchPubSub>();
|
||||
s.AddSingleton<TwitchApiClient>();
|
||||
|
||||
s.AddSingleton<SevenApiClient>();
|
||||
s.AddSingleton<EmoteDatabase>(sp => {
|
||||
var api = sp.GetRequiredService<SevenApiClient>();
|
||||
var task = api.GetSevenEmotes();
|
||||
task.Wait();
|
||||
return task.Result;
|
||||
});
|
||||
s.AddSingleton<EmoteCounter>(sp => {
|
||||
if (!string.IsNullOrWhiteSpace(configuration.Emotes?.CounterFilePath) && File.Exists(configuration.Emotes.CounterFilePath.Trim()))
|
||||
return deserializer.Deserialize<EmoteCounter>(File.ReadAllText(configuration.Emotes.CounterFilePath.Trim()));
|
||||
return new EmoteCounter();
|
||||
});
|
||||
s.AddSingleton<EmoteDatabase>(new EmoteDatabase());
|
||||
|
||||
// OBS websocket
|
||||
s.AddSingleton<HelloContext>(sp =>
|
||||
new HelloContext() {
|
||||
new HelloContext()
|
||||
{
|
||||
Host = string.IsNullOrWhiteSpace(configuration.Obs?.Host) ? null : configuration.Obs.Host.Trim(),
|
||||
Port = configuration.Obs?.Port,
|
||||
Password = string.IsNullOrWhiteSpace(configuration.Obs?.Password) ? null : configuration.Obs.Password.Trim()
|
||||
}
|
||||
);
|
||||
s.AddSingleton<OBSRequestBatchManager>();
|
||||
s.AddKeyedSingleton<IWebSocketHandler, HelloHandler>("obs-hello");
|
||||
s.AddKeyedSingleton<IWebSocketHandler, IdentifiedHandler>("obs-identified");
|
||||
s.AddKeyedSingleton<IWebSocketHandler, RequestResponseHandler>("obs-requestresponse");
|
||||
s.AddKeyedSingleton<IWebSocketHandler, RequestBatchResponseHandler>("obs-requestbatchresponse");
|
||||
s.AddKeyedSingleton<IWebSocketHandler, EventMessageHandler>("obs-eventmessage");
|
||||
|
||||
s.AddKeyedSingleton<HandlerManager<WebSocketClient, IWebSocketHandler>, OBSHandlerManager>("obs");
|
||||
@ -112,15 +118,18 @@ s.AddKeyedSingleton<HandlerTypeManager<WebSocketClient, IWebSocketHandler>, OBSH
|
||||
s.AddKeyedSingleton<SocketClient<WebSocketMessage>, OBSSocketClient>("obs");
|
||||
|
||||
// 7tv websocket
|
||||
s.AddTransient(sp => {
|
||||
var logger = sp.GetRequiredService<ILogger<ReconnectContext>>();
|
||||
s.AddTransient(sp =>
|
||||
{
|
||||
var logger = sp.GetRequiredService<ILogger>();
|
||||
var client = sp.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("7tv") as SevenSocketClient;
|
||||
if (client == null) {
|
||||
logger.LogError("7tv client == null.");
|
||||
if (client == null)
|
||||
{
|
||||
logger.Error("7tv client == null.");
|
||||
return new ReconnectContext() { SessionId = null };
|
||||
}
|
||||
if (client.ConnectionDetails == null) {
|
||||
logger.LogError("Connection details in 7tv client == null.");
|
||||
if (client.ConnectionDetails == null)
|
||||
{
|
||||
logger.Error("Connection details in 7tv client == null.");
|
||||
return new ReconnectContext() { SessionId = null };
|
||||
}
|
||||
return new ReconnectContext() { SessionId = client.ConnectionDetails.SessionId };
|
||||
@ -147,6 +156,5 @@ s.AddKeyedSingleton<HandlerTypeManager<WebSocketClient, IWebSocketHandler>, Herm
|
||||
s.AddKeyedSingleton<SocketClient<WebSocketMessage>, HermesSocketClient>("hermes");
|
||||
|
||||
s.AddHostedService<TTS>();
|
||||
|
||||
using IHost host = builder.Build();
|
||||
await host.RunAsync();
|
460
TTS.cs
460
TTS.cs
@ -5,263 +5,341 @@ using CommonSocketLibrary.Common;
|
||||
using HermesSocketLibrary.Socket.Data;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Serilog;
|
||||
using NAudio.Wave.SampleProviders;
|
||||
using TwitchChatTTS.Hermes.Socket;
|
||||
using TwitchChatTTS.Seven;
|
||||
using TwitchLib.Client.Events;
|
||||
using YamlDotNet.Serialization;
|
||||
using YamlDotNet.Serialization.NamingConventions;
|
||||
|
||||
namespace TwitchChatTTS
|
||||
{
|
||||
public class TTS : IHostedService
|
||||
{
|
||||
public const int MAJOR_VERSION = 3;
|
||||
public const int MINOR_VERSION = 3;
|
||||
|
||||
private readonly ILogger _logger;
|
||||
private readonly Configuration _configuration;
|
||||
private readonly TTSPlayer _player;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
|
||||
public TTS(ILogger<TTS> logger, Configuration configuration, TTSPlayer player, IServiceProvider serviceProvider) {
|
||||
public TTS(ILogger logger, Configuration configuration, TTSPlayer player, IServiceProvider serviceProvider)
|
||||
{
|
||||
_logger = logger;
|
||||
_configuration = configuration;
|
||||
_player = player;
|
||||
_serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken) {
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
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 hermes = _serviceProvider.GetRequiredService<HermesApiClient>();
|
||||
var seven = _serviceProvider.GetRequiredService<SevenApiClient>();
|
||||
|
||||
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 InitializeObs();
|
||||
|
||||
try {
|
||||
AudioPlaybackEngine.Instance.AddOnMixerInputEnded((object? s, SampleProviderEventArgs e) => {
|
||||
if (e.SampleProvider == _player.Playing) {
|
||||
_player.Playing = null;
|
||||
}
|
||||
});
|
||||
|
||||
Task.Run(async () => {
|
||||
while (true) {
|
||||
try {
|
||||
if (cancellationToken.IsCancellationRequested) {
|
||||
_logger.LogWarning("TTS Buffer - Cancellation token was canceled.");
|
||||
return;
|
||||
}
|
||||
|
||||
var m = _player.ReceiveBuffer();
|
||||
if (m == null) {
|
||||
await Task.Delay(200);
|
||||
continue;
|
||||
}
|
||||
|
||||
string url = $"https://api.streamelements.com/kappa/v2/speech?voice={m.Voice}&text={HttpUtility.UrlEncode(m.Message)}";
|
||||
var sound = new NetworkWavSound(url);
|
||||
var provider = new CachedWavProvider(sound);
|
||||
var data = AudioPlaybackEngine.Instance.ConvertSound(provider);
|
||||
var resampled = new WdlResamplingSampleProvider(data, AudioPlaybackEngine.Instance.SampleRate);
|
||||
_logger.LogDebug("Fetched TTS audio data.");
|
||||
|
||||
m.Audio = resampled;
|
||||
_player.Ready(m);
|
||||
} catch (COMException e) {
|
||||
_logger.LogError(e, "Failed to send request for TTS (HResult: " + e.HResult + ").");
|
||||
} catch (Exception e) {
|
||||
_logger.LogError(e, "Failed to send request for TTS.");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Task.Run(async () => {
|
||||
while (true) {
|
||||
try {
|
||||
if (cancellationToken.IsCancellationRequested) {
|
||||
_logger.LogWarning("TTS Queue - Cancellation token was canceled.");
|
||||
return;
|
||||
}
|
||||
while (_player.IsEmpty() || _player.Playing != null) {
|
||||
await Task.Delay(200);
|
||||
continue;
|
||||
}
|
||||
var m = _player.ReceiveReady();
|
||||
if (m == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(m.File) && File.Exists(m.File)) {
|
||||
_logger.LogInformation("Playing message: " + m.File);
|
||||
AudioPlaybackEngine.Instance.PlaySound(m.File);
|
||||
continue;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Playing message: " + m.Message);
|
||||
_player.Playing = m.Audio;
|
||||
if (m.Audio != null)
|
||||
AudioPlaybackEngine.Instance.AddMixerInput(m.Audio);
|
||||
} catch (Exception e) {
|
||||
_logger.LogError(e, "Failed to play a TTS audio message");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
StartSavingEmoteCounter();
|
||||
|
||||
_logger.LogInformation("Twitch API client connecting...");
|
||||
await twitchapiclient.Connect();
|
||||
} catch (Exception e) {
|
||||
_logger.LogError(e, "Failed to initialize.");
|
||||
var hermesVersion = await hermes.GetTTSVersion();
|
||||
if (hermesVersion.MajorVersion > TTS.MAJOR_VERSION || hermesVersion.MajorVersion == TTS.MAJOR_VERSION && hermesVersion.MinorVersion > TTS.MINOR_VERSION)
|
||||
{
|
||||
_logger.Information($"A new update for TTS is avaiable! Version {hermesVersion.MajorVersion}.{hermesVersion.MinorVersion} is available at {hermesVersion.Download}");
|
||||
var changes = hermesVersion.Changelog.Split("\n");
|
||||
if (changes != null && changes.Any())
|
||||
_logger.Information("Changelogs:\n - " + string.Join("\n - ", changes) + "\n\n");
|
||||
await Task.Delay(15 * 1000);
|
||||
}
|
||||
Console.ReadLine();
|
||||
|
||||
try
|
||||
{
|
||||
await FetchUserData(user, hermes, seven);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Failed to initialize properly.");
|
||||
await Task.Delay(30 * 1000);
|
||||
}
|
||||
|
||||
var twitchapiclient = await InitializeTwitchApiClient(user.TwitchUsername, user.TwitchUserId.ToString());
|
||||
if (twitchapiclient == null)
|
||||
{
|
||||
await Task.Delay(30 * 1000);
|
||||
return;
|
||||
}
|
||||
|
||||
var emoteSet = await seven.FetchChannelEmoteSet(user.TwitchUserId.ToString());
|
||||
user.SevenEmoteSetId = emoteSet?.Id;
|
||||
|
||||
await InitializeEmotes(seven, emoteSet);
|
||||
await InitializeHermesWebsocket();
|
||||
await InitializeSevenTv(emoteSet.Id);
|
||||
await InitializeObs();
|
||||
|
||||
AudioPlaybackEngine.Instance.AddOnMixerInputEnded((object? s, SampleProviderEventArgs e) =>
|
||||
{
|
||||
if (e.SampleProvider == _player.Playing)
|
||||
{
|
||||
_player.Playing = null;
|
||||
}
|
||||
});
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
_logger.Warning("TTS Buffer - Cancellation token was canceled.");
|
||||
return;
|
||||
}
|
||||
|
||||
var m = _player.ReceiveBuffer();
|
||||
if (m == null)
|
||||
{
|
||||
await Task.Delay(200);
|
||||
continue;
|
||||
}
|
||||
|
||||
string url = $"https://api.streamelements.com/kappa/v2/speech?voice={m.Voice}&text={HttpUtility.UrlEncode(m.Message)}";
|
||||
var sound = new NetworkWavSound(url);
|
||||
var provider = new CachedWavProvider(sound);
|
||||
var data = AudioPlaybackEngine.Instance.ConvertSound(provider);
|
||||
var resampled = new WdlResamplingSampleProvider(data, AudioPlaybackEngine.Instance.SampleRate);
|
||||
_logger.Debug("Fetched TTS audio data.");
|
||||
|
||||
m.Audio = resampled;
|
||||
_player.Ready(m);
|
||||
}
|
||||
catch (COMException e)
|
||||
{
|
||||
_logger.Error(e, "Failed to send request for TTS (HResult: " + e.HResult + ").");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Error(e, "Failed to send request for TTS.");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
_logger.Warning("TTS Queue - Cancellation token was canceled.");
|
||||
return;
|
||||
}
|
||||
while (_player.IsEmpty() || _player.Playing != null)
|
||||
{
|
||||
await Task.Delay(200);
|
||||
continue;
|
||||
}
|
||||
var m = _player.ReceiveReady();
|
||||
if (m == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(m.File) && File.Exists(m.File))
|
||||
{
|
||||
_logger.Information("Playing message: " + m.File);
|
||||
AudioPlaybackEngine.Instance.PlaySound(m.File);
|
||||
continue;
|
||||
}
|
||||
|
||||
_logger.Information("Playing message: " + m.Message);
|
||||
_player.Playing = m.Audio;
|
||||
if (m.Audio != null)
|
||||
AudioPlaybackEngine.Instance.AddMixerInput(m.Audio);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Error(e, "Failed to play a TTS audio message");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
_logger.Information("Twitch websocket client connecting...");
|
||||
await twitchapiclient.Connect();
|
||||
}
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
_logger.LogWarning("Application has stopped due to cancellation token.");
|
||||
_logger.Warning("Application has stopped due to cancellation token.");
|
||||
else
|
||||
_logger.LogWarning("Application has stopped.");
|
||||
_logger.Warning("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;
|
||||
}
|
||||
private async Task FetchUserData(User user, HermesApiClient hermes, SevenApiClient seven)
|
||||
{
|
||||
var hermesAccount = await hermes.FetchHermesAccountDetails();
|
||||
if (hermesAccount == null)
|
||||
throw new Exception("Cannot connect to Hermes. Ensure your token is valid.");
|
||||
|
||||
try {
|
||||
_logger.LogInformation("Initializing hermes websocket client.");
|
||||
user.HermesUserId = hermesAccount.Id;
|
||||
user.HermesUsername = hermesAccount.Username;
|
||||
user.TwitchUsername = hermesAccount.Username;
|
||||
|
||||
var twitchBotToken = await hermes.FetchTwitchBotToken();
|
||||
user.TwitchUserId = long.Parse(twitchBotToken.BroadcasterId);
|
||||
_logger.Information($"Username: {user.TwitchUsername} (id: {user.TwitchUserId})");
|
||||
|
||||
user.DefaultTTSVoice = await hermes.FetchTTSDefaultVoice();
|
||||
_logger.Information("Default Voice: " + user.DefaultTTSVoice);
|
||||
|
||||
var wordFilters = await hermes.FetchTTSWordFilters();
|
||||
user.RegexFilters = wordFilters.ToList();
|
||||
_logger.Information($"{user.RegexFilters.Count()} TTS word filters.");
|
||||
|
||||
var usernameFilters = await hermes.FetchTTSUsernameFilters();
|
||||
user.ChatterFilters = usernameFilters.ToDictionary(e => e.Username, e => e);
|
||||
_logger.Information($"{user.ChatterFilters.Where(f => f.Value.Tag == "blacklisted").Count()} username(s) have been blocked.");
|
||||
_logger.Information($"{user.ChatterFilters.Where(f => f.Value.Tag == "priority").Count()} user(s) have been prioritized.");
|
||||
|
||||
var voicesSelected = await hermes.FetchTTSChatterSelectedVoices();
|
||||
user.VoicesSelected = voicesSelected.ToDictionary(s => s.ChatterId, s => s.Voice);
|
||||
_logger.Information($"{user.VoicesSelected.Count} TTS voices have been selected for specific chatters.");
|
||||
|
||||
var voicesEnabled = await hermes.FetchTTSEnabledVoices();
|
||||
if (voicesEnabled == null || !voicesEnabled.Any())
|
||||
user.VoicesEnabled = new HashSet<string>(new string[] { "Brian" });
|
||||
else
|
||||
user.VoicesEnabled = new HashSet<string>(voicesEnabled.Select(v => v));
|
||||
_logger.Information($"{user.VoicesEnabled.Count} TTS voices have been enabled.");
|
||||
|
||||
var defaultedChatters = voicesSelected.Where(item => item.Voice == null || !user.VoicesEnabled.Contains(item.Voice));
|
||||
_logger.Information($"{defaultedChatters.Count()} chatters will have their TTS voice set to default due to having selected a disabled TTS voice.");
|
||||
}
|
||||
|
||||
private async Task InitializeHermesWebsocket()
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.Information("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}");
|
||||
_logger.Debug($"Attempting to connect to {url}");
|
||||
await hermesClient.ConnectAsync(url);
|
||||
await hermesClient.Send(1, new HermesLoginMessage() {
|
||||
hermesClient.Connected = true;
|
||||
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.");
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
_logger.Warning("Connecting to hermes failed. Skipping hermes websockets.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task InitializeSevenTv() {
|
||||
if (_configuration.Seven?.UserId == null) {
|
||||
_logger.LogDebug("No user id given to 7tv. Skipping 7tv websockets.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
_logger.LogInformation("Initializing 7tv websocket client.");
|
||||
private async Task InitializeSevenTv(string emoteSetId)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.Information("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}");
|
||||
if (string.IsNullOrWhiteSpace(emoteSetId))
|
||||
{
|
||||
_logger.Warning("Could not fetch 7tv emotes.");
|
||||
return;
|
||||
}
|
||||
var url = $"{SevenApiClient.WEBSOCKET_URL}@emote_set.*<object_id={emoteSetId}>";
|
||||
_logger.Debug($"Attempting to connect to {url}");
|
||||
await sevenClient.ConnectAsync($"{url}");
|
||||
} catch (Exception) {
|
||||
_logger.LogWarning("Connecting to 7tv failed. Skipping 7tv websockets.");
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
_logger.Warning("Connecting to 7tv failed. Skipping 7tv websockets.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task InitializeObs() {
|
||||
if (_configuration.Obs == null || string.IsNullOrWhiteSpace(_configuration.Obs.Host) || !_configuration.Obs.Port.HasValue || _configuration.Obs.Port.Value < 0) {
|
||||
_logger.LogDebug("Lacking obs connection info. Skipping obs websockets.");
|
||||
private async Task InitializeObs()
|
||||
{
|
||||
if (_configuration.Obs == null || string.IsNullOrWhiteSpace(_configuration.Obs.Host) || !_configuration.Obs.Port.HasValue || _configuration.Obs.Port.Value < 0)
|
||||
{
|
||||
_logger.Warning("Lacking OBS connection info. Skipping OBS websockets.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
_logger.LogInformation("Initializing obs websocket client.");
|
||||
try
|
||||
{
|
||||
var obsClient = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("obs");
|
||||
var url = $"ws://{_configuration.Obs.Host.Trim()}:{_configuration.Obs.Port}";
|
||||
_logger.LogDebug($"Attempting to connect to {url}");
|
||||
_logger.Debug($"Initializing OBS websocket client. Attempting to connect to {url}");
|
||||
await obsClient.ConnectAsync(url);
|
||||
} catch (Exception) {
|
||||
_logger.LogWarning("Connecting to obs failed. Skipping obs websockets.");
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
_logger.Warning("Connecting to obs failed. Skipping obs websockets.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<HermesClient> InitializeHermes() {
|
||||
// Fetch id and username based on api key given.
|
||||
_logger.LogInformation("Initializing hermes client.");
|
||||
var hermes = _serviceProvider.GetRequiredService<HermesClient>();
|
||||
await hermes.FetchHermesAccountDetails();
|
||||
return hermes;
|
||||
}
|
||||
|
||||
private async Task<TwitchApiClient> InitializeTwitchApiClient(string username) {
|
||||
_logger.LogInformation("Initializing twitch client.");
|
||||
private async Task<TwitchApiClient?> InitializeTwitchApiClient(string username, string broadcasterId)
|
||||
{
|
||||
_logger.Debug("Initializing twitch client.");
|
||||
var twitchapiclient = _serviceProvider.GetRequiredService<TwitchApiClient>();
|
||||
await twitchapiclient.Authorize();
|
||||
if (!await twitchapiclient.Authorize(broadcasterId))
|
||||
{
|
||||
_logger.Error("Cannot connect to Twitch API.");
|
||||
return null;
|
||||
}
|
||||
|
||||
var channels = _configuration.Twitch.Channels ?? [username];
|
||||
_logger.LogInformation("Twitch channels: " + string.Join(", ", channels));
|
||||
_logger.Information("Twitch channels: " + string.Join(", ", channels));
|
||||
twitchapiclient.InitializeClient(username, channels);
|
||||
twitchapiclient.InitializePublisher();
|
||||
|
||||
var handler = _serviceProvider.GetRequiredService<ChatMessageHandler>();
|
||||
twitchapiclient.AddOnNewMessageReceived(async Task (object? s, OnMessageReceivedArgs e) => {
|
||||
var result = await handler.Handle(e);
|
||||
twitchapiclient.AddOnNewMessageReceived(async (object? s, OnMessageReceivedArgs e) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await handler.Handle(e);
|
||||
if (result.Status != MessageStatus.None || result.Emotes == null || !result.Emotes.Any())
|
||||
return;
|
||||
|
||||
var ws = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("hermes");
|
||||
await ws.Send(8, new EmoteUsageMessage()
|
||||
{
|
||||
MessageId = e.ChatMessage.Id,
|
||||
DateTime = DateTime.UtcNow,
|
||||
BroadcasterId = result.BroadcasterId,
|
||||
ChatterId = result.ChatterId,
|
||||
Emotes = result.Emotes
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Unable to send emote usage message.");
|
||||
}
|
||||
});
|
||||
|
||||
return twitchapiclient;
|
||||
}
|
||||
|
||||
private async Task StartSavingEmoteCounter() {
|
||||
Task.Run(async () => {
|
||||
while (true) {
|
||||
try {
|
||||
await Task.Delay(TimeSpan.FromSeconds(300));
|
||||
|
||||
var serializer = new SerializerBuilder()
|
||||
.WithNamingConvention(HyphenatedNamingConvention.Instance)
|
||||
.Build();
|
||||
|
||||
var chathandler = _serviceProvider.GetRequiredService<ChatMessageHandler>();
|
||||
using (TextWriter writer = File.CreateText(_configuration.Emotes.CounterFilePath.Trim()))
|
||||
{
|
||||
await writer.WriteAsync(serializer.Serialize(chathandler._emoteCounter));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
_logger.LogError(e, "Failed to save the emote counter.");
|
||||
}
|
||||
}
|
||||
});
|
||||
private async Task InitializeEmotes(SevenApiClient sevenapi, EmoteSet emoteSet)
|
||||
{
|
||||
var emotes = _serviceProvider.GetRequiredService<EmoteDatabase>();
|
||||
var channelEmotes = emoteSet;
|
||||
var globalEmotes = await sevenapi.FetchGlobalSevenEmotes();
|
||||
|
||||
if (channelEmotes != null && channelEmotes.Emotes.Any())
|
||||
{
|
||||
_logger.Information($"Loaded {channelEmotes.Emotes.Count()} 7tv channel emotes.");
|
||||
foreach (var entry in channelEmotes.Emotes)
|
||||
emotes.Add(entry.Name, entry.Id);
|
||||
}
|
||||
if (globalEmotes != null && globalEmotes.Any())
|
||||
{
|
||||
_logger.Information($"Loaded {globalEmotes.Count()} 7tv global emotes.");
|
||||
foreach (var entry in globalEmotes)
|
||||
emotes.Add(entry.Name, entry.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
using System.Text.Json;
|
||||
using TwitchChatTTS.Helpers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Serilog;
|
||||
using TwitchChatTTS;
|
||||
using TwitchLib.Api.Core.Exceptions;
|
||||
using TwitchLib.Client.Events;
|
||||
@ -14,133 +14,164 @@ using TwitchLib.PubSub.Interfaces;
|
||||
using TwitchLib.Client.Interfaces;
|
||||
using TwitchChatTTS.OBS.Socket;
|
||||
|
||||
public class TwitchApiClient {
|
||||
public class TwitchApiClient
|
||||
{
|
||||
private readonly Configuration _configuration;
|
||||
private readonly ILogger<TwitchApiClient> _logger;
|
||||
private readonly TwitchBotToken _token;
|
||||
private readonly ILogger _logger;
|
||||
private TwitchBotAuth _token;
|
||||
private readonly ITwitchClient _client;
|
||||
private readonly ITwitchPubSub _publisher;
|
||||
private readonly WebClientWrap Web;
|
||||
private readonly WebClientWrap _web;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private bool Initialized;
|
||||
private bool _initialized;
|
||||
private string _broadcasterId;
|
||||
|
||||
|
||||
public TwitchApiClient(
|
||||
Configuration configuration,
|
||||
ILogger<TwitchApiClient> logger,
|
||||
TwitchBotToken token,
|
||||
TwitchBotAuth token,
|
||||
ITwitchClient twitchClient,
|
||||
ITwitchPubSub twitchPublisher,
|
||||
IServiceProvider serviceProvider
|
||||
) {
|
||||
IServiceProvider serviceProvider,
|
||||
ILogger logger
|
||||
)
|
||||
{
|
||||
_configuration = configuration;
|
||||
_logger = logger;
|
||||
_token = token;
|
||||
_client = twitchClient;
|
||||
_publisher = twitchPublisher;
|
||||
_serviceProvider = serviceProvider;
|
||||
Initialized = false;
|
||||
_logger = logger;
|
||||
_initialized = false;
|
||||
|
||||
Web = new WebClientWrap(new JsonSerializerOptions() {
|
||||
_web = new WebClientWrap(new JsonSerializerOptions()
|
||||
{
|
||||
PropertyNameCaseInsensitive = false,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
|
||||
});
|
||||
if (!string.IsNullOrWhiteSpace(_configuration.Hermes?.Token))
|
||||
Web.AddHeader("x-api-key", _configuration.Hermes.Token.Trim());
|
||||
_web.AddHeader("x-api-key", _configuration.Hermes.Token.Trim());
|
||||
}
|
||||
|
||||
public async Task Authorize() {
|
||||
try {
|
||||
var authorize = await Web.GetJson<TwitchBotAuth>("https://hermes.goblincaves.com/api/account/reauthorize");
|
||||
if (authorize != null && _token.BroadcasterId == authorize.BroadcasterId) {
|
||||
public async Task<bool> Authorize(string broadcasterId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var authorize = await _web.GetJson<TwitchBotAuth>("https://hermes.goblincaves.com/api/account/reauthorize");
|
||||
if (authorize != null && broadcasterId == authorize.BroadcasterId)
|
||||
{
|
||||
_token.AccessToken = authorize.AccessToken;
|
||||
_token.RefreshToken = authorize.RefreshToken;
|
||||
_logger.LogInformation("Updated Twitch API tokens.");
|
||||
} else if (authorize != null) {
|
||||
_logger.LogError("Twitch API Authorization failed.");
|
||||
_token.UserId = authorize.UserId;
|
||||
_token.BroadcasterId = authorize.BroadcasterId;
|
||||
_logger.Information("Updated Twitch API tokens.");
|
||||
}
|
||||
} catch (HttpResponseException e) {
|
||||
if (string.IsNullOrWhiteSpace(_configuration.Hermes?.Token))
|
||||
_logger.LogError("No Hermes API key found. Enter it into the configuration file.");
|
||||
else
|
||||
_logger.LogError("Invalid Hermes API key. Double check the token. HTTP Error Code: " + e.HttpResponse.StatusCode);
|
||||
} catch (JsonException) {
|
||||
} catch (Exception e) {
|
||||
_logger.LogError(e, "Failed to authorize to Twitch API.");
|
||||
else if (authorize != null)
|
||||
{
|
||||
_logger.Error("Twitch API Authorization failed: " + authorize.AccessToken + " | " + authorize.RefreshToken + " | " + authorize.UserId + " | " + authorize.BroadcasterId);
|
||||
return false;
|
||||
}
|
||||
_broadcasterId = broadcasterId;
|
||||
return true;
|
||||
}
|
||||
catch (HttpResponseException e)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_configuration.Hermes?.Token))
|
||||
_logger.Error("No Hermes API key found. Enter it into the configuration file.");
|
||||
else
|
||||
_logger.Error("Invalid Hermes API key. Double check the token. HTTP Error Code: " + e.HttpResponse.StatusCode);
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Error(e, "Failed to authorize to Twitch API.");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public async Task Connect() {
|
||||
public async Task Connect()
|
||||
{
|
||||
_client.Connect();
|
||||
await _publisher.ConnectAsync();
|
||||
}
|
||||
|
||||
public void InitializeClient(string username, IEnumerable<string> channels) {
|
||||
public void InitializeClient(string username, IEnumerable<string> channels)
|
||||
{
|
||||
ConnectionCredentials credentials = new ConnectionCredentials(username, _token?.AccessToken);
|
||||
_client.Initialize(credentials, channels.Distinct().ToList());
|
||||
|
||||
if (Initialized) {
|
||||
_logger.LogDebug("Twitch API client has already been initialized.");
|
||||
if (_initialized)
|
||||
{
|
||||
_logger.Debug("Twitch API client has already been initialized.");
|
||||
return;
|
||||
}
|
||||
|
||||
Initialized = true;
|
||||
_initialized = true;
|
||||
|
||||
_client.OnJoinedChannel += async Task (object? s, OnJoinedChannelArgs e) => {
|
||||
_logger.LogInformation("Joined channel: " + e.Channel);
|
||||
_client.OnJoinedChannel += async Task (object? s, OnJoinedChannelArgs e) =>
|
||||
{
|
||||
_logger.Information("Joined channel: " + e.Channel);
|
||||
};
|
||||
|
||||
_client.OnConnected += async Task (object? s, OnConnectedArgs e) => {
|
||||
_logger.LogInformation("-----------------------------------------------------------");
|
||||
_client.OnConnected += async Task (object? s, OnConnectedArgs e) =>
|
||||
{
|
||||
_logger.Information("-----------------------------------------------------------");
|
||||
};
|
||||
|
||||
_client.OnIncorrectLogin += async Task (object? s, OnIncorrectLoginArgs e) => {
|
||||
_logger.LogError(e.Exception, "Incorrect Login on Twitch API client.");
|
||||
_client.OnIncorrectLogin += async Task (object? s, OnIncorrectLoginArgs e) =>
|
||||
{
|
||||
_logger.Error(e.Exception, "Incorrect Login on Twitch API client.");
|
||||
|
||||
_logger.LogInformation("Attempting to re-authorize.");
|
||||
await Authorize();
|
||||
_logger.Information("Attempting to re-authorize.");
|
||||
await Authorize(_broadcasterId);
|
||||
};
|
||||
|
||||
_client.OnConnectionError += async Task (object? s, OnConnectionErrorArgs e) => {
|
||||
_logger.LogError("Connection Error: " + e.Error.Message + " (" + e.Error.GetType().Name + ")");
|
||||
_client.OnConnectionError += async Task (object? s, OnConnectionErrorArgs e) =>
|
||||
{
|
||||
_logger.Error("Connection Error: " + e.Error.Message + " (" + e.Error.GetType().Name + ")");
|
||||
|
||||
_logger.LogInformation("Attempting to re-authorize.");
|
||||
await Authorize();
|
||||
_logger.Information("Attempting to re-authorize.");
|
||||
await Authorize(_broadcasterId);
|
||||
};
|
||||
|
||||
_client.OnError += async Task (object? s, OnErrorEventArgs e) => {
|
||||
_logger.LogError(e.Exception, "Twitch API client error.");
|
||||
_client.OnError += async Task (object? s, OnErrorEventArgs e) =>
|
||||
{
|
||||
_logger.Error(e.Exception, "Twitch API client error.");
|
||||
};
|
||||
}
|
||||
|
||||
public void InitializePublisher() {
|
||||
_publisher.OnPubSubServiceConnected += async (s, e) => {
|
||||
public void InitializePublisher()
|
||||
{
|
||||
_publisher.OnPubSubServiceConnected += async (s, e) =>
|
||||
{
|
||||
_publisher.ListenToChannelPoints(_token.BroadcasterId);
|
||||
_publisher.ListenToFollows(_token.BroadcasterId);
|
||||
|
||||
await _publisher.SendTopicsAsync(_token.AccessToken);
|
||||
_logger.LogInformation("Twitch PubSub has been connected.");
|
||||
_logger.Information("Twitch PubSub has been connected.");
|
||||
};
|
||||
|
||||
_publisher.OnFollow += (s, e) => {
|
||||
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.OnFollow += (s, e) =>
|
||||
{
|
||||
var client = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("obs") as OBSSocketClient;
|
||||
if (_configuration.Twitch?.TtsWhenOffline != true && client?.Live == false)
|
||||
return;
|
||||
|
||||
_logger.LogInformation($"Channel Point Reward Redeemed: {e.RewardRedeemed.Redemption.Reward.Title} (id: {e.RewardRedeemed.Redemption.Id})");
|
||||
_logger.Information("Follow: " + e.DisplayName);
|
||||
};
|
||||
|
||||
if (_configuration.Twitch?.Redeems == null) {
|
||||
_logger.LogDebug("No redeems found in the configuration.");
|
||||
_publisher.OnChannelPointsRewardRedeemed += (s, e) =>
|
||||
{
|
||||
var client = _serviceProvider.GetRequiredKeyedService<SocketClient<WebSocketMessage>>("obs") as OBSSocketClient;
|
||||
if (_configuration.Twitch?.TtsWhenOffline != true && client?.Live == false)
|
||||
return;
|
||||
|
||||
_logger.Information($"Channel Point Reward Redeemed [redeem: {e.RewardRedeemed.Redemption.Reward.Title}][id: {e.RewardRedeemed.Redemption.Id}]");
|
||||
|
||||
if (_configuration.Twitch?.Redeems == null)
|
||||
return;
|
||||
}
|
||||
|
||||
var redeemName = e.RewardRedeemed.Redemption.Reward.Title.ToLower().Trim().Replace(" ", "-");
|
||||
if (!_configuration.Twitch.Redeems.TryGetValue(redeemName, out RedeemConfiguration? redeem))
|
||||
@ -148,19 +179,28 @@ public class TwitchApiClient {
|
||||
|
||||
if (redeem == null)
|
||||
return;
|
||||
|
||||
|
||||
// Write or append to file if needed.
|
||||
var outputFile = string.IsNullOrWhiteSpace(redeem.OutputFilePath) ? null : redeem.OutputFilePath.Trim();
|
||||
if (outputFile == null) {
|
||||
_logger.LogDebug($"No output file was provided for redeem '{e.RewardRedeemed.Redemption.Reward.Title}'.");
|
||||
} else {
|
||||
if (outputFile == null)
|
||||
{
|
||||
_logger.Debug($"No output file was provided for redeem [redeem: {e.RewardRedeemed.Redemption.Reward.Title}][id: {e.RewardRedeemed.Redemption.Id}]");
|
||||
}
|
||||
else
|
||||
{
|
||||
var outputContent = string.IsNullOrWhiteSpace(redeem.OutputContent) ? null : redeem.OutputContent.Trim().Replace("%USER%", e.RewardRedeemed.Redemption.User.DisplayName).Replace("\\n", "\n");
|
||||
if (outputContent == null) {
|
||||
_logger.LogWarning($"No output content was provided for redeem '{e.RewardRedeemed.Redemption.Reward.Title}'.");
|
||||
} else {
|
||||
if (redeem.OutputAppend == true) {
|
||||
if (outputContent == null)
|
||||
{
|
||||
_logger.Warning($"No output content was provided for redeem [redeem: {e.RewardRedeemed.Redemption.Reward.Title}][id: {e.RewardRedeemed.Redemption.Id}]");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (redeem.OutputAppend == true)
|
||||
{
|
||||
File.AppendAllText(outputFile, outputContent + "\n");
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
File.WriteAllText(outputFile, outputContent);
|
||||
}
|
||||
}
|
||||
@ -168,40 +208,23 @@ public class TwitchApiClient {
|
||||
|
||||
// Play audio file if needed.
|
||||
var audioFile = string.IsNullOrWhiteSpace(redeem.AudioFilePath) ? null : redeem.AudioFilePath.Trim();
|
||||
if (audioFile == null) {
|
||||
_logger.LogDebug($"No audio file was provided for redeem '{e.RewardRedeemed.Redemption.Reward.Title}'.");
|
||||
return;
|
||||
if (audioFile == null)
|
||||
{
|
||||
_logger.Debug($"No audio file was provided for redeem [redeem: {e.RewardRedeemed.Redemption.Reward.Title}][id: {e.RewardRedeemed.Redemption.Id}]");
|
||||
}
|
||||
if (!File.Exists(audioFile)) {
|
||||
_logger.LogWarning($"Cannot find audio file @ {audioFile} for redeem '{e.RewardRedeemed.Redemption.Reward.Title}'.");
|
||||
return;
|
||||
else if (!File.Exists(audioFile))
|
||||
{
|
||||
_logger.Warning($"Cannot find audio file [location: {audioFile}] for redeem [redeem: {e.RewardRedeemed.Redemption.Reward.Title}][id: {e.RewardRedeemed.Redemption.Id}]");
|
||||
}
|
||||
else
|
||||
{
|
||||
AudioPlaybackEngine.Instance.PlaySound(audioFile);
|
||||
}
|
||||
|
||||
AudioPlaybackEngine.Instance.PlaySound(audioFile);
|
||||
};
|
||||
|
||||
/*int psConnectionFailures = 0;
|
||||
publisher.OnPubSubServiceError += async (s, e) => {
|
||||
Console.WriteLine("PubSub ran into a service error. Attempting to connect again.");
|
||||
await Task.Delay(Math.Min(3000 + (1 << psConnectionFailures), 120000));
|
||||
var connect = await WebHelper.Get("https://hermes.goblincaves.com/api/account/reauthorize");
|
||||
if ((int) connect.StatusCode == 200 || (int) connect.StatusCode == 201) {
|
||||
psConnectionFailures = 0;
|
||||
} else {
|
||||
psConnectionFailures++;
|
||||
}
|
||||
|
||||
var twitchBotData2 = await WebHelper.GetJson<TwitchBotToken>("https://hermes.goblincaves.com/api/token/bot");
|
||||
if (twitchBotData2 == null) {
|
||||
Console.WriteLine("The API is down. Contact the owner.");
|
||||
return;
|
||||
}
|
||||
twitchBotData.access_token = twitchBotData2.access_token;
|
||||
await pubsub.ConnectAsync();
|
||||
};*/
|
||||
}
|
||||
|
||||
public void AddOnNewMessageReceived(AsyncEventHandler<OnMessageReceivedArgs> handler) {
|
||||
public void AddOnNewMessageReceived(AsyncEventHandler<OnMessageReceivedArgs> handler)
|
||||
{
|
||||
_client.OnMessageReceived += handler;
|
||||
}
|
||||
}
|
@ -10,11 +10,21 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Configuration" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" />
|
||||
<PackageReference Include="NAudio" Version="2.2.1" />
|
||||
<PackageReference Include="NAudio.Extras" Version="2.2.1" />
|
||||
<PackageReference Include="Serilog" Version="4.0.0" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="8.0.2-dev-00338" />
|
||||
<PackageReference Include="Serilog.Enrichers.Thread" Version="4.0.0" />
|
||||
<PackageReference Include="Serilog.Extensions.Hosting" Version="8.0.0" />
|
||||
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.1-dev-10391" />
|
||||
<PackageReference Include="Serilog.Formatting.Compact" Version="3.0.0" />
|
||||
<PackageReference Include="Serilog.Settings.Configuration" Version="8.0.1" />
|
||||
<PackageReference Include="Serilog.Sinks.Async" Version="2.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Debug" Version="3.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="5.0.1-dev-00972" />
|
||||
<PackageReference Include="Serilog.Sinks.RollingFile" Version="3.3.1-dev-00771" />
|
||||
<PackageReference Include="Serilog.Sinks.Trace" Version="4.0.0" />
|
||||
<PackageReference Include="System.Text.Json" Version="8.0.1" />
|
||||
<PackageReference Include="System.Threading.Tasks.Dataflow" Version="8.0.0" />
|
||||
<PackageReference Include="TwitchLib.Api.Core" Version="3.10.0-preview-e47ba7f" />
|
||||
|
28
User.cs
28
User.cs
@ -1,5 +1,6 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.RegularExpressions;
|
||||
using TwitchChatTTS.Hermes;
|
||||
using HermesSocketLibrary.Requests.Messages;
|
||||
|
||||
namespace TwitchChatTTS
|
||||
{
|
||||
@ -7,29 +8,38 @@ namespace TwitchChatTTS
|
||||
{
|
||||
// Hermes user id
|
||||
public string HermesUserId { get; set; }
|
||||
public string HermesUsername { get; set; }
|
||||
public long TwitchUserId { get; set; }
|
||||
public string TwitchUsername { get; set; }
|
||||
|
||||
public string SevenEmoteSetId { get; set; }
|
||||
|
||||
public string DefaultTTSVoice { get; set; }
|
||||
// voice id -> voice name
|
||||
public IDictionary<string, string> VoicesAvailable { get; set; }
|
||||
public IDictionary<string, string> VoicesAvailable { get => _voicesAvailable; set { _voicesAvailable = value; WordFilterRegex = GenerateEnabledVoicesRegex(); } }
|
||||
// chatter/twitch id -> voice name
|
||||
public IDictionary<long, string> VoicesSelected { get; set; }
|
||||
public HashSet<string> VoicesEnabled { get; set; }
|
||||
// voice names
|
||||
public HashSet<string> VoicesEnabled { get => _voicesEnabled; set { _voicesEnabled = value; WordFilterRegex = GenerateEnabledVoicesRegex(); } }
|
||||
|
||||
public IDictionary<string, TTSUsernameFilter> ChatterFilters { get; set; }
|
||||
public IList<TTSWordFilter> RegexFilters { get; set; }
|
||||
public IList<TTSWordFilter> RegexFilters { get; set; }
|
||||
[JsonIgnore]
|
||||
public Regex? WordFilterRegex { get; set; }
|
||||
|
||||
private IDictionary<string, string> _voicesAvailable;
|
||||
private HashSet<string> _voicesEnabled;
|
||||
|
||||
|
||||
public User() {
|
||||
|
||||
public User()
|
||||
{
|
||||
}
|
||||
|
||||
public Regex? GenerateEnabledVoicesRegex() {
|
||||
private Regex? GenerateEnabledVoicesRegex()
|
||||
{
|
||||
if (VoicesAvailable == null || VoicesAvailable.Count() <= 0)
|
||||
return null;
|
||||
|
||||
var enabledVoicesString = string.Join("|", VoicesAvailable.Select(v => v.Value));
|
||||
var enabledVoicesString = string.Join("|", VoicesAvailable.Where(v => VoicesEnabled == null || !VoicesEnabled.Any() || VoicesEnabled.Contains(v.Value)).Select(v => v.Value));
|
||||
return new Regex($@"\b({enabledVoicesString})\:(.*?)(?=\Z|\b(?:{enabledVoicesString})\:)", RegexOptions.IgnoreCase);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user