Fixed command permissions. Moved to using Twitch's EventSub via websockets. Cleaned some code up. Added detection for subscription messages (no TTS), message deletion, full or partial chat clear. Removes messages from TTS queue if applicable. Added command aliases for static parameters. Word filters use compiled regex if possible. Fixed TTS voice deletion.
This commit is contained in:
parent
472bfcee5d
commit
75fcb8e0f8
@ -1,311 +1,310 @@
|
|||||||
using System.Text.RegularExpressions;
|
// using System.Text.RegularExpressions;
|
||||||
using TwitchLib.Client.Events;
|
// using TwitchLib.Client.Events;
|
||||||
using Serilog;
|
// using Serilog;
|
||||||
using TwitchChatTTS;
|
// using TwitchChatTTS;
|
||||||
using TwitchChatTTS.Chat.Commands;
|
// using TwitchChatTTS.Chat.Commands;
|
||||||
using TwitchChatTTS.Hermes.Socket;
|
// using TwitchChatTTS.Hermes.Socket;
|
||||||
using TwitchChatTTS.Chat.Groups.Permissions;
|
// using TwitchChatTTS.Chat.Groups.Permissions;
|
||||||
using TwitchChatTTS.Chat.Groups;
|
// using TwitchChatTTS.Chat.Groups;
|
||||||
using TwitchChatTTS.Chat.Emotes;
|
// using TwitchChatTTS.Chat.Emotes;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
// using Microsoft.Extensions.DependencyInjection;
|
||||||
using CommonSocketLibrary.Common;
|
// using CommonSocketLibrary.Common;
|
||||||
using CommonSocketLibrary.Abstract;
|
// using CommonSocketLibrary.Abstract;
|
||||||
using TwitchChatTTS.OBS.Socket;
|
// using TwitchChatTTS.OBS.Socket;
|
||||||
|
|
||||||
|
|
||||||
public class ChatMessageHandler
|
// public class ChatMessageHandler
|
||||||
{
|
// {
|
||||||
private readonly User _user;
|
// private readonly User _user;
|
||||||
private readonly TTSPlayer _player;
|
// private readonly TTSPlayer _player;
|
||||||
private readonly CommandManager _commands;
|
// private readonly CommandManager _commands;
|
||||||
private readonly IGroupPermissionManager _permissionManager;
|
// private readonly IGroupPermissionManager _permissionManager;
|
||||||
private readonly IChatterGroupManager _chatterGroupManager;
|
// private readonly IChatterGroupManager _chatterGroupManager;
|
||||||
private readonly IEmoteDatabase _emotes;
|
// private readonly IEmoteDatabase _emotes;
|
||||||
private readonly OBSSocketClient _obs;
|
// private readonly OBSSocketClient _obs;
|
||||||
private readonly HermesSocketClient _hermes;
|
// private readonly HermesSocketClient _hermes;
|
||||||
private readonly Configuration _configuration;
|
// private readonly Configuration _configuration;
|
||||||
|
|
||||||
private readonly ILogger _logger;
|
// private readonly ILogger _logger;
|
||||||
|
|
||||||
private Regex _sfxRegex;
|
// private Regex _sfxRegex;
|
||||||
private HashSet<long> _chatters;
|
// private HashSet<long> _chatters;
|
||||||
|
|
||||||
public HashSet<long> Chatters { get => _chatters; set => _chatters = value; }
|
// public HashSet<long> Chatters { get => _chatters; set => _chatters = value; }
|
||||||
|
|
||||||
|
|
||||||
public ChatMessageHandler(
|
// public ChatMessageHandler(
|
||||||
User user,
|
// User user,
|
||||||
TTSPlayer player,
|
// TTSPlayer player,
|
||||||
CommandManager commands,
|
// CommandManager commands,
|
||||||
IGroupPermissionManager permissionManager,
|
// IGroupPermissionManager permissionManager,
|
||||||
IChatterGroupManager chatterGroupManager,
|
// IChatterGroupManager chatterGroupManager,
|
||||||
IEmoteDatabase emotes,
|
// IEmoteDatabase emotes,
|
||||||
[FromKeyedServices("hermes")] SocketClient<WebSocketMessage> hermes,
|
// [FromKeyedServices("hermes")] SocketClient<WebSocketMessage> hermes,
|
||||||
[FromKeyedServices("obs")] SocketClient<WebSocketMessage> obs,
|
// [FromKeyedServices("obs")] SocketClient<WebSocketMessage> obs,
|
||||||
Configuration configuration,
|
// Configuration configuration,
|
||||||
ILogger logger
|
// ILogger logger
|
||||||
)
|
// )
|
||||||
{
|
// {
|
||||||
_user = user;
|
// _user = user;
|
||||||
_player = player;
|
// _player = player;
|
||||||
_commands = commands;
|
// _commands = commands;
|
||||||
_permissionManager = permissionManager;
|
// _permissionManager = permissionManager;
|
||||||
_chatterGroupManager = chatterGroupManager;
|
// _chatterGroupManager = chatterGroupManager;
|
||||||
_emotes = emotes;
|
// _emotes = emotes;
|
||||||
_obs = (obs as OBSSocketClient)!;
|
// _obs = (obs as OBSSocketClient)!;
|
||||||
_hermes = (hermes as HermesSocketClient)!;
|
// _hermes = (hermes as HermesSocketClient)!;
|
||||||
_configuration = configuration;
|
// _configuration = configuration;
|
||||||
_logger = logger;
|
// _logger = logger;
|
||||||
|
|
||||||
_chatters = new HashSet<long>();
|
// _chatters = new HashSet<long>();
|
||||||
_sfxRegex = new Regex(@"\(([A-Za-z0-9_-]+)\)");
|
// _sfxRegex = new Regex(@"\(([A-Za-z0-9_-]+)\)");
|
||||||
}
|
// }
|
||||||
|
|
||||||
|
|
||||||
public async Task<MessageResult> Handle(OnMessageReceivedArgs e)
|
// public async Task<MessageResult> Handle(OnMessageReceivedArgs e)
|
||||||
{
|
// {
|
||||||
var m = e.ChatMessage;
|
// var m = e.ChatMessage;
|
||||||
|
|
||||||
if (!_hermes.Ready)
|
// if (_hermes.Connected && !_hermes.Ready)
|
||||||
{
|
// {
|
||||||
_logger.Debug($"TTS is not yet ready. Ignoring chat messages [message id: {m.Id}]");
|
// _logger.Debug($"TTS is not yet ready. Ignoring chat messages [message id: {m.Id}]");
|
||||||
return new MessageResult(MessageStatus.NotReady, -1, -1);
|
// return new MessageResult(MessageStatus.NotReady, -1, -1);
|
||||||
}
|
// }
|
||||||
if (_configuration.Twitch?.TtsWhenOffline != true && !_obs.Streaming)
|
// if (_configuration.Twitch?.TtsWhenOffline != true && !_obs.Streaming)
|
||||||
{
|
// {
|
||||||
_logger.Debug($"OBS is not streaming. Ignoring chat messages [message id: {m.Id}]");
|
// _logger.Debug($"OBS is not streaming. Ignoring chat messages [message id: {m.Id}]");
|
||||||
return new MessageResult(MessageStatus.NotReady, -1, -1);
|
// return new MessageResult(MessageStatus.NotReady, -1, -1);
|
||||||
}
|
// }
|
||||||
|
|
||||||
|
|
||||||
var msg = e.ChatMessage.Message;
|
// var msg = e.ChatMessage.Message;
|
||||||
var chatterId = long.Parse(m.UserId);
|
// var chatterId = long.Parse(m.UserId);
|
||||||
var tasks = new List<Task>();
|
// var tasks = new List<Task>();
|
||||||
|
|
||||||
var checks = new bool[] { true, m.IsSubscriber, m.IsVip, m.IsModerator, m.IsBroadcaster };
|
// var checks = new bool[] { true, m.IsSubscriber, m.IsVip, m.IsModerator, m.IsBroadcaster };
|
||||||
var defaultGroups = new string[] { "everyone", "subscribers", "vip", "moderators", "broadcaster" };
|
// var defaultGroups = new string[] { "everyone", "subscribers", "vip", "moderators", "broadcaster" };
|
||||||
var customGroups = _chatterGroupManager.GetGroupNamesFor(chatterId);
|
// var customGroups = _chatterGroupManager.GetGroupNamesFor(chatterId);
|
||||||
var groups = defaultGroups.Where((e, i) => checks[i]).Union(customGroups);
|
// var groups = defaultGroups.Where((e, i) => checks[i]).Union(customGroups);
|
||||||
|
|
||||||
try
|
// try
|
||||||
{
|
// {
|
||||||
var commandResult = await _commands.Execute(msg, m, groups);
|
// var commandResult = await _commands.Execute(msg, m, groups);
|
||||||
if (commandResult != ChatCommandResult.Unknown)
|
// if (commandResult != ChatCommandResult.Unknown)
|
||||||
return new MessageResult(MessageStatus.Command, -1, -1);
|
// return new MessageResult(MessageStatus.Command, -1, -1);
|
||||||
}
|
// }
|
||||||
catch (Exception ex)
|
// catch (Exception ex)
|
||||||
{
|
// {
|
||||||
_logger.Error(ex, $"Failed executing a chat command [message: {msg}][chatter: {m.Username}][chatter id: {m.UserId}][message id: {m.Id}]");
|
// _logger.Error(ex, $"Failed executing a chat command [message: {msg}][chatter: {m.Username}][chatter id: {m.UserId}][message id: {m.Id}]");
|
||||||
}
|
// }
|
||||||
|
|
||||||
var permissionPath = "tts.chat.messages.read";
|
// var permissionPath = "tts.chat.messages.read";
|
||||||
if (!string.IsNullOrWhiteSpace(m.CustomRewardId))
|
// if (!string.IsNullOrWhiteSpace(m.CustomRewardId))
|
||||||
permissionPath = "tts.chat.redemptions.read";
|
// permissionPath = "tts.chat.redemptions.read";
|
||||||
|
|
||||||
var permission = chatterId == _user.OwnerId ? true : _permissionManager.CheckIfAllowed(groups, permissionPath);
|
// var permission = chatterId == _user.OwnerId ? true : _permissionManager.CheckIfAllowed(groups, permissionPath);
|
||||||
if (permission != true)
|
// if (permission != true)
|
||||||
{
|
// {
|
||||||
_logger.Debug($"Blocked message by {m.Username}: {msg}");
|
// _logger.Debug($"Blocked message by {m.Username}: {msg}");
|
||||||
return new MessageResult(MessageStatus.Blocked, -1, -1);
|
// return new MessageResult(MessageStatus.Blocked, -1, -1);
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (_obs.Streaming && !_chatters.Contains(chatterId))
|
// if (_obs.Streaming && !_chatters.Contains(chatterId))
|
||||||
{
|
// {
|
||||||
tasks.Add(_hermes.SendChatterDetails(chatterId, m.Username));
|
// tasks.Add(_hermes.SendChatterDetails(chatterId, m.Username));
|
||||||
_chatters.Add(chatterId);
|
// _chatters.Add(chatterId);
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Filter highly repetitive words (like emotes) from the message.
|
// // Filter highly repetitive words (like emotes) from the message.
|
||||||
int totalEmoteUsed = 0;
|
// int totalEmoteUsed = 0;
|
||||||
var emotesUsed = new HashSet<string>();
|
// var emotesUsed = new HashSet<string>();
|
||||||
var words = msg.Split(' ');
|
// var words = msg.Split(' ');
|
||||||
var wordCounter = new Dictionary<string, int>();
|
// var wordCounter = new Dictionary<string, int>();
|
||||||
string filteredMsg = string.Empty;
|
// string filteredMsg = string.Empty;
|
||||||
var newEmotes = new Dictionary<string, string>();
|
// var newEmotes = new Dictionary<string, string>();
|
||||||
foreach (var w in words)
|
// foreach (var w in words)
|
||||||
{
|
// {
|
||||||
if (wordCounter.ContainsKey(w))
|
// if (wordCounter.ContainsKey(w))
|
||||||
wordCounter[w]++;
|
// wordCounter[w]++;
|
||||||
else
|
// else
|
||||||
wordCounter.Add(w, 1);
|
// wordCounter.Add(w, 1);
|
||||||
|
|
||||||
var emoteId = _emotes.Get(w);
|
// var emoteId = _emotes.Get(w);
|
||||||
if (emoteId == null)
|
// if (emoteId == null)
|
||||||
{
|
// {
|
||||||
emoteId = m.EmoteSet.Emotes.FirstOrDefault(e => e.Name == w)?.Id;
|
// emoteId = m.EmoteSet.Emotes.FirstOrDefault(e => e.Name == w)?.Id;
|
||||||
if (emoteId != null)
|
// if (emoteId != null)
|
||||||
{
|
// {
|
||||||
newEmotes.Add(emoteId, w);
|
// newEmotes.Add(emoteId, w);
|
||||||
_emotes.Add(w, emoteId);
|
// _emotes.Add(w, emoteId);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
if (emoteId != null)
|
// if (emoteId != null)
|
||||||
{
|
// {
|
||||||
emotesUsed.Add(emoteId);
|
// emotesUsed.Add(emoteId);
|
||||||
totalEmoteUsed++;
|
// totalEmoteUsed++;
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (wordCounter[w] <= 4 && (emoteId == null || totalEmoteUsed <= 5))
|
// if (wordCounter[w] <= 4 && (emoteId == null || totalEmoteUsed <= 5))
|
||||||
filteredMsg += w + " ";
|
// filteredMsg += w + " ";
|
||||||
}
|
// }
|
||||||
if (_obs.Streaming && newEmotes.Any())
|
// if (_obs.Streaming && newEmotes.Any())
|
||||||
tasks.Add(_hermes.SendEmoteDetails(newEmotes));
|
// tasks.Add(_hermes.SendEmoteDetails(newEmotes));
|
||||||
msg = filteredMsg;
|
// msg = filteredMsg;
|
||||||
|
|
||||||
// Replace filtered words.
|
// // Replace filtered words.
|
||||||
if (_user.RegexFilters != null)
|
// if (_user.RegexFilters != null)
|
||||||
{
|
// {
|
||||||
foreach (var wf in _user.RegexFilters)
|
// foreach (var wf in _user.RegexFilters)
|
||||||
{
|
// {
|
||||||
if (wf.Search == null || wf.Replace == null)
|
// if (wf.Search == null || wf.Replace == null)
|
||||||
continue;
|
// continue;
|
||||||
|
|
||||||
if (wf.IsRegex)
|
// if (wf.IsRegex)
|
||||||
{
|
// {
|
||||||
try
|
// try
|
||||||
{
|
// {
|
||||||
var regex = new Regex(wf.Search);
|
// var regex = new Regex(wf.Search);
|
||||||
msg = regex.Replace(msg, wf.Replace);
|
// msg = regex.Replace(msg, wf.Replace);
|
||||||
continue;
|
// continue;
|
||||||
}
|
// }
|
||||||
catch (Exception)
|
// catch (Exception)
|
||||||
{
|
// {
|
||||||
wf.IsRegex = false;
|
// wf.IsRegex = false;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
msg = msg.Replace(wf.Search, wf.Replace);
|
// msg = msg.Replace(wf.Search, wf.Replace);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Determine the priority of this message
|
// // Determine the priority of this message
|
||||||
int priority = _chatterGroupManager.GetPriorityFor(groups) + m.SubscribedMonthCount * (m.IsSubscriber ? 10 : 5);
|
// int priority = _chatterGroupManager.GetPriorityFor(groups) + m.SubscribedMonthCount * (m.IsSubscriber ? 10 : 5);
|
||||||
|
|
||||||
// Determine voice selected.
|
// // Determine voice selected.
|
||||||
string voiceSelected = _user.DefaultTTSVoice;
|
// string voiceSelected = _user.DefaultTTSVoice;
|
||||||
if (_user.VoicesSelected?.ContainsKey(chatterId) == true)
|
// if (_user.VoicesSelected?.ContainsKey(chatterId) == true)
|
||||||
{
|
// {
|
||||||
var voiceId = _user.VoicesSelected[chatterId];
|
// var voiceId = _user.VoicesSelected[chatterId];
|
||||||
if (_user.VoicesAvailable.TryGetValue(voiceId, out string? voiceName) && voiceName != null)
|
// if (_user.VoicesAvailable.TryGetValue(voiceId, out string? voiceName) && voiceName != null)
|
||||||
{
|
// {
|
||||||
if (_user.VoicesEnabled.Contains(voiceName) || chatterId == _user.OwnerId || m.IsStaff)
|
// if (_user.VoicesEnabled.Contains(voiceName) || chatterId == _user.OwnerId || m.IsStaff)
|
||||||
{
|
// {
|
||||||
voiceSelected = voiceName;
|
// voiceSelected = voiceName;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Determine additional voices used
|
// // Determine additional voices used
|
||||||
var matches = _user.WordFilterRegex?.Matches(msg).ToArray();
|
// var matches = _user.WordFilterRegex?.Matches(msg).ToArray();
|
||||||
if (matches == null || matches.FirstOrDefault() == null || matches.First().Index < 0)
|
// if (matches == null || matches.FirstOrDefault() == null || matches.First().Index < 0)
|
||||||
{
|
// {
|
||||||
HandlePartialMessage(priority, voiceSelected, msg.Trim(), e);
|
// HandlePartialMessage(priority, voiceSelected, msg.Trim(), e);
|
||||||
return new MessageResult(MessageStatus.None, _user.TwitchUserId, chatterId, emotesUsed);
|
// return new MessageResult(MessageStatus.None, _user.TwitchUserId, chatterId, emotesUsed);
|
||||||
}
|
// }
|
||||||
|
|
||||||
HandlePartialMessage(priority, voiceSelected, msg.Substring(0, matches.First().Index).Trim(), e);
|
// HandlePartialMessage(priority, voiceSelected, msg.Substring(0, matches.First().Index).Trim(), e);
|
||||||
foreach (Match match in matches)
|
// foreach (Match match in matches)
|
||||||
{
|
// {
|
||||||
var message = match.Groups[2].ToString();
|
// var message = match.Groups[2].ToString();
|
||||||
if (string.IsNullOrWhiteSpace(message))
|
// if (string.IsNullOrWhiteSpace(message))
|
||||||
continue;
|
// continue;
|
||||||
|
|
||||||
var voice = match.Groups[1].ToString();
|
// var voice = match.Groups[1].ToString();
|
||||||
voice = voice[0].ToString().ToUpper() + voice.Substring(1).ToLower();
|
// voice = voice[0].ToString().ToUpper() + voice.Substring(1).ToLower();
|
||||||
HandlePartialMessage(priority, voice, message.Trim(), e);
|
// HandlePartialMessage(priority, voice, message.Trim(), e);
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (tasks.Any())
|
// if (tasks.Any())
|
||||||
await Task.WhenAll(tasks);
|
// await Task.WhenAll(tasks);
|
||||||
|
|
||||||
return new MessageResult(MessageStatus.None, _user.TwitchUserId, chatterId, emotesUsed);
|
// return new MessageResult(MessageStatus.None, _user.TwitchUserId, chatterId, emotesUsed);
|
||||||
}
|
// }
|
||||||
|
|
||||||
private void HandlePartialMessage(int priority, string voice, string message, OnMessageReceivedArgs e)
|
// private void HandlePartialMessage(int priority, string voice, string message, OnMessageReceivedArgs e)
|
||||||
{
|
// {
|
||||||
if (string.IsNullOrWhiteSpace(message))
|
// if (string.IsNullOrWhiteSpace(message))
|
||||||
return;
|
// return;
|
||||||
|
|
||||||
var m = e.ChatMessage;
|
// var m = e.ChatMessage;
|
||||||
var parts = _sfxRegex.Split(message);
|
// var parts = _sfxRegex.Split(message);
|
||||||
var badgesString = string.Join(", ", e.ChatMessage.Badges.Select(b => b.Key + " = " + b.Value));
|
// var badgesString = string.Join(", ", e.ChatMessage.Badges.Select(b => b.Key + " = " + b.Value));
|
||||||
|
|
||||||
if (parts.Length == 1)
|
// if (parts.Length == 1)
|
||||||
{
|
// {
|
||||||
_logger.Information($"Username: {m.Username}; User ID: {m.UserId}; Voice: {voice}; Priority: {priority}; Message: {message}; Month: {m.SubscribedMonthCount}; Reward Id: {m.CustomRewardId}; {badgesString}");
|
// _logger.Information($"Username: {m.Username}; User ID: {m.UserId}; Voice: {voice}; Priority: {priority}; Message: {message}; Month: {m.SubscribedMonthCount}; Reward Id: {m.CustomRewardId}; {badgesString}");
|
||||||
_player.Add(new TTSMessage()
|
// _player.Add(new TTSMessage()
|
||||||
{
|
// {
|
||||||
Voice = voice,
|
// Voice = voice,
|
||||||
Message = message,
|
// Message = message,
|
||||||
Moderator = m.IsModerator,
|
// Timestamp = DateTime.UtcNow,
|
||||||
Timestamp = DateTime.UtcNow,
|
// Username = m.Username,
|
||||||
Username = m.Username,
|
// //Bits = m.Bits,
|
||||||
Bits = m.Bits,
|
// Badges = e.Badges,
|
||||||
Badges = m.Badges,
|
// Priority = priority
|
||||||
Priority = priority
|
// });
|
||||||
});
|
// return;
|
||||||
return;
|
// }
|
||||||
}
|
|
||||||
|
|
||||||
var sfxMatches = _sfxRegex.Matches(message);
|
// var sfxMatches = _sfxRegex.Matches(message);
|
||||||
var sfxStart = sfxMatches.FirstOrDefault()?.Index ?? message.Length;
|
// 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 sfxMatch = sfxMatches[i];
|
||||||
var sfxName = sfxMatch.Groups[1]?.ToString()?.ToLower();
|
// 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];
|
// parts[i * 2 + 2] = parts[i * 2] + " (" + parts[i * 2 + 1] + ")" + parts[i * 2 + 2];
|
||||||
continue;
|
// continue;
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(parts[i * 2]))
|
// 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}");
|
// _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()
|
// _player.Add(new TTSMessage()
|
||||||
{
|
// {
|
||||||
Voice = voice,
|
// Voice = voice,
|
||||||
Message = parts[i * 2],
|
// Message = parts[i * 2],
|
||||||
Moderator = m.IsModerator,
|
// Moderator = m.IsModerator,
|
||||||
Timestamp = DateTime.UtcNow,
|
// Timestamp = DateTime.UtcNow,
|
||||||
Username = m.Username,
|
// Username = m.Username,
|
||||||
Bits = m.Bits,
|
// Bits = m.Bits,
|
||||||
Badges = m.Badges,
|
// Badges = m.Badges,
|
||||||
Priority = priority
|
// Priority = priority
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
|
|
||||||
_logger.Information($"Username: {m.Username}; User ID: {m.UserId}; Voice: {voice}; Priority: {priority}; SFX: {sfxName}; Month: {m.SubscribedMonthCount}; {badgesString}");
|
// _logger.Information($"Username: {m.Username}; User ID: {m.UserId}; Voice: {voice}; Priority: {priority}; SFX: {sfxName}; Month: {m.SubscribedMonthCount}; {badgesString}");
|
||||||
_player.Add(new TTSMessage()
|
// _player.Add(new TTSMessage()
|
||||||
{
|
// {
|
||||||
Voice = voice,
|
// Voice = voice,
|
||||||
Message = sfxName,
|
// Message = sfxName,
|
||||||
File = $"sfx/{sfxName}.mp3",
|
// File = $"sfx/{sfxName}.mp3",
|
||||||
Moderator = m.IsModerator,
|
// Moderator = m.IsModerator,
|
||||||
Timestamp = DateTime.UtcNow,
|
// Timestamp = DateTime.UtcNow,
|
||||||
Username = m.Username,
|
// Username = m.Username,
|
||||||
Bits = m.Bits,
|
// Bits = m.Bits,
|
||||||
Badges = m.Badges,
|
// Badges = m.Badges,
|
||||||
Priority = priority
|
// Priority = priority
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(parts.Last()))
|
// 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}");
|
// _logger.Information($"Username: {m.Username}; User ID: {m.UserId}; Voice: {voice}; Priority: {priority}; Message: {parts.Last()}; Month: {m.SubscribedMonthCount}; {badgesString}");
|
||||||
_player.Add(new TTSMessage()
|
// _player.Add(new TTSMessage()
|
||||||
{
|
// {
|
||||||
Voice = voice,
|
// Voice = voice,
|
||||||
Message = parts.Last(),
|
// Message = parts.Last(),
|
||||||
Moderator = m.IsModerator,
|
// Moderator = m.IsModerator,
|
||||||
Timestamp = DateTime.UtcNow,
|
// Timestamp = DateTime.UtcNow,
|
||||||
Username = m.Username,
|
// Username = m.Username,
|
||||||
Bits = m.Bits,
|
// Bits = m.Bits,
|
||||||
Badges = m.Badges,
|
// Badges = m.Badges,
|
||||||
Priority = priority
|
// Priority = priority
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
@ -1,17 +1,18 @@
|
|||||||
using TwitchChatTTS.Hermes.Socket;
|
using TwitchChatTTS.Hermes.Socket;
|
||||||
using TwitchLib.Client.Models;
|
using TwitchChatTTS.Twitch.Socket.Messages;
|
||||||
using static TwitchChatTTS.Chat.Commands.TTSCommands;
|
using static TwitchChatTTS.Chat.Commands.TTSCommands;
|
||||||
|
|
||||||
namespace TwitchChatTTS.Chat.Commands
|
namespace TwitchChatTTS.Chat.Commands
|
||||||
{
|
{
|
||||||
public interface IChatCommand {
|
public interface IChatCommand
|
||||||
|
{
|
||||||
string Name { get; }
|
string Name { get; }
|
||||||
void Build(ICommandBuilder builder);
|
void Build(ICommandBuilder builder);
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IChatPartialCommand {
|
public interface IChatPartialCommand
|
||||||
|
{
|
||||||
bool AcceptCustomPermission { get; }
|
bool AcceptCustomPermission { get; }
|
||||||
bool CheckDefaultPermissions(ChatMessage message);
|
Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client);
|
||||||
Task Execute(IDictionary<string, string> values, ChatMessage message, HermesSocketClient client);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,5 +1,6 @@
|
|||||||
using Serilog;
|
using Serilog;
|
||||||
using TwitchChatTTS.Chat.Commands.Parameters;
|
using TwitchChatTTS.Chat.Commands.Parameters;
|
||||||
|
using TwitchChatTTS.Twitch.Socket.Messages;
|
||||||
|
|
||||||
namespace TwitchChatTTS.Chat.Commands
|
namespace TwitchChatTTS.Chat.Commands
|
||||||
{
|
{
|
||||||
@ -8,10 +9,13 @@ namespace TwitchChatTTS.Chat.Commands
|
|||||||
public interface ICommandBuilder
|
public interface ICommandBuilder
|
||||||
{
|
{
|
||||||
ICommandSelector Build();
|
ICommandSelector Build();
|
||||||
|
ICommandBuilder AddPermission(string path);
|
||||||
|
ICommandBuilder AddAlias(string alias, string child);
|
||||||
void Clear();
|
void Clear();
|
||||||
ICommandBuilder CreateCommandTree(string name, Action<ICommandBuilder> callback);
|
ICommandBuilder CreateCommandTree(string name, Action<ICommandBuilder> callback);
|
||||||
ICommandBuilder CreateCommand(IChatPartialCommand command);
|
ICommandBuilder CreateCommand(IChatPartialCommand command);
|
||||||
ICommandBuilder CreateStaticInputParameter(string value, Action<ICommandBuilder> callback, bool optional = false);
|
ICommandBuilder CreateStaticInputParameter(string value, Action<ICommandBuilder> callback, bool optional = false);
|
||||||
|
ICommandBuilder CreateMentionParameter(string name, bool enabled, bool optional = false);
|
||||||
ICommandBuilder CreateObsTransformationParameter(string name, bool optional = false);
|
ICommandBuilder CreateObsTransformationParameter(string name, bool optional = false);
|
||||||
ICommandBuilder CreateStateParameter(string name, bool optional = false);
|
ICommandBuilder CreateStateParameter(string name, bool optional = false);
|
||||||
ICommandBuilder CreateUnvalidatedParameter(string name, bool optional = false);
|
ICommandBuilder CreateUnvalidatedParameter(string name, bool optional = false);
|
||||||
@ -37,6 +41,25 @@ namespace TwitchChatTTS.Chat.Commands
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public ICommandBuilder AddPermission(string path)
|
||||||
|
{
|
||||||
|
if (_current == _root)
|
||||||
|
throw new Exception("Cannot add permissions without a command name.");
|
||||||
|
|
||||||
|
_current.AddPermission(path);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ICommandBuilder AddAlias(string alias, string child) {
|
||||||
|
if (_current == _root)
|
||||||
|
throw new Exception("Cannot add aliases without a command name.");
|
||||||
|
if (_current.Children == null || !_current.Children.Any())
|
||||||
|
throw new Exception("Cannot add alias if this has no parameter.");
|
||||||
|
|
||||||
|
_current.AddAlias(alias, child);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public ICommandSelector Build()
|
public ICommandSelector Build()
|
||||||
{
|
{
|
||||||
return new CommandSelector(_root);
|
return new CommandSelector(_root);
|
||||||
@ -89,6 +112,19 @@ namespace TwitchChatTTS.Chat.Commands
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ICommandBuilder CreateMentionParameter(string name, bool enabled, bool optional = false)
|
||||||
|
{
|
||||||
|
if (_root == _current)
|
||||||
|
throw new Exception("Cannot create a parameter without a command name.");
|
||||||
|
if (optional && _current.IsRequired() && _current.Command == null)
|
||||||
|
throw new Exception("Cannot create a optional parameter without giving the command to the last node with required parameter.");
|
||||||
|
|
||||||
|
var node = _current.CreateUserInput(new MentionParameter(name, optional));
|
||||||
|
_logger.Debug($"Creating obs transformation parameter '{name}'");
|
||||||
|
_current = node;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public ICommandBuilder CreateObsTransformationParameter(string name, bool optional = false)
|
public ICommandBuilder CreateObsTransformationParameter(string name, bool optional = false)
|
||||||
{
|
{
|
||||||
if (_root == _current)
|
if (_root == _current)
|
||||||
@ -164,9 +200,8 @@ namespace TwitchChatTTS.Chat.Commands
|
|||||||
|
|
||||||
public interface ICommandSelector
|
public interface ICommandSelector
|
||||||
{
|
{
|
||||||
CommandSelectorResult GetBestMatch(string[] args);
|
CommandSelectorResult GetBestMatch(string[] args, ChannelChatMessage message);
|
||||||
IDictionary<string, CommandParameter> GetNonStaticArguments(string[] args, string path);
|
IDictionary<string, CommandParameter> GetNonStaticArguments(string[] args, string path);
|
||||||
CommandValidationResult Validate(string[] args, string path);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class CommandSelector : ICommandSelector
|
public sealed class CommandSelector : ICommandSelector
|
||||||
@ -178,67 +213,36 @@ namespace TwitchChatTTS.Chat.Commands
|
|||||||
_root = root;
|
_root = root;
|
||||||
}
|
}
|
||||||
|
|
||||||
public CommandSelectorResult GetBestMatch(string[] args)
|
public CommandSelectorResult GetBestMatch(string[] args, ChannelChatMessage message)
|
||||||
{
|
{
|
||||||
return GetBestMatch(_root, args, null, string.Empty);
|
return GetBestMatch(_root, message, args, null, string.Empty, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private CommandSelectorResult GetBestMatch(CommandNode node, IEnumerable<string> args, IChatPartialCommand? match, string path)
|
private CommandSelectorResult GetBestMatch(CommandNode node, ChannelChatMessage message, IEnumerable<string> args, IChatPartialCommand? match, string path, string[]? permissions)
|
||||||
{
|
{
|
||||||
if (node == null || !args.Any())
|
if (node == null || !args.Any())
|
||||||
return new CommandSelectorResult(match, path);
|
return new CommandSelectorResult(match, path, permissions);
|
||||||
if (!node.Children.Any())
|
if (!node.Children.Any())
|
||||||
return new CommandSelectorResult(node.Command ?? match, path);
|
return new CommandSelectorResult(node.Command ?? match, path, permissions);
|
||||||
|
|
||||||
var argument = args.First();
|
var argument = args.First();
|
||||||
var argumentLower = argument.ToLower();
|
var argumentLower = argument.ToLower();
|
||||||
foreach (var child in node.Children)
|
foreach (var child in node.Children)
|
||||||
{
|
{
|
||||||
|
var perms = child.Permissions != null ? (permissions ?? []).Union(child.Permissions).Distinct().ToArray() : permissions;
|
||||||
if (child.Parameter.GetType() == typeof(StaticParameter))
|
if (child.Parameter.GetType() == typeof(StaticParameter))
|
||||||
{
|
{
|
||||||
if (child.Parameter.Name.ToLower() == argumentLower)
|
if (child.Parameter.Name.ToLower() == argumentLower)
|
||||||
{
|
return GetBestMatch(child, message, args.Skip(1), child.Command ?? match, (path.Length == 0 ? string.Empty : path + ".") + child.Parameter.Name.ToLower(), perms);
|
||||||
return GetBestMatch(child, args.Skip(1), child.Command ?? match, (path.Length == 0 ? string.Empty : path + ".") + child.Parameter.Name.ToLower());
|
|
||||||
}
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if ((!child.Parameter.Optional || child.Parameter.Validate(argument, message)) && child.Command != null)
|
||||||
return GetBestMatch(child, args.Skip(1), child.Command ?? match, (path.Length == 0 ? string.Empty : path + ".") + "*");
|
return GetBestMatch(child, message, args.Skip(1), child.Command, (path.Length == 0 ? string.Empty : path + ".") + "*", perms);
|
||||||
|
if (!child.Parameter.Optional)
|
||||||
|
return GetBestMatch(child, message, args.Skip(1), match, (path.Length == 0 ? string.Empty : path + ".") + "*", permissions);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new CommandSelectorResult(match, path);
|
return new CommandSelectorResult(match, path, permissions);
|
||||||
}
|
|
||||||
|
|
||||||
public CommandValidationResult Validate(string[] args, string path)
|
|
||||||
{
|
|
||||||
CommandNode? current = _root;
|
|
||||||
var parts = path.Split('.');
|
|
||||||
if (args.Length < parts.Length)
|
|
||||||
throw new Exception($"Command path too long for the number of arguments passed in [path: {path}][parts: {parts.Length}][args count: {args.Length}]");
|
|
||||||
|
|
||||||
for (var i = 0; i < parts.Length; i++)
|
|
||||||
{
|
|
||||||
var part = parts[i];
|
|
||||||
if (part == "*")
|
|
||||||
{
|
|
||||||
current = current.Children.FirstOrDefault(n => n.Parameter.GetType() != typeof(StaticParameter));
|
|
||||||
if (current == null)
|
|
||||||
throw new Exception($"Cannot find command path [path: {path}][subpath: {part}]");
|
|
||||||
|
|
||||||
if (!current.Parameter.Validate(args[i]))
|
|
||||||
{
|
|
||||||
return new CommandValidationResult(false, args[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
current = current.Children.FirstOrDefault(n => n.Parameter.GetType() == typeof(StaticParameter) && n.Parameter.Name == part);
|
|
||||||
if (current == null)
|
|
||||||
throw new Exception($"Cannot find command path [path: {path}][subpath: {part}]");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new CommandValidationResult(true, null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public IDictionary<string, CommandParameter> GetNonStaticArguments(string[] args, string path)
|
public IDictionary<string, CommandParameter> GetNonStaticArguments(string[] args, string path)
|
||||||
@ -276,11 +280,13 @@ namespace TwitchChatTTS.Chat.Commands
|
|||||||
{
|
{
|
||||||
public IChatPartialCommand? Command { get; set; }
|
public IChatPartialCommand? Command { get; set; }
|
||||||
public string Path { get; set; }
|
public string Path { get; set; }
|
||||||
|
public string[]? Permissions { get; set; }
|
||||||
|
|
||||||
public CommandSelectorResult(IChatPartialCommand? command, string path)
|
public CommandSelectorResult(IChatPartialCommand? command, string path, string[]? permissions)
|
||||||
{
|
{
|
||||||
Command = command;
|
Command = command;
|
||||||
Path = path;
|
Path = path;
|
||||||
|
Permissions = permissions;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -300,6 +306,7 @@ namespace TwitchChatTTS.Chat.Commands
|
|||||||
{
|
{
|
||||||
public IChatPartialCommand? Command { get; private set; }
|
public IChatPartialCommand? Command { get; private set; }
|
||||||
public CommandParameter Parameter { get; }
|
public CommandParameter Parameter { get; }
|
||||||
|
public string[]? Permissions { get; private set; }
|
||||||
public IList<CommandNode> Children { get => _children.AsReadOnly(); }
|
public IList<CommandNode> Children { get => _children.AsReadOnly(); }
|
||||||
|
|
||||||
private IList<CommandNode> _children;
|
private IList<CommandNode> _children;
|
||||||
@ -308,9 +315,34 @@ namespace TwitchChatTTS.Chat.Commands
|
|||||||
{
|
{
|
||||||
Parameter = parameter;
|
Parameter = parameter;
|
||||||
_children = new List<CommandNode>();
|
_children = new List<CommandNode>();
|
||||||
|
Permissions = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void AddPermission(string path)
|
||||||
|
{
|
||||||
|
if (Permissions == null)
|
||||||
|
Permissions = [path];
|
||||||
|
else
|
||||||
|
Permissions = Permissions.Union([path]).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandNode AddAlias(string alias, string child) {
|
||||||
|
var target = _children.FirstOrDefault(c => c.Parameter.Name == child);
|
||||||
|
if (target == null)
|
||||||
|
throw new Exception($"Cannot find child parameter [parameter: {child}][alias: {alias}]");
|
||||||
|
if (target.Parameter.GetType() != typeof(StaticParameter))
|
||||||
|
throw new Exception("Command aliases can only be used on static parameters.");
|
||||||
|
if (Children.FirstOrDefault(n => n.Parameter.Name == alias) != null)
|
||||||
|
throw new Exception("Failed to create a command alias - name is already in use.");
|
||||||
|
|
||||||
|
var clone = target.MemberwiseClone() as CommandNode;
|
||||||
|
var node = new CommandNode(new StaticParameter(alias, alias, target.Parameter.Optional));
|
||||||
|
node._children = target._children;
|
||||||
|
_children.Add(node);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public CommandNode CreateCommand(IChatPartialCommand command)
|
public CommandNode CreateCommand(IChatPartialCommand command)
|
||||||
{
|
{
|
||||||
if (Command != null)
|
if (Command != null)
|
||||||
|
@ -5,7 +5,7 @@ using Microsoft.Extensions.DependencyInjection;
|
|||||||
using Serilog;
|
using Serilog;
|
||||||
using TwitchChatTTS.Chat.Groups.Permissions;
|
using TwitchChatTTS.Chat.Groups.Permissions;
|
||||||
using TwitchChatTTS.Hermes.Socket;
|
using TwitchChatTTS.Hermes.Socket;
|
||||||
using TwitchLib.Client.Models;
|
using TwitchChatTTS.Twitch.Socket.Messages;
|
||||||
using static TwitchChatTTS.Chat.Commands.TTSCommands;
|
using static TwitchChatTTS.Chat.Commands.TTSCommands;
|
||||||
|
|
||||||
namespace TwitchChatTTS.Chat.Commands
|
namespace TwitchChatTTS.Chat.Commands
|
||||||
@ -44,7 +44,7 @@ namespace TwitchChatTTS.Chat.Commands
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public async Task<ChatCommandResult> Execute(string arg, ChatMessage message, IEnumerable<string> groups)
|
public async Task<ChatCommandResult> Execute(string arg, ChannelChatMessage message, IEnumerable<string> groups)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(arg))
|
if (string.IsNullOrWhiteSpace(arg))
|
||||||
return ChatCommandResult.Unknown;
|
return ChatCommandResult.Unknown;
|
||||||
@ -62,7 +62,7 @@ namespace TwitchChatTTS.Chat.Commands
|
|||||||
string[] args = parts.ToArray();
|
string[] args = parts.ToArray();
|
||||||
string com = args.First().ToLower();
|
string com = args.First().ToLower();
|
||||||
|
|
||||||
CommandSelectorResult selectorResult = _commandSelector.GetBestMatch(args);
|
CommandSelectorResult selectorResult = _commandSelector.GetBestMatch(args, message);
|
||||||
if (selectorResult.Command == null)
|
if (selectorResult.Command == null)
|
||||||
{
|
{
|
||||||
_logger.Warning($"Could not match '{arg}' to any command.");
|
_logger.Warning($"Could not match '{arg}' to any command.");
|
||||||
@ -71,31 +71,27 @@ namespace TwitchChatTTS.Chat.Commands
|
|||||||
|
|
||||||
// Check if command can be executed by this chatter.
|
// Check if command can be executed by this chatter.
|
||||||
var command = selectorResult.Command;
|
var command = selectorResult.Command;
|
||||||
long chatterId = long.Parse(message.UserId);
|
long chatterId = long.Parse(message.ChatterUserId);
|
||||||
if (chatterId != _user.OwnerId)
|
if (chatterId != _user.OwnerId)
|
||||||
{
|
{
|
||||||
var executable = command.AcceptCustomPermission ? CanExecute(chatterId, groups, com) : null;
|
bool executable = command.AcceptCustomPermission ? CanExecute(chatterId, groups, $"tts.command.{com}", selectorResult.Permissions) : false;
|
||||||
if (executable == false)
|
if (!executable)
|
||||||
{
|
{
|
||||||
_logger.Debug($"Denied permission to use command [chatter id: {chatterId}][command: {com}]");
|
_logger.Debug($"Denied permission to use command [chatter id: {chatterId}][command: {com}]");
|
||||||
return ChatCommandResult.Permission;
|
return ChatCommandResult.Permission;
|
||||||
}
|
}
|
||||||
else if (executable == null && !command.CheckDefaultPermissions(message))
|
|
||||||
{
|
|
||||||
_logger.Debug($"Chatter is missing default permission to execute command named '{com}' [args: {arg}][command type: {command.GetType().Name}][chatter: {message.Username}][chatter id: {message.UserId}]");
|
|
||||||
return ChatCommandResult.Permission;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the arguments are correct.
|
// Check if the arguments are valid.
|
||||||
var arguments = _commandSelector.GetNonStaticArguments(args, selectorResult.Path);
|
var arguments = _commandSelector.GetNonStaticArguments(args, selectorResult.Path);
|
||||||
foreach (var entry in arguments)
|
foreach (var entry in arguments)
|
||||||
{
|
{
|
||||||
var parameter = entry.Value;
|
var parameter = entry.Value;
|
||||||
var argument = entry.Key;
|
var argument = entry.Key;
|
||||||
if (!parameter.Validate(argument))
|
// Optional parameters were validated while fetching this command.
|
||||||
|
if (!parameter.Optional && !parameter.Validate(argument, message))
|
||||||
{
|
{
|
||||||
_logger.Warning($"Command failed due to an argument being invalid [argument name: {parameter.Name}][argument value: {argument}][arguments: {arg}][command type: {command.GetType().Name}][chatter: {message.Username}][chatter id: {message.UserId}]");
|
_logger.Warning($"Command failed due to an argument being invalid [argument name: {parameter.Name}][argument value: {argument}][arguments: {arg}][command type: {command.GetType().Name}][chatter: {message.ChatterUserLogin}][chatter id: {message.ChatterUserId}]");
|
||||||
return ChatCommandResult.Syntax;
|
return ChatCommandResult.Syntax;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -107,18 +103,18 @@ namespace TwitchChatTTS.Chat.Commands
|
|||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
_logger.Error(e, $"Command '{arg}' failed [args: {arg}][command type: {command.GetType().Name}][chatter: {message.Username}][chatter id: {message.UserId}]");
|
_logger.Error(e, $"Command '{arg}' failed [args: {arg}][command type: {command.GetType().Name}][chatter: {message.ChatterUserLogin}][chatter id: {message.ChatterUserId}]");
|
||||||
return ChatCommandResult.Fail;
|
return ChatCommandResult.Fail;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.Information($"Executed the {com} command [args: {arg}][command type: {command.GetType().Name}][chatter: {message.Username}][chatter id: {message.UserId}]");
|
_logger.Information($"Executed the {com} command [args: {arg}][command type: {command.GetType().Name}][chatter: {message.ChatterUserLogin}][chatter id: {message.ChatterUserId}]");
|
||||||
return ChatCommandResult.Success;
|
return ChatCommandResult.Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool? CanExecute(long chatterId, IEnumerable<string> groups, string path)
|
private bool CanExecute(long chatterId, IEnumerable<string> groups, string path, string[]? additionalPaths)
|
||||||
{
|
{
|
||||||
_logger.Debug($"Checking for permission [chatter id: {chatterId}][group: {string.Join(", ", groups)}][path: {path}]");
|
_logger.Debug($"Checking for permission [chatter id: {chatterId}][group: {string.Join(", ", groups)}][path: {path}]{(additionalPaths != null ? "[paths: " + string.Join('|', additionalPaths) + "]" : string.Empty)}");
|
||||||
return _permissionManager.CheckIfAllowed(groups, path);
|
return _permissionManager.CheckIfAllowed(groups, path) != false && (additionalPaths == null || additionalPaths.All(p => _permissionManager.CheckIfAllowed(groups, p) != false));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
88
Chat/Commands/Limits/CommandLimitManager.cs
Normal file
88
Chat/Commands/Limits/CommandLimitManager.cs
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
namespace TwitchChatTTS.Chat.Commands.Limits
|
||||||
|
{
|
||||||
|
public interface ICommandLimitManager
|
||||||
|
{
|
||||||
|
|
||||||
|
bool HasReachedLimit(long chatterId, string name, string group);
|
||||||
|
void RemoveUsageLimit(string name, string group);
|
||||||
|
void SetUsageLimit(int count, TimeSpan span, string name, string group);
|
||||||
|
bool TryUse(long chatterId, string name, string group);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CommandLimitManager : ICommandLimitManager
|
||||||
|
{
|
||||||
|
// group + name -> chatter id -> usage
|
||||||
|
private readonly IDictionary<string, IDictionary<long, Usage>> _usages;
|
||||||
|
// group + name -> limit
|
||||||
|
private readonly IDictionary<string, Limit> _limits;
|
||||||
|
|
||||||
|
|
||||||
|
public CommandLimitManager()
|
||||||
|
{
|
||||||
|
_usages = new Dictionary<string, IDictionary<long, Usage>>();
|
||||||
|
_limits = new Dictionary<string, Limit>();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public bool HasReachedLimit(long chatterId, string name, string group)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveUsageLimit(string name, string group)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetUsageLimit(int count, TimeSpan span, string name, string group)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryUse(long chatterId, string name, string group)
|
||||||
|
{
|
||||||
|
var path = $"{group}.{name}";
|
||||||
|
if (!_limits.TryGetValue(path, out var limit))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (!_usages.TryGetValue(path, out var groupUsage))
|
||||||
|
{
|
||||||
|
groupUsage = new Dictionary<long, Usage>();
|
||||||
|
_usages.Add(path, groupUsage);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!groupUsage.TryGetValue(chatterId, out var usage))
|
||||||
|
{
|
||||||
|
usage = new Usage()
|
||||||
|
{
|
||||||
|
Usages = new long[limit.Count],
|
||||||
|
Index = 0
|
||||||
|
};
|
||||||
|
groupUsage.Add(chatterId, usage);
|
||||||
|
}
|
||||||
|
|
||||||
|
int first = (usage.Index + 1) % limit.Count;
|
||||||
|
long timestamp = DateTime.UtcNow.Ticks / TimeSpan.TicksPerMillisecond;
|
||||||
|
if (timestamp - usage.Usages[first] < limit.Span)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
usage.Usages[usage.Index] = timestamp;
|
||||||
|
usage.Index = first;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Usage
|
||||||
|
{
|
||||||
|
public long[] Usages { get; set; }
|
||||||
|
public int Index { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct Limit
|
||||||
|
{
|
||||||
|
public int Count { get; set; }
|
||||||
|
public int Span { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -5,7 +5,7 @@ using Serilog;
|
|||||||
using TwitchChatTTS.Hermes.Socket;
|
using TwitchChatTTS.Hermes.Socket;
|
||||||
using TwitchChatTTS.OBS.Socket;
|
using TwitchChatTTS.OBS.Socket;
|
||||||
using TwitchChatTTS.OBS.Socket.Data;
|
using TwitchChatTTS.OBS.Socket.Data;
|
||||||
using TwitchLib.Client.Models;
|
using TwitchChatTTS.Twitch.Socket.Messages;
|
||||||
using static TwitchChatTTS.Chat.Commands.TTSCommands;
|
using static TwitchChatTTS.Chat.Commands.TTSCommands;
|
||||||
|
|
||||||
namespace TwitchChatTTS.Chat.Commands
|
namespace TwitchChatTTS.Chat.Commands
|
||||||
@ -71,12 +71,7 @@ namespace TwitchChatTTS.Chat.Commands
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool CheckDefaultPermissions(ChatMessage message)
|
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
|
||||||
{
|
|
||||||
return message.IsModerator || message.IsBroadcaster;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Execute(IDictionary<string, string> values, ChatMessage message, HermesSocketClient client)
|
|
||||||
{
|
{
|
||||||
string sceneName = values["sceneName"];
|
string sceneName = values["sceneName"];
|
||||||
string sourceName = values["sourceName"];
|
string sourceName = values["sourceName"];
|
||||||
@ -102,12 +97,7 @@ namespace TwitchChatTTS.Chat.Commands
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool CheckDefaultPermissions(ChatMessage message)
|
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
|
||||||
{
|
|
||||||
return message.IsModerator || message.IsBroadcaster;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Execute(IDictionary<string, string> values, ChatMessage message, HermesSocketClient client)
|
|
||||||
{
|
{
|
||||||
string sceneName = values["sceneName"];
|
string sceneName = values["sceneName"];
|
||||||
string sourceName = values["sourceName"];
|
string sourceName = values["sourceName"];
|
||||||
@ -143,12 +133,7 @@ namespace TwitchChatTTS.Chat.Commands
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool CheckDefaultPermissions(ChatMessage message)
|
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
|
||||||
{
|
|
||||||
return message.IsModerator || message.IsBroadcaster;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Execute(IDictionary<string, string> values, ChatMessage message, HermesSocketClient client)
|
|
||||||
{
|
{
|
||||||
string sceneName = values["sceneName"];
|
string sceneName = values["sceneName"];
|
||||||
string sourceName = values["sourceName"];
|
string sourceName = values["sourceName"];
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
|
using TwitchChatTTS.Twitch.Socket.Messages;
|
||||||
|
|
||||||
namespace TwitchChatTTS.Chat.Commands.Parameters
|
namespace TwitchChatTTS.Chat.Commands.Parameters
|
||||||
{
|
{
|
||||||
public abstract class CommandParameter : ICloneable
|
public abstract class CommandParameter
|
||||||
{
|
{
|
||||||
public string Name { get; }
|
public string Name { get; }
|
||||||
public bool Optional { get; }
|
public bool Optional { get; }
|
||||||
@ -11,10 +13,6 @@ namespace TwitchChatTTS.Chat.Commands.Parameters
|
|||||||
Optional = optional;
|
Optional = optional;
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract bool Validate(string value);
|
public abstract bool Validate(string value, ChannelChatMessage message);
|
||||||
|
|
||||||
public object Clone() {
|
|
||||||
return (CommandParameter) MemberwiseClone();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
16
Chat/Commands/Parameters/MentionParameter.cs
Normal file
16
Chat/Commands/Parameters/MentionParameter.cs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
using TwitchChatTTS.Twitch.Socket.Messages;
|
||||||
|
|
||||||
|
namespace TwitchChatTTS.Chat.Commands.Parameters
|
||||||
|
{
|
||||||
|
public class MentionParameter : CommandParameter
|
||||||
|
{
|
||||||
|
public MentionParameter(string name, bool optional = false) : base(name, optional)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Validate(string value, ChannelChatMessage message)
|
||||||
|
{
|
||||||
|
return value.StartsWith('@') && message.Message.Fragments.Any(f => f.Text == value && f.Mention != null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,5 @@
|
|||||||
|
using TwitchChatTTS.Twitch.Socket.Messages;
|
||||||
|
|
||||||
namespace TwitchChatTTS.Chat.Commands.Parameters
|
namespace TwitchChatTTS.Chat.Commands.Parameters
|
||||||
{
|
{
|
||||||
public class OBSTransformationParameter : CommandParameter
|
public class OBSTransformationParameter : CommandParameter
|
||||||
@ -8,7 +10,7 @@ namespace TwitchChatTTS.Chat.Commands.Parameters
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool Validate(string value)
|
public override bool Validate(string value, ChannelChatMessage message)
|
||||||
{
|
{
|
||||||
return _values.Contains(value.ToLower());
|
return _values.Contains(value.ToLower());
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
using TwitchChatTTS.Twitch.Socket.Messages;
|
||||||
|
|
||||||
namespace TwitchChatTTS.Chat.Commands.Parameters
|
namespace TwitchChatTTS.Chat.Commands.Parameters
|
||||||
{
|
{
|
||||||
public class StateParameter : CommandParameter
|
public class StateParameter : CommandParameter
|
||||||
@ -8,7 +10,7 @@ namespace TwitchChatTTS.Chat.Commands.Parameters
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool Validate(string value)
|
public override bool Validate(string value, ChannelChatMessage message)
|
||||||
{
|
{
|
||||||
return _values.Contains(value.ToLower());
|
return _values.Contains(value.ToLower());
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
using TwitchChatTTS.Twitch.Socket.Messages;
|
||||||
|
|
||||||
namespace TwitchChatTTS.Chat.Commands.Parameters
|
namespace TwitchChatTTS.Chat.Commands.Parameters
|
||||||
{
|
{
|
||||||
public class StaticParameter : CommandParameter
|
public class StaticParameter : CommandParameter
|
||||||
@ -11,7 +13,7 @@ namespace TwitchChatTTS.Chat.Commands.Parameters
|
|||||||
_value = value.ToLower();
|
_value = value.ToLower();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool Validate(string value)
|
public override bool Validate(string value, ChannelChatMessage message)
|
||||||
{
|
{
|
||||||
return _value == value.ToLower();
|
return _value == value.ToLower();
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
using TwitchChatTTS.Twitch.Socket.Messages;
|
||||||
|
|
||||||
namespace TwitchChatTTS.Chat.Commands.Parameters
|
namespace TwitchChatTTS.Chat.Commands.Parameters
|
||||||
{
|
{
|
||||||
public class TTSVoiceNameParameter : CommandParameter
|
public class TTSVoiceNameParameter : CommandParameter
|
||||||
@ -11,7 +13,7 @@ namespace TwitchChatTTS.Chat.Commands.Parameters
|
|||||||
_user = user;
|
_user = user;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool Validate(string value)
|
public override bool Validate(string value, ChannelChatMessage message)
|
||||||
{
|
{
|
||||||
if (_user.VoicesAvailable == null)
|
if (_user.VoicesAvailable == null)
|
||||||
return false;
|
return false;
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
using TwitchChatTTS.Twitch.Socket.Messages;
|
||||||
|
|
||||||
namespace TwitchChatTTS.Chat.Commands.Parameters
|
namespace TwitchChatTTS.Chat.Commands.Parameters
|
||||||
{
|
{
|
||||||
public class UnvalidatedParameter : CommandParameter
|
public class UnvalidatedParameter : CommandParameter
|
||||||
@ -6,7 +8,7 @@ namespace TwitchChatTTS.Chat.Commands.Parameters
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool Validate(string value)
|
public override bool Validate(string value, ChannelChatMessage message)
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ using Microsoft.Extensions.DependencyInjection;
|
|||||||
using Serilog;
|
using Serilog;
|
||||||
using TwitchChatTTS.Hermes.Socket;
|
using TwitchChatTTS.Hermes.Socket;
|
||||||
using TwitchChatTTS.OBS.Socket;
|
using TwitchChatTTS.OBS.Socket;
|
||||||
using TwitchLib.Client.Models;
|
using TwitchChatTTS.Twitch.Socket.Messages;
|
||||||
using static TwitchChatTTS.Chat.Commands.TTSCommands;
|
using static TwitchChatTTS.Chat.Commands.TTSCommands;
|
||||||
|
|
||||||
namespace TwitchChatTTS.Chat.Commands
|
namespace TwitchChatTTS.Chat.Commands
|
||||||
@ -44,12 +44,7 @@ namespace TwitchChatTTS.Chat.Commands
|
|||||||
{
|
{
|
||||||
public bool AcceptCustomPermission { get => true; }
|
public bool AcceptCustomPermission { get => true; }
|
||||||
|
|
||||||
public bool CheckDefaultPermissions(ChatMessage message)
|
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
|
||||||
{
|
|
||||||
return message.IsModerator || message.IsBroadcaster;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Execute(IDictionary<string, string> values, ChatMessage message, HermesSocketClient client)
|
|
||||||
{
|
{
|
||||||
await client.FetchEnabledTTSVoices();
|
await client.FetchEnabledTTSVoices();
|
||||||
}
|
}
|
||||||
@ -59,12 +54,7 @@ namespace TwitchChatTTS.Chat.Commands
|
|||||||
{
|
{
|
||||||
public bool AcceptCustomPermission { get => true; }
|
public bool AcceptCustomPermission { get => true; }
|
||||||
|
|
||||||
public bool CheckDefaultPermissions(ChatMessage message)
|
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
|
||||||
{
|
|
||||||
return message.IsModerator || message.IsBroadcaster;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Execute(IDictionary<string, string> values, ChatMessage message, HermesSocketClient client)
|
|
||||||
{
|
{
|
||||||
await client.FetchTTSWordFilters();
|
await client.FetchTTSWordFilters();
|
||||||
}
|
}
|
||||||
@ -74,12 +64,7 @@ namespace TwitchChatTTS.Chat.Commands
|
|||||||
{
|
{
|
||||||
public bool AcceptCustomPermission { get => true; }
|
public bool AcceptCustomPermission { get => true; }
|
||||||
|
|
||||||
public bool CheckDefaultPermissions(ChatMessage message)
|
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
|
||||||
{
|
|
||||||
return message.IsModerator || message.IsBroadcaster;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Execute(IDictionary<string, string> values, ChatMessage message, HermesSocketClient client)
|
|
||||||
{
|
{
|
||||||
await client.FetchTTSChatterVoices();
|
await client.FetchTTSChatterVoices();
|
||||||
}
|
}
|
||||||
@ -89,12 +74,7 @@ namespace TwitchChatTTS.Chat.Commands
|
|||||||
{
|
{
|
||||||
public bool AcceptCustomPermission { get => true; }
|
public bool AcceptCustomPermission { get => true; }
|
||||||
|
|
||||||
public bool CheckDefaultPermissions(ChatMessage message)
|
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
|
||||||
{
|
|
||||||
return message.IsModerator || message.IsBroadcaster;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Execute(IDictionary<string, string> values, ChatMessage message, HermesSocketClient client)
|
|
||||||
{
|
{
|
||||||
await client.FetchDefaultTTSVoice();
|
await client.FetchDefaultTTSVoice();
|
||||||
}
|
}
|
||||||
@ -104,12 +84,7 @@ namespace TwitchChatTTS.Chat.Commands
|
|||||||
{
|
{
|
||||||
public bool AcceptCustomPermission { get => true; }
|
public bool AcceptCustomPermission { get => true; }
|
||||||
|
|
||||||
public bool CheckDefaultPermissions(ChatMessage message)
|
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
|
||||||
{
|
|
||||||
return message.IsModerator || message.IsBroadcaster;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Execute(IDictionary<string, string> values, ChatMessage message, HermesSocketClient client)
|
|
||||||
{
|
{
|
||||||
await client.FetchRedemptions();
|
await client.FetchRedemptions();
|
||||||
}
|
}
|
||||||
@ -127,12 +102,7 @@ namespace TwitchChatTTS.Chat.Commands
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool CheckDefaultPermissions(ChatMessage message)
|
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
|
||||||
{
|
|
||||||
return message.IsModerator || message.IsBroadcaster;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Execute(IDictionary<string, string> values, ChatMessage message, HermesSocketClient client)
|
|
||||||
{
|
{
|
||||||
_obsManager.ClearCache();
|
_obsManager.ClearCache();
|
||||||
_logger.Information("Cleared the cache used for OBS.");
|
_logger.Information("Cleared the cache used for OBS.");
|
||||||
@ -144,20 +114,10 @@ namespace TwitchChatTTS.Chat.Commands
|
|||||||
|
|
||||||
public bool AcceptCustomPermission { get => true; }
|
public bool AcceptCustomPermission { get => true; }
|
||||||
|
|
||||||
public bool CheckDefaultPermissions(ChatMessage message)
|
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
|
||||||
{
|
|
||||||
return message.IsModerator || message.IsBroadcaster;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Execute(IDictionary<string, string> values, ChatMessage message, HermesSocketClient client)
|
|
||||||
{
|
{
|
||||||
await client.FetchPermissions();
|
await client.FetchPermissions();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool CheckDefaultPermissions(ChatMessage message)
|
|
||||||
{
|
|
||||||
return message.IsModerator || message.IsBroadcaster;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,6 +1,6 @@
|
|||||||
using Serilog;
|
using Serilog;
|
||||||
using TwitchChatTTS.Hermes.Socket;
|
using TwitchChatTTS.Hermes.Socket;
|
||||||
using TwitchLib.Client.Models;
|
using TwitchChatTTS.Twitch.Socket.Messages;
|
||||||
using static TwitchChatTTS.Chat.Commands.TTSCommands;
|
using static TwitchChatTTS.Chat.Commands.TTSCommands;
|
||||||
|
|
||||||
namespace TwitchChatTTS.Chat.Commands
|
namespace TwitchChatTTS.Chat.Commands
|
||||||
@ -8,11 +8,13 @@ namespace TwitchChatTTS.Chat.Commands
|
|||||||
public class SkipCommand : IChatCommand
|
public class SkipCommand : IChatCommand
|
||||||
{
|
{
|
||||||
private readonly TTSPlayer _player;
|
private readonly TTSPlayer _player;
|
||||||
|
private readonly AudioPlaybackEngine _playback;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
public SkipCommand(TTSPlayer ttsPlayer, ILogger logger)
|
public SkipCommand(TTSPlayer player, AudioPlaybackEngine playback, ILogger logger)
|
||||||
{
|
{
|
||||||
_player = ttsPlayer;
|
_player = player;
|
||||||
|
_playback = playback;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -24,40 +26,38 @@ namespace TwitchChatTTS.Chat.Commands
|
|||||||
{
|
{
|
||||||
b.CreateStaticInputParameter("all", b =>
|
b.CreateStaticInputParameter("all", b =>
|
||||||
{
|
{
|
||||||
b.CreateCommand(new TTSPlayerSkipAllCommand(_player, _logger));
|
b.CreateCommand(new TTSPlayerSkipAllCommand(_player, _playback, _logger));
|
||||||
}).CreateCommand(new TTSPlayerSkipCommand(_player, _logger));
|
}).CreateCommand(new TTSPlayerSkipCommand(_player, _playback, _logger));
|
||||||
});
|
});
|
||||||
builder.CreateCommandTree("skipall", b => {
|
builder.CreateCommandTree("skipall", b =>
|
||||||
b.CreateCommand(new TTSPlayerSkipAllCommand(_player, _logger));
|
{
|
||||||
|
b.CreateCommand(new TTSPlayerSkipAllCommand(_player, _playback, _logger));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private sealed class TTSPlayerSkipCommand : IChatPartialCommand
|
private sealed class TTSPlayerSkipCommand : IChatPartialCommand
|
||||||
{
|
{
|
||||||
private readonly TTSPlayer _ttsPlayer;
|
private readonly TTSPlayer _player;
|
||||||
|
private readonly AudioPlaybackEngine _playback;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
public bool AcceptCustomPermission { get => true; }
|
public bool AcceptCustomPermission { get => true; }
|
||||||
|
|
||||||
public TTSPlayerSkipCommand(TTSPlayer ttsPlayer, ILogger logger)
|
public TTSPlayerSkipCommand(TTSPlayer player, AudioPlaybackEngine playback, ILogger logger)
|
||||||
{
|
{
|
||||||
_ttsPlayer = ttsPlayer;
|
_player = player;
|
||||||
|
_playback = playback;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool CheckDefaultPermissions(ChatMessage message)
|
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
|
||||||
{
|
{
|
||||||
return message.IsModerator || message.IsVip || message.IsBroadcaster;
|
if (_player.Playing == null)
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Execute(IDictionary<string, string> values, ChatMessage message, HermesSocketClient client)
|
|
||||||
{
|
|
||||||
if (_ttsPlayer.Playing == null)
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
AudioPlaybackEngine.Instance.RemoveMixerInput(_ttsPlayer.Playing);
|
_playback.RemoveMixerInput(_player.Playing.Audio!);
|
||||||
_ttsPlayer.Playing = null;
|
_player.Playing = null;
|
||||||
|
|
||||||
_logger.Information("Skipped current tts.");
|
_logger.Information("Skipped current tts.");
|
||||||
}
|
}
|
||||||
@ -65,31 +65,28 @@ namespace TwitchChatTTS.Chat.Commands
|
|||||||
|
|
||||||
private sealed class TTSPlayerSkipAllCommand : IChatPartialCommand
|
private sealed class TTSPlayerSkipAllCommand : IChatPartialCommand
|
||||||
{
|
{
|
||||||
private readonly TTSPlayer _ttsPlayer;
|
private readonly TTSPlayer _player;
|
||||||
|
private readonly AudioPlaybackEngine _playback;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
public bool AcceptCustomPermission { get => true; }
|
public bool AcceptCustomPermission { get => true; }
|
||||||
|
|
||||||
public TTSPlayerSkipAllCommand(TTSPlayer ttsPlayer, ILogger logger)
|
public TTSPlayerSkipAllCommand(TTSPlayer player, AudioPlaybackEngine playback, ILogger logger)
|
||||||
{
|
{
|
||||||
_ttsPlayer = ttsPlayer;
|
_player = player;
|
||||||
|
_playback = playback;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool CheckDefaultPermissions(ChatMessage message)
|
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
|
||||||
{
|
{
|
||||||
return message.IsModerator || message.IsVip || message.IsBroadcaster;
|
_player.RemoveAll();
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Execute(IDictionary<string, string> values, ChatMessage message, HermesSocketClient client)
|
if (_player.Playing == null)
|
||||||
{
|
|
||||||
_ttsPlayer.RemoveAll();
|
|
||||||
|
|
||||||
if (_ttsPlayer.Playing == null)
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
AudioPlaybackEngine.Instance.RemoveMixerInput(_ttsPlayer.Playing);
|
_playback.RemoveMixerInput(_player.Playing.Audio!);
|
||||||
_ttsPlayer.Playing = null;
|
_player.Playing = null;
|
||||||
|
|
||||||
_logger.Information("Skipped all queued and playing tts.");
|
_logger.Information("Skipped all queued and playing tts.");
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
using Serilog;
|
using Serilog;
|
||||||
using TwitchChatTTS.Hermes.Socket;
|
using TwitchChatTTS.Hermes.Socket;
|
||||||
using TwitchLib.Client.Models;
|
using TwitchChatTTS.Twitch.Socket.Messages;
|
||||||
using static TwitchChatTTS.Chat.Commands.TTSCommands;
|
using static TwitchChatTTS.Chat.Commands.TTSCommands;
|
||||||
|
|
||||||
namespace TwitchChatTTS.Chat.Commands
|
namespace TwitchChatTTS.Chat.Commands
|
||||||
@ -28,41 +28,30 @@ namespace TwitchChatTTS.Chat.Commands
|
|||||||
b.CreateVoiceNameParameter("voiceName", false)
|
b.CreateVoiceNameParameter("voiceName", false)
|
||||||
.CreateCommand(new AddTTSVoiceCommand(_user, _logger));
|
.CreateCommand(new AddTTSVoiceCommand(_user, _logger));
|
||||||
})
|
})
|
||||||
.CreateStaticInputParameter("del", b =>
|
.AddAlias("insert", "add")
|
||||||
{
|
|
||||||
b.CreateVoiceNameParameter("voiceName", true)
|
|
||||||
.CreateCommand(new DeleteTTSVoiceCommand(_user, _logger));
|
|
||||||
})
|
|
||||||
.CreateStaticInputParameter("delete", b =>
|
.CreateStaticInputParameter("delete", b =>
|
||||||
{
|
{
|
||||||
b.CreateVoiceNameParameter("voiceName", true)
|
b.CreateVoiceNameParameter("voiceName", true)
|
||||||
.CreateCommand(new DeleteTTSVoiceCommand(_user, _logger));
|
.CreateCommand(new DeleteTTSVoiceCommand(_user, _logger));
|
||||||
})
|
})
|
||||||
.CreateStaticInputParameter("remove", b =>
|
.AddAlias("del", "delete")
|
||||||
{
|
.AddAlias("remove", "delete")
|
||||||
b.CreateVoiceNameParameter("voiceName", true)
|
|
||||||
.CreateCommand(new DeleteTTSVoiceCommand(_user, _logger));
|
|
||||||
})
|
|
||||||
.CreateStaticInputParameter("enable", b =>
|
.CreateStaticInputParameter("enable", b =>
|
||||||
{
|
{
|
||||||
b.CreateVoiceNameParameter("voiceName", false)
|
b.CreateVoiceNameParameter("voiceName", false)
|
||||||
.CreateCommand(new SetTTSVoiceStateCommand(true, _user, _logger));
|
.CreateCommand(new SetTTSVoiceStateCommand(true, _user, _logger));
|
||||||
})
|
})
|
||||||
.CreateStaticInputParameter("on", b =>
|
.AddAlias("on", "enable")
|
||||||
{
|
.AddAlias("enabled", "enable")
|
||||||
b.CreateVoiceNameParameter("voiceName", false)
|
.AddAlias("true", "enable")
|
||||||
.CreateCommand(new SetTTSVoiceStateCommand(true, _user, _logger));
|
|
||||||
})
|
|
||||||
.CreateStaticInputParameter("disable", b =>
|
.CreateStaticInputParameter("disable", b =>
|
||||||
{
|
{
|
||||||
b.CreateVoiceNameParameter("voiceName", true)
|
b.CreateVoiceNameParameter("voiceName", true)
|
||||||
.CreateCommand(new SetTTSVoiceStateCommand(false, _user, _logger));
|
.CreateCommand(new SetTTSVoiceStateCommand(false, _user, _logger));
|
||||||
})
|
})
|
||||||
.CreateStaticInputParameter("off", b =>
|
.AddAlias("off", "disable")
|
||||||
{
|
.AddAlias("disabled", "disable")
|
||||||
b.CreateVoiceNameParameter("voiceName", true)
|
.AddAlias("false", "disable");
|
||||||
.CreateCommand(new SetTTSVoiceStateCommand(false, _user, _logger));
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,12 +69,7 @@ namespace TwitchChatTTS.Chat.Commands
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool CheckDefaultPermissions(ChatMessage message)
|
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Execute(IDictionary<string, string> values, ChatMessage message, HermesSocketClient client)
|
|
||||||
{
|
{
|
||||||
if (_user == null || _user.VoicesAvailable == null)
|
if (_user == null || _user.VoicesAvailable == null)
|
||||||
return;
|
return;
|
||||||
@ -95,12 +79,12 @@ namespace TwitchChatTTS.Chat.Commands
|
|||||||
var exists = _user.VoicesAvailable.Any(v => v.Value.ToLower() == voiceNameLower);
|
var exists = _user.VoicesAvailable.Any(v => v.Value.ToLower() == voiceNameLower);
|
||||||
if (exists)
|
if (exists)
|
||||||
{
|
{
|
||||||
_logger.Warning($"Voice already exists [voice: {voiceName}][id: {message.UserId}]");
|
_logger.Warning($"Voice already exists [voice: {voiceName}][id: {message.ChatterUserId}]");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await client.CreateTTSVoice(voiceName);
|
await client.CreateTTSVoice(voiceName);
|
||||||
_logger.Information($"Added a new TTS voice by {message.Username} [voice: {voiceName}][id: {message.UserId}]");
|
_logger.Information($"Added a new TTS voice [voice: {voiceName}][creator: {message.ChatterUserLogin}][creator id: {message.ChatterUserId}]");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -117,16 +101,11 @@ namespace TwitchChatTTS.Chat.Commands
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool CheckDefaultPermissions(ChatMessage message)
|
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Execute(IDictionary<string, string> values, ChatMessage message, HermesSocketClient client)
|
|
||||||
{
|
{
|
||||||
if (_user == null || _user.VoicesAvailable == null)
|
if (_user == null || _user.VoicesAvailable == null)
|
||||||
{
|
{
|
||||||
_logger.Debug($"Voices available are not loaded [chatter: {message.Username}][chatter id: {message.UserId}]");
|
_logger.Warning($"Voices available are not loaded [chatter: {message.ChatterUserLogin}][chatter id: {message.ChatterUserId}]");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,13 +114,18 @@ namespace TwitchChatTTS.Chat.Commands
|
|||||||
var exists = _user.VoicesAvailable.Any(v => v.Value.ToLower() == voiceNameLower);
|
var exists = _user.VoicesAvailable.Any(v => v.Value.ToLower() == voiceNameLower);
|
||||||
if (!exists)
|
if (!exists)
|
||||||
{
|
{
|
||||||
_logger.Debug($"Voice does not exist [voice: {voiceName}][chatter: {message.Username}][chatter id: {message.UserId}]");
|
_logger.Warning($"Voice does not exist [voice: {voiceName}][chatter: {message.ChatterUserLogin}][chatter id: {message.ChatterUserId}]");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var voiceId = _user.VoicesAvailable.FirstOrDefault(v => v.Value.ToLower() == voiceNameLower).Key;
|
||||||
|
if (voiceId == null) {
|
||||||
|
_logger.Warning($"Could not find the identifier for the tts voice [voice name: {voiceName}]");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var voiceId = _user.VoicesAvailable.FirstOrDefault(v => v.Value.ToLower() == voiceName).Key;
|
|
||||||
await client.DeleteTTSVoice(voiceId);
|
await client.DeleteTTSVoice(voiceId);
|
||||||
_logger.Information($"Deleted a TTS voice [voice: {voiceName}][chatter: {message.Username}][chatter id: {message.UserId}]");
|
_logger.Information($"Deleted a TTS voice [voice: {voiceName}][chatter: {message.ChatterUserLogin}][chatter id: {message.ChatterUserId}]");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -160,12 +144,7 @@ namespace TwitchChatTTS.Chat.Commands
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool CheckDefaultPermissions(ChatMessage message)
|
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
|
||||||
{
|
|
||||||
return message.IsModerator || message.IsBroadcaster;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Execute(IDictionary<string, string> values, ChatMessage message, HermesSocketClient client)
|
|
||||||
{
|
{
|
||||||
if (_user == null || _user.VoicesAvailable == null)
|
if (_user == null || _user.VoicesAvailable == null)
|
||||||
return;
|
return;
|
||||||
@ -175,7 +154,7 @@ namespace TwitchChatTTS.Chat.Commands
|
|||||||
var voiceId = _user.VoicesAvailable.FirstOrDefault(v => v.Value.ToLower() == voiceNameLower).Key;
|
var voiceId = _user.VoicesAvailable.FirstOrDefault(v => v.Value.ToLower() == voiceNameLower).Key;
|
||||||
|
|
||||||
await client.UpdateTTSVoiceState(voiceId, _state);
|
await client.UpdateTTSVoiceState(voiceId, _state);
|
||||||
_logger.Information($"Changed state for TTS voice [voice: {voiceName}][state: {_state}][invoker: {message.Username}][id: {message.UserId}]");
|
_logger.Information($"Changed state for TTS voice [voice: {voiceName}][state: {_state}][invoker: {message.ChatterUserLogin}][id: {message.ChatterUserId}]");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
using HermesSocketLibrary.Socket.Data;
|
using HermesSocketLibrary.Socket.Data;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
using TwitchChatTTS.Hermes.Socket;
|
using TwitchChatTTS.Hermes.Socket;
|
||||||
using TwitchLib.Client.Models;
|
using TwitchChatTTS.Twitch.Socket.Messages;
|
||||||
using static TwitchChatTTS.Chat.Commands.TTSCommands;
|
using static TwitchChatTTS.Chat.Commands.TTSCommands;
|
||||||
|
|
||||||
namespace TwitchChatTTS.Chat.Commands
|
namespace TwitchChatTTS.Chat.Commands
|
||||||
@ -37,12 +37,7 @@ namespace TwitchChatTTS.Chat.Commands
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool CheckDefaultPermissions(ChatMessage message)
|
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
|
||||||
{
|
|
||||||
return message.IsBroadcaster;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Execute(IDictionary<string, string> values, ChatMessage message, HermesSocketClient client)
|
|
||||||
{
|
{
|
||||||
_logger.Information($"TTS Version: {TTS.MAJOR_VERSION}.{TTS.MINOR_VERSION}");
|
_logger.Information($"TTS Version: {TTS.MAJOR_VERSION}.{TTS.MINOR_VERSION}");
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
using Serilog;
|
using Serilog;
|
||||||
using TwitchChatTTS.Hermes.Socket;
|
using TwitchChatTTS.Hermes.Socket;
|
||||||
using TwitchLib.Client.Models;
|
using TwitchChatTTS.Twitch.Socket.Messages;
|
||||||
using static TwitchChatTTS.Chat.Commands.TTSCommands;
|
using static TwitchChatTTS.Chat.Commands.TTSCommands;
|
||||||
|
|
||||||
namespace TwitchChatTTS.Chat.Commands
|
namespace TwitchChatTTS.Chat.Commands
|
||||||
@ -8,6 +8,8 @@ namespace TwitchChatTTS.Chat.Commands
|
|||||||
public class VoiceCommand : IChatCommand
|
public class VoiceCommand : IChatCommand
|
||||||
{
|
{
|
||||||
private readonly User _user;
|
private readonly User _user;
|
||||||
|
// TODO: get permissions
|
||||||
|
// TODO: validated parameter for username by including '@' and regex for username
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
public VoiceCommand(User user, ILogger logger)
|
public VoiceCommand(User user, ILogger logger)
|
||||||
@ -23,7 +25,10 @@ namespace TwitchChatTTS.Chat.Commands
|
|||||||
builder.CreateCommandTree(Name, b =>
|
builder.CreateCommandTree(Name, b =>
|
||||||
{
|
{
|
||||||
b.CreateVoiceNameParameter("voiceName", true)
|
b.CreateVoiceNameParameter("voiceName", true)
|
||||||
.CreateCommand(new TTSVoiceSelector(_user, _logger));
|
.CreateCommand(new TTSVoiceSelector(_user, _logger))
|
||||||
|
.CreateUnvalidatedParameter("chatter", optional: true)
|
||||||
|
.AddPermission("tts.command.voice.admin")
|
||||||
|
.CreateCommand(new TTSVoiceSelectorAdmin(_user, _logger));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,18 +45,12 @@ namespace TwitchChatTTS.Chat.Commands
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
|
||||||
public bool CheckDefaultPermissions(ChatMessage message)
|
|
||||||
{
|
|
||||||
return message.IsModerator || message.IsBroadcaster || message.IsSubscriber || message.Bits >= 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Execute(IDictionary<string, string> values, ChatMessage message, HermesSocketClient client)
|
|
||||||
{
|
{
|
||||||
if (_user == null || _user.VoicesSelected == null)
|
if (_user == null || _user.VoicesSelected == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
long chatterId = long.Parse(message.UserId);
|
long chatterId = long.Parse(message.ChatterUserId);
|
||||||
var voiceName = values["voiceName"];
|
var voiceName = values["voiceName"];
|
||||||
var voiceNameLower = voiceName.ToLower();
|
var voiceNameLower = voiceName.ToLower();
|
||||||
var voice = _user.VoicesAvailable.First(v => v.Value.ToLower() == voiceNameLower);
|
var voice = _user.VoicesAvailable.First(v => v.Value.ToLower() == voiceNameLower);
|
||||||
@ -59,12 +58,56 @@ namespace TwitchChatTTS.Chat.Commands
|
|||||||
if (_user.VoicesSelected.ContainsKey(chatterId))
|
if (_user.VoicesSelected.ContainsKey(chatterId))
|
||||||
{
|
{
|
||||||
await client.UpdateTTSUser(chatterId, voice.Key);
|
await client.UpdateTTSUser(chatterId, voice.Key);
|
||||||
_logger.Debug($"Sent request to create chat TTS voice [voice: {voice.Value}][username: {message.Username}][reason: command]");
|
_logger.Debug($"Sent request to create chat TTS voice [voice: {voice.Value}][username: {message.ChatterUserLogin}][reason: command]");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
await client.CreateTTSUser(chatterId, voice.Key);
|
await client.CreateTTSUser(chatterId, voice.Key);
|
||||||
_logger.Debug($"Sent request to update chat TTS voice [voice: {voice.Value}][username: {message.Username}][reason: command]");
|
_logger.Debug($"Sent request to update chat TTS voice [voice: {voice.Value}][username: {message.ChatterUserLogin}][reason: command]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TTSVoiceSelectorAdmin : IChatPartialCommand
|
||||||
|
{
|
||||||
|
private readonly User _user;
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
|
public bool AcceptCustomPermission { get => true; }
|
||||||
|
|
||||||
|
public TTSVoiceSelectorAdmin(User user, ILogger logger)
|
||||||
|
{
|
||||||
|
_user = user;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
|
||||||
|
{
|
||||||
|
if (_user == null || _user.VoicesSelected == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var chatterLogin = values["chatter"].Substring(1);
|
||||||
|
var mention = message.Message.Fragments.FirstOrDefault(f => f.Mention != null && f.Mention.UserLogin == chatterLogin)?.Mention;
|
||||||
|
if (mention == null)
|
||||||
|
{
|
||||||
|
_logger.Warning("Failed to find the chatter to apply voice command to.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
long chatterId = long.Parse(mention.UserId);
|
||||||
|
var voiceName = values["voiceName"];
|
||||||
|
var voiceNameLower = voiceName.ToLower();
|
||||||
|
var voice = _user.VoicesAvailable.First(v => v.Value.ToLower() == voiceNameLower);
|
||||||
|
|
||||||
|
if (_user.VoicesSelected.ContainsKey(chatterId))
|
||||||
|
{
|
||||||
|
await client.UpdateTTSUser(chatterId, voice.Key);
|
||||||
|
_logger.Debug($"Sent request to create chat TTS voice [voice: {voice.Value}][username: {mention.UserLogin}][reason: command]");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await client.CreateTTSUser(chatterId, voice.Key);
|
||||||
|
_logger.Debug($"Sent request to update chat TTS voice [voice: {voice.Value}][username: {mention.UserLogin}][reason: command]");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,9 +23,11 @@ namespace TwitchChatTTS.Chat.Groups.Permissions
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool? CheckIfAllowed(IEnumerable<string> groups, string path) {
|
public bool? CheckIfAllowed(IEnumerable<string> groups, string path)
|
||||||
|
{
|
||||||
bool overall = false;
|
bool overall = false;
|
||||||
foreach (var group in groups) {
|
foreach (var group in groups)
|
||||||
|
{
|
||||||
var result = CheckIfAllowed($"{group}.{path}");
|
var result = CheckIfAllowed($"{group}.{path}");
|
||||||
if (result == false)
|
if (result == false)
|
||||||
return false;
|
return false;
|
||||||
@ -87,9 +89,8 @@ namespace TwitchChatTTS.Chat.Groups.Permissions
|
|||||||
}
|
}
|
||||||
return Get(next, string.Join('.', parts.Skip(1)), edit);
|
return Get(next, string.Join('.', parts.Skip(1)), edit);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
internal class PermissionNode
|
private sealed class PermissionNode
|
||||||
{
|
{
|
||||||
public string Name { get; }
|
public string Name { get; }
|
||||||
public bool? Allow
|
public bool? Allow
|
||||||
@ -103,7 +104,7 @@ namespace TwitchChatTTS.Chat.Groups.Permissions
|
|||||||
}
|
}
|
||||||
set => _allow = value;
|
set => _allow = value;
|
||||||
}
|
}
|
||||||
public int Priority;
|
|
||||||
internal PermissionNode? Parent { get => _parent; }
|
internal PermissionNode? Parent { get => _parent; }
|
||||||
public IList<PermissionNode>? Children { get => _children == null ? null : new ReadOnlyCollection<PermissionNode>(_children); }
|
public IList<PermissionNode>? Children { get => _children == null ? null : new ReadOnlyCollection<PermissionNode>(_children); }
|
||||||
|
|
||||||
@ -126,7 +127,8 @@ namespace TwitchChatTTS.Chat.Groups.Permissions
|
|||||||
_children.Add(child);
|
_children.Add(child);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void Clear() {
|
internal void Clear()
|
||||||
|
{
|
||||||
if (_children != null)
|
if (_children != null)
|
||||||
_children.Clear();
|
_children.Clear();
|
||||||
}
|
}
|
||||||
@ -146,4 +148,5 @@ namespace TwitchChatTTS.Chat.Groups.Permissions
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -2,24 +2,23 @@ using NAudio.Wave;
|
|||||||
using NAudio.Extras;
|
using NAudio.Extras;
|
||||||
using NAudio.Wave.SampleProviders;
|
using NAudio.Wave.SampleProviders;
|
||||||
|
|
||||||
public class AudioPlaybackEngine : IDisposable
|
public sealed class AudioPlaybackEngine : IDisposable
|
||||||
{
|
{
|
||||||
public static readonly AudioPlaybackEngine Instance = new AudioPlaybackEngine(44100, 2);
|
|
||||||
|
|
||||||
private readonly IWavePlayer outputDevice;
|
|
||||||
private readonly MixingSampleProvider mixer;
|
|
||||||
public int SampleRate { get; }
|
public int SampleRate { get; }
|
||||||
|
|
||||||
private AudioPlaybackEngine(int sampleRate = 44100, int channelCount = 2)
|
private readonly IWavePlayer _outputDevice;
|
||||||
|
private readonly MixingSampleProvider _mixer;
|
||||||
|
|
||||||
|
public AudioPlaybackEngine(int sampleRate = 44100, int channelCount = 2)
|
||||||
{
|
{
|
||||||
SampleRate = sampleRate;
|
SampleRate = sampleRate;
|
||||||
outputDevice = new WaveOutEvent();
|
_outputDevice = new WaveOutEvent();
|
||||||
|
|
||||||
mixer = new MixingSampleProvider(WaveFormat.CreateIeeeFloatWaveFormat(sampleRate, channelCount));
|
_mixer = new MixingSampleProvider(WaveFormat.CreateIeeeFloatWaveFormat(sampleRate, channelCount));
|
||||||
mixer.ReadFully = true;
|
_mixer.ReadFully = true;
|
||||||
|
|
||||||
outputDevice.Init(mixer);
|
_outputDevice.Init(_mixer);
|
||||||
outputDevice.Play();
|
_outputDevice.Play();
|
||||||
}
|
}
|
||||||
|
|
||||||
private ISampleProvider ConvertToRightChannelCount(ISampleProvider? input)
|
private ISampleProvider ConvertToRightChannelCount(ISampleProvider? input)
|
||||||
@ -27,11 +26,11 @@ public class AudioPlaybackEngine : IDisposable
|
|||||||
if (input == null)
|
if (input == null)
|
||||||
throw new NullReferenceException(nameof(input));
|
throw new NullReferenceException(nameof(input));
|
||||||
|
|
||||||
if (input.WaveFormat.Channels == mixer.WaveFormat.Channels)
|
if (input.WaveFormat.Channels == _mixer.WaveFormat.Channels)
|
||||||
return input;
|
return input;
|
||||||
if (input.WaveFormat.Channels == 1 && mixer.WaveFormat.Channels == 2)
|
if (input.WaveFormat.Channels == 1 && _mixer.WaveFormat.Channels == 2)
|
||||||
return new MonoToStereoSampleProvider(input);
|
return new MonoToStereoSampleProvider(input);
|
||||||
if (input.WaveFormat.Channels == 2 && mixer.WaveFormat.Channels == 1)
|
if (input.WaveFormat.Channels == 2 && _mixer.WaveFormat.Channels == 1)
|
||||||
return new StereoToMonoSampleProvider(input);
|
return new StereoToMonoSampleProvider(input);
|
||||||
throw new NotImplementedException("Not yet implemented this channel count conversion");
|
throw new NotImplementedException("Not yet implemented this channel count conversion");
|
||||||
}
|
}
|
||||||
@ -89,26 +88,26 @@ public class AudioPlaybackEngine : IDisposable
|
|||||||
|
|
||||||
public void AddMixerInput(ISampleProvider input)
|
public void AddMixerInput(ISampleProvider input)
|
||||||
{
|
{
|
||||||
mixer.AddMixerInput(input);
|
_mixer.AddMixerInput(input);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AddMixerInput(IWaveProvider input)
|
public void AddMixerInput(IWaveProvider input)
|
||||||
{
|
{
|
||||||
mixer.AddMixerInput(input);
|
_mixer.AddMixerInput(input);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RemoveMixerInput(ISampleProvider sound)
|
public void RemoveMixerInput(ISampleProvider sound)
|
||||||
{
|
{
|
||||||
mixer.RemoveMixerInput(sound);
|
_mixer.RemoveMixerInput(sound);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AddOnMixerInputEnded(EventHandler<SampleProviderEventArgs> e)
|
public void AddOnMixerInputEnded(EventHandler<SampleProviderEventArgs> e)
|
||||||
{
|
{
|
||||||
mixer.MixerInputEnded += e;
|
_mixer.MixerInputEnded += e;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
outputDevice.Dispose();
|
_outputDevice.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,4 +1,5 @@
|
|||||||
using NAudio.Wave;
|
using NAudio.Wave;
|
||||||
|
using TwitchChatTTS.Twitch.Socket.Messages;
|
||||||
|
|
||||||
public class TTSPlayer
|
public class TTSPlayer
|
||||||
{
|
{
|
||||||
@ -7,7 +8,7 @@ public class TTSPlayer
|
|||||||
private readonly Mutex _mutex;
|
private readonly Mutex _mutex;
|
||||||
private readonly Mutex _mutex2;
|
private readonly Mutex _mutex2;
|
||||||
|
|
||||||
public ISampleProvider? Playing { get; set; }
|
public TTSMessage? Playing { get; set; }
|
||||||
|
|
||||||
public TTSPlayer()
|
public TTSPlayer()
|
||||||
{
|
{
|
||||||
@ -100,12 +101,80 @@ public class TTSPlayer
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void RemoveAll(long chatterId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_mutex2.WaitOne();
|
||||||
|
if (_buffer.UnorderedItems.Any(i => i.Element.ChatterId == chatterId)) {
|
||||||
|
var list = _buffer.UnorderedItems.Where(i => i.Element.ChatterId != chatterId).ToArray();
|
||||||
|
_buffer.Clear();
|
||||||
|
foreach (var item in list)
|
||||||
|
_buffer.Enqueue(item.Element, item.Element.Priority);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_mutex2.ReleaseMutex();
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_mutex.WaitOne();
|
||||||
|
if (_messages.UnorderedItems.Any(i => i.Element.ChatterId == chatterId)) {
|
||||||
|
var list = _messages.UnorderedItems.Where(i => i.Element.ChatterId != chatterId).ToArray();
|
||||||
|
_messages.Clear();
|
||||||
|
foreach (var item in list)
|
||||||
|
_messages.Enqueue(item.Element, item.Element.Priority);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_mutex.ReleaseMutex();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveMessage(string messageId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_mutex2.WaitOne();
|
||||||
|
if (_buffer.UnorderedItems.Any(i => i.Element.MessageId == messageId)) {
|
||||||
|
var list = _buffer.UnorderedItems.Where(i => i.Element.MessageId != messageId).ToArray();
|
||||||
|
_buffer.Clear();
|
||||||
|
foreach (var item in list)
|
||||||
|
_buffer.Enqueue(item.Element, item.Element.Priority);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_mutex2.ReleaseMutex();
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_mutex.WaitOne();
|
||||||
|
if (_messages.UnorderedItems.Any(i => i.Element.MessageId == messageId)) {
|
||||||
|
var list = _messages.UnorderedItems.Where(i => i.Element.MessageId != messageId).ToArray();
|
||||||
|
_messages.Clear();
|
||||||
|
foreach (var item in list)
|
||||||
|
_messages.Enqueue(item.Element, item.Element.Priority);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_mutex.ReleaseMutex();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public bool IsEmpty()
|
public bool IsEmpty()
|
||||||
{
|
{
|
||||||
return _messages.Count == 0;
|
return _messages.Count == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private class DescendingOrder : IComparer<int> {
|
private class DescendingOrder : IComparer<int>
|
||||||
|
{
|
||||||
public int Compare(int x, int y) => y.CompareTo(x);
|
public int Compare(int x, int y) => y.CompareTo(x);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -113,15 +182,12 @@ public class TTSPlayer
|
|||||||
public class TTSMessage
|
public class TTSMessage
|
||||||
{
|
{
|
||||||
public string? Voice { get; set; }
|
public string? Voice { get; set; }
|
||||||
public string? Channel { get; set; }
|
public long ChatterId { get; set; }
|
||||||
public string? Username { get; set; }
|
public string MessageId { get; set; }
|
||||||
public string? Message { get; set; }
|
public string? Message { get; set; }
|
||||||
public string? File { get; set; }
|
public string? File { get; set; }
|
||||||
public DateTime Timestamp { get; set; }
|
public DateTime Timestamp { get; set; }
|
||||||
public bool Moderator { get; set; }
|
public IEnumerable<TwitchBadge> Badges { get; set; }
|
||||||
public bool Bot { get; set; }
|
|
||||||
public IEnumerable<KeyValuePair<string, string>>? Badges { get; set; }
|
|
||||||
public int Bits { get; set; }
|
|
||||||
public int Priority { get; set; }
|
public int Priority { get; set; }
|
||||||
public ISampleProvider? Audio { get; set; }
|
public ISampleProvider? Audio { get; set; }
|
||||||
}
|
}
|
@ -36,12 +36,12 @@ namespace TwitchChatTTS.Helpers
|
|||||||
|
|
||||||
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);
|
return await _client.PostAsJsonAsync(uri, data, _options);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<HttpResponseMessage> Post(string uri)
|
public async Task<HttpResponseMessage> Post(string uri)
|
||||||
{
|
{
|
||||||
return await _client.PostAsJsonAsync(uri, new object());
|
return await _client.PostAsJsonAsync(uri, new object(), _options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
46
Hermes/CustomDataManager.cs
Normal file
46
Hermes/CustomDataManager.cs
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
namespace TwitchChatTTS.Hermes
|
||||||
|
{
|
||||||
|
public interface ICustomDataManager {
|
||||||
|
void Add(string key, object value, string type);
|
||||||
|
void Change(string key, object value);
|
||||||
|
void Delete(string key);
|
||||||
|
object? Get(string key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CustomDataManager : ICustomDataManager
|
||||||
|
{
|
||||||
|
private IDictionary<string, DataInfo> _data;
|
||||||
|
|
||||||
|
public CustomDataManager() {
|
||||||
|
_data = new Dictionary<string, DataInfo>();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void Add(string key, object value, string type)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Change(string key, object value)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Delete(string key)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public object? Get(string key)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// type: text (string), whole number (int), number (double), boolean, formula (string, data type of number)
|
||||||
|
public struct DataInfo {
|
||||||
|
public string Id { get; set; }
|
||||||
|
public string Type { get; set; }
|
||||||
|
public object Value { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@ -3,29 +3,68 @@ using TwitchChatTTS;
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using HermesSocketLibrary.Requests.Messages;
|
using HermesSocketLibrary.Requests.Messages;
|
||||||
using TwitchChatTTS.Hermes;
|
using TwitchChatTTS.Hermes;
|
||||||
using TwitchChatTTS.Chat.Groups.Permissions;
|
using Serilog;
|
||||||
using TwitchChatTTS.Chat.Groups;
|
|
||||||
using HermesSocketLibrary.Socket.Data;
|
|
||||||
|
|
||||||
public class HermesApiClient
|
public class HermesApiClient
|
||||||
{
|
{
|
||||||
|
private readonly TwitchBotAuth _token;
|
||||||
private readonly WebClientWrap _web;
|
private readonly WebClientWrap _web;
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
public const string BASE_URL = "tomtospeech.com";
|
public const string BASE_URL = "tomtospeech.com";
|
||||||
|
|
||||||
public HermesApiClient(Configuration configuration)
|
public HermesApiClient(TwitchBotAuth token, Configuration configuration, ILogger logger)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(configuration.Hermes?.Token))
|
if (string.IsNullOrWhiteSpace(configuration.Hermes?.Token))
|
||||||
{
|
{
|
||||||
throw new Exception("Ensure you have written your API key in \".token\" file, in the same folder as this application.");
|
throw new Exception("Ensure you have written your API key in \".token\" file, in the same folder as this application.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_token = token;
|
||||||
_web = new WebClientWrap(new JsonSerializerOptions()
|
_web = new WebClientWrap(new JsonSerializerOptions()
|
||||||
{
|
{
|
||||||
PropertyNameCaseInsensitive = false,
|
PropertyNameCaseInsensitive = false,
|
||||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
|
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
|
||||||
});
|
});
|
||||||
_web.AddHeader("x-api-key", configuration.Hermes.Token);
|
_web.AddHeader("x-api-key", configuration.Hermes.Token);
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async Task<bool> AuthorizeTwitch()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.Debug($"Attempting to authorize Twitch API...");
|
||||||
|
var authorize = await _web.GetJson<TwitchBotAuth>($"https://{HermesApiClient.BASE_URL}/api/account/reauthorize");
|
||||||
|
if (authorize != null)
|
||||||
|
{
|
||||||
|
_token.AccessToken = authorize.AccessToken;
|
||||||
|
_token.RefreshToken = authorize.RefreshToken;
|
||||||
|
_token.UserId = authorize.UserId;
|
||||||
|
_token.BroadcasterId = authorize.BroadcasterId;
|
||||||
|
_token.ExpiresIn = authorize.ExpiresIn;
|
||||||
|
_token.UpdatedAt = DateTime.Now;
|
||||||
|
_logger.Information("Updated Twitch API tokens.");
|
||||||
|
_logger.Debug($"Twitch API Auth data [user id: {_token.UserId}][id: {_token.BroadcasterId}][expires in: {_token.ExpiresIn}][expires at: {_token.ExpiresAt.ToShortTimeString()}]");
|
||||||
|
}
|
||||||
|
else if (authorize != null)
|
||||||
|
{
|
||||||
|
_logger.Error("Twitch API Authorization failed: " + authorize.AccessToken + " | " + authorize.RefreshToken + " | " + authorize.UserId + " | " + authorize.BroadcasterId);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
_logger.Debug($"Authorized Twitch API.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (JsonException)
|
||||||
|
{
|
||||||
|
_logger.Debug($"Failed to Authorize Twitch API due to JSON error.");
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.Error(e, "Failed to authorize to Twitch API.");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<TTSVersion?> GetLatestTTSVersion()
|
public async Task<TTSVersion?> GetLatestTTSVersion()
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
using CommonSocketLibrary.Abstract;
|
using CommonSocketLibrary.Abstract;
|
||||||
using CommonSocketLibrary.Common;
|
using CommonSocketLibrary.Common;
|
||||||
using HermesSocketLibrary.Requests.Callbacks;
|
using HermesSocketLibrary.Requests.Callbacks;
|
||||||
@ -160,8 +161,7 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
|
|||||||
if (chatters == null)
|
if (chatters == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var client = _serviceProvider.GetRequiredService<ChatMessageHandler>();
|
_user.Chatters = [.. chatters];
|
||||||
client.Chatters = [.. chatters];
|
|
||||||
_logger.Information($"Fetched {chatters.Count()} chatters' id.");
|
_logger.Information($"Fetched {chatters.Count()} chatters' id.");
|
||||||
}
|
}
|
||||||
else if (message.Request.Type == "get_emotes")
|
else if (message.Request.Type == "get_emotes")
|
||||||
@ -254,8 +254,19 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_user.RegexFilters = wordFilters.ToList();
|
var filters = wordFilters.Where(f => f.Search != null && f.Replace != null).ToArray();
|
||||||
_logger.Information($"TTS word filters [count: {_user.RegexFilters.Count}] have been refreshed.");
|
foreach (var filter in filters)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var re = new Regex(filter.Search!, RegexOptions.Compiled);
|
||||||
|
re.Match(string.Empty);
|
||||||
|
filter.Regex = re;
|
||||||
|
}
|
||||||
|
catch (Exception e) { }
|
||||||
|
}
|
||||||
|
_user.RegexFilters = filters;
|
||||||
|
_logger.Information($"TTS word filters [count: {_user.RegexFilters.Count()}] have been refreshed.");
|
||||||
}
|
}
|
||||||
else if (message.Request.Type == "update_tts_voice_state")
|
else if (message.Request.Type == "update_tts_voice_state")
|
||||||
{
|
{
|
||||||
|
@ -383,7 +383,7 @@ namespace TwitchChatTTS.Hermes.Socket
|
|||||||
}
|
}
|
||||||
catch (WebSocketException wse) when (wse.Message.Contains("502"))
|
catch (WebSocketException wse) when (wse.Message.Contains("502"))
|
||||||
{
|
{
|
||||||
_logger.Error("Hermes websocket server cannot be found.");
|
_logger.Error($"Hermes websocket server cannot be found [code: {wse.ErrorCode}]");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using CommonSocketLibrary.Abstract;
|
|
||||||
using CommonSocketLibrary.Common;
|
using CommonSocketLibrary.Common;
|
||||||
using CommonSocketLibrary.Socket.Manager;
|
using CommonSocketLibrary.Socket.Manager;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
public class TTSUsernameFilter {
|
|
||||||
public string Username { get; set; }
|
|
||||||
public string Tag { get; set; }
|
|
||||||
public string UserId { get; set; }
|
|
||||||
}
|
|
@ -23,12 +23,14 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
|
|||||||
{
|
{
|
||||||
if (data is not HelloMessage message || message == null)
|
if (data is not HelloMessage message || message == null)
|
||||||
return;
|
return;
|
||||||
|
if (sender is not OBSSocketClient client)
|
||||||
|
return;
|
||||||
|
|
||||||
string? password = string.IsNullOrWhiteSpace(_configuration.Obs?.Password) ? null : _configuration.Obs.Password.Trim();
|
string? password = string.IsNullOrWhiteSpace(_configuration.Obs?.Password) ? null : _configuration.Obs.Password.Trim();
|
||||||
_logger.Verbose("OBS websocket password: " + password);
|
_logger.Verbose("OBS websocket password: " + password);
|
||||||
if (message.Authentication == null || string.IsNullOrEmpty(password))
|
if (message.Authentication == null || string.IsNullOrEmpty(password))
|
||||||
{
|
{
|
||||||
await sender.Send(1, new IdentifyMessage(message.RpcVersion, null, 1023 | 262144));
|
await client.Send(1, new IdentifyMessage(message.RpcVersion, null, 1023 | 262144));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,7 +54,7 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
|
|||||||
}
|
}
|
||||||
|
|
||||||
_logger.Verbose("Final hash: " + hash);
|
_logger.Verbose("Final hash: " + hash);
|
||||||
await sender.Send(1, new IdentifyMessage(message.RpcVersion, hash, 1023 | 262144));
|
await client.Send(1, new IdentifyMessage(message.RpcVersion, hash, 1023 | 262144));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -134,7 +134,7 @@ namespace TwitchChatTTS.OBS.Socket
|
|||||||
}
|
}
|
||||||
catch (WebSocketException wse) when (wse.Message.Contains("502"))
|
catch (WebSocketException wse) when (wse.Message.Contains("502"))
|
||||||
{
|
{
|
||||||
_logger.Error("OBS websocket server cannot be found. Be sure the server is on by looking at OBS > Tools > Websocket Server Settings.");
|
_logger.Error($"OBS websocket server cannot be found. Be sure the server is on by looking at OBS > Tools > Websocket Server Settings [code: {wse.ErrorCode}]");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
@ -94,7 +94,20 @@ namespace TwitchChatTTS.Seven.Socket
|
|||||||
}
|
}
|
||||||
|
|
||||||
_logger.Debug($"7tv client attempting to connect to {URL}");
|
_logger.Debug($"7tv client attempting to connect to {URL}");
|
||||||
await ConnectAsync($"{URL}");
|
try
|
||||||
|
{
|
||||||
|
await ConnectAsync(URL);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Error(ex, "Could not connect to 7tv websocket.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Connected)
|
||||||
|
{
|
||||||
|
await Task.Delay(30000);
|
||||||
|
await Connect();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void OnDisconnection(object? sender, SocketDisconnectionEventArgs e)
|
private async void OnDisconnection(object? sender, SocketDisconnectionEventArgs e)
|
||||||
@ -108,20 +121,19 @@ namespace TwitchChatTTS.Seven.Socket
|
|||||||
else
|
else
|
||||||
_logger.Warning($"Received end of stream message for 7tv websocket [code: {code}]");
|
_logger.Warning($"Received end of stream message for 7tv websocket [code: {code}]");
|
||||||
|
|
||||||
if (code >= 0 && code < _reconnectDelay.Length && _reconnectDelay[code] < 0)
|
if (code < 0 || code >= _reconnectDelay.Length)
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(30));
|
||||||
|
else if (_reconnectDelay[code] < 0)
|
||||||
{
|
{
|
||||||
_logger.Error($"7tv client will remain disconnected due to a bad client implementation.");
|
_logger.Error($"7tv client will remain disconnected due to a bad client implementation.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
else if (_reconnectDelay[code] > 0)
|
||||||
if (_reconnectDelay[code] > 0)
|
|
||||||
await Task.Delay(_reconnectDelay[code]);
|
await Task.Delay(_reconnectDelay[code]);
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
if (string.IsNullOrWhiteSpace(_user.SevenEmoteSetId))
|
_logger.Warning("Unknown 7tv disconnection.");
|
||||||
{
|
await Task.Delay(TimeSpan.FromSeconds(30));
|
||||||
_logger.Warning("Could not find the 7tv emote set id. Not reconnecting.");
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await Connect();
|
await Connect();
|
||||||
|
30
Startup.cs
30
Startup.cs
@ -10,16 +10,10 @@ using YamlDotNet.Serialization.NamingConventions;
|
|||||||
using TwitchChatTTS.Seven.Socket;
|
using TwitchChatTTS.Seven.Socket;
|
||||||
using TwitchChatTTS.OBS.Socket.Handlers;
|
using TwitchChatTTS.OBS.Socket.Handlers;
|
||||||
using TwitchChatTTS.Seven.Socket.Handlers;
|
using TwitchChatTTS.Seven.Socket.Handlers;
|
||||||
using TwitchLib.Client.Interfaces;
|
|
||||||
using TwitchLib.Client;
|
|
||||||
using TwitchLib.PubSub.Interfaces;
|
|
||||||
using TwitchLib.PubSub;
|
|
||||||
using TwitchLib.Communication.Interfaces;
|
|
||||||
using TwitchChatTTS.Seven.Socket.Managers;
|
using TwitchChatTTS.Seven.Socket.Managers;
|
||||||
using TwitchChatTTS.Hermes.Socket.Handlers;
|
using TwitchChatTTS.Hermes.Socket.Handlers;
|
||||||
using TwitchChatTTS.Hermes.Socket;
|
using TwitchChatTTS.Hermes.Socket;
|
||||||
using TwitchChatTTS.Hermes.Socket.Managers;
|
using TwitchChatTTS.Hermes.Socket.Managers;
|
||||||
using TwitchChatTTS.Chat.Commands.Parameters;
|
|
||||||
using TwitchChatTTS.Chat.Commands;
|
using TwitchChatTTS.Chat.Commands;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
@ -31,6 +25,9 @@ using TwitchChatTTS.Chat.Groups;
|
|||||||
using TwitchChatTTS.Chat.Emotes;
|
using TwitchChatTTS.Chat.Emotes;
|
||||||
using HermesSocketLibrary.Requests.Callbacks;
|
using HermesSocketLibrary.Requests.Callbacks;
|
||||||
using static TwitchChatTTS.Chat.Commands.TTSCommands;
|
using static TwitchChatTTS.Chat.Commands.TTSCommands;
|
||||||
|
using TwitchChatTTS.Twitch.Socket;
|
||||||
|
using TwitchChatTTS.Twitch.Socket.Messages;
|
||||||
|
using TwitchChatTTS.Twitch.Socket.Handlers;
|
||||||
|
|
||||||
// dotnet publish -r linux-x64 -p:PublishSingleFile=true --self-contained true
|
// dotnet publish -r linux-x64 -p:PublishSingleFile=true --self-contained true
|
||||||
// dotnet publish -r win-x64 -p:PublishSingleFile=true --self-contained true
|
// dotnet publish -r win-x64 -p:PublishSingleFile=true --self-contained true
|
||||||
@ -61,6 +58,7 @@ var logger = new LoggerConfiguration()
|
|||||||
|
|
||||||
s.AddSerilog(logger);
|
s.AddSerilog(logger);
|
||||||
s.AddSingleton<User>(new User());
|
s.AddSingleton<User>(new User());
|
||||||
|
s.AddSingleton<AudioPlaybackEngine>();
|
||||||
s.AddSingleton<ICallbackManager<HermesRequestData>, CallbackManager<HermesRequestData>>();
|
s.AddSingleton<ICallbackManager<HermesRequestData>, CallbackManager<HermesRequestData>>();
|
||||||
|
|
||||||
s.AddSingleton<JsonSerializerOptions>(new JsonSerializerOptions()
|
s.AddSingleton<JsonSerializerOptions>(new JsonSerializerOptions()
|
||||||
@ -82,13 +80,9 @@ s.AddSingleton<IGroupPermissionManager, GroupPermissionManager>();
|
|||||||
s.AddSingleton<CommandManager>();
|
s.AddSingleton<CommandManager>();
|
||||||
|
|
||||||
s.AddSingleton<TTSPlayer>();
|
s.AddSingleton<TTSPlayer>();
|
||||||
s.AddSingleton<ChatMessageHandler>();
|
|
||||||
s.AddSingleton<RedemptionManager>();
|
s.AddSingleton<RedemptionManager>();
|
||||||
s.AddSingleton<HermesApiClient>();
|
s.AddSingleton<HermesApiClient>();
|
||||||
s.AddSingleton<TwitchBotAuth>();
|
s.AddSingleton<TwitchBotAuth>();
|
||||||
s.AddTransient<IClient, TwitchLib.Communication.Clients.WebSocketClient>();
|
|
||||||
s.AddTransient<ITwitchClient, TwitchClient>();
|
|
||||||
s.AddTransient<ITwitchPubSub, TwitchPubSub>();
|
|
||||||
s.AddSingleton<TwitchApiClient>();
|
s.AddSingleton<TwitchApiClient>();
|
||||||
|
|
||||||
s.AddSingleton<SevenApiClient>();
|
s.AddSingleton<SevenApiClient>();
|
||||||
@ -114,11 +108,25 @@ s.AddKeyedSingleton<IWebSocketHandler, EndOfStreamHandler>("7tv");
|
|||||||
s.AddKeyedSingleton<MessageTypeManager<IWebSocketHandler>, SevenMessageTypeManager>("7tv");
|
s.AddKeyedSingleton<MessageTypeManager<IWebSocketHandler>, SevenMessageTypeManager>("7tv");
|
||||||
s.AddKeyedSingleton<SocketClient<WebSocketMessage>, SevenSocketClient>("7tv");
|
s.AddKeyedSingleton<SocketClient<WebSocketMessage>, SevenSocketClient>("7tv");
|
||||||
|
|
||||||
|
// twitch websocket
|
||||||
|
s.AddKeyedSingleton<SocketClient<TwitchWebsocketMessage>, TwitchWebsocketClient>("twitch");
|
||||||
|
|
||||||
|
s.AddKeyedSingleton<ITwitchSocketHandler, SessionWelcomeHandler>("twitch");
|
||||||
|
s.AddKeyedSingleton<ITwitchSocketHandler, SessionReconnectHandler>("twitch");
|
||||||
|
s.AddKeyedSingleton<ITwitchSocketHandler, NotificationHandler>("twitch");
|
||||||
|
|
||||||
|
s.AddKeyedSingleton<ITwitchSocketHandler, ChannelBanHandler>("twitch-notifications");
|
||||||
|
s.AddKeyedSingleton<ITwitchSocketHandler, ChannelChatMessageHandler>("twitch-notifications");
|
||||||
|
s.AddKeyedSingleton<ITwitchSocketHandler, ChannelChatClearUserHandler>("twitch-notifications");
|
||||||
|
s.AddKeyedSingleton<ITwitchSocketHandler, ChannelChatClearHandler>("twitch-notifications");
|
||||||
|
s.AddKeyedSingleton<ITwitchSocketHandler, ChannelChatDeleteMessageHandler>("twitch-notifications");
|
||||||
|
s.AddKeyedSingleton<ITwitchSocketHandler, ChannelCustomRedemptionHandler>("twitch-notifications");
|
||||||
|
s.AddKeyedSingleton<ITwitchSocketHandler, ChannelSubscriptionHandler>("twitch-notifications");
|
||||||
|
|
||||||
// hermes websocket
|
// hermes websocket
|
||||||
s.AddKeyedSingleton<IWebSocketHandler, HeartbeatHandler>("hermes");
|
s.AddKeyedSingleton<IWebSocketHandler, HeartbeatHandler>("hermes");
|
||||||
s.AddKeyedSingleton<IWebSocketHandler, LoginAckHandler>("hermes");
|
s.AddKeyedSingleton<IWebSocketHandler, LoginAckHandler>("hermes");
|
||||||
s.AddKeyedSingleton<IWebSocketHandler, RequestAckHandler>("hermes");
|
s.AddKeyedSingleton<IWebSocketHandler, RequestAckHandler>("hermes");
|
||||||
//s.AddKeyedSingleton<IWebSocketHandler, HeartbeatHandler>("hermes");
|
|
||||||
|
|
||||||
s.AddKeyedSingleton<MessageTypeManager<IWebSocketHandler>, HermesMessageTypeManager>("hermes");
|
s.AddKeyedSingleton<MessageTypeManager<IWebSocketHandler>, HermesMessageTypeManager>("hermes");
|
||||||
s.AddKeyedSingleton<SocketClient<WebSocketMessage>, HermesSocketClient>("hermes");
|
s.AddKeyedSingleton<SocketClient<WebSocketMessage>, HermesSocketClient>("hermes");
|
||||||
|
130
TTS.cs
130
TTS.cs
@ -4,33 +4,34 @@ using Microsoft.Extensions.DependencyInjection;
|
|||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
using NAudio.Wave.SampleProviders;
|
using NAudio.Wave.SampleProviders;
|
||||||
using TwitchLib.Client.Events;
|
|
||||||
using org.mariuszgromada.math.mxparser;
|
using org.mariuszgromada.math.mxparser;
|
||||||
using TwitchChatTTS.Hermes.Socket;
|
using TwitchChatTTS.Hermes.Socket;
|
||||||
using TwitchChatTTS.Chat.Groups.Permissions;
|
|
||||||
using TwitchChatTTS.Chat.Groups;
|
|
||||||
using TwitchChatTTS.Seven.Socket;
|
using TwitchChatTTS.Seven.Socket;
|
||||||
using TwitchChatTTS.Chat.Emotes;
|
using TwitchChatTTS.Chat.Emotes;
|
||||||
using CommonSocketLibrary.Abstract;
|
using CommonSocketLibrary.Abstract;
|
||||||
using CommonSocketLibrary.Common;
|
using CommonSocketLibrary.Common;
|
||||||
using TwitchChatTTS.OBS.Socket;
|
using TwitchChatTTS.OBS.Socket;
|
||||||
|
using TwitchChatTTS.Twitch.Socket.Messages;
|
||||||
|
using TwitchChatTTS.Twitch.Socket;
|
||||||
|
|
||||||
namespace TwitchChatTTS
|
namespace TwitchChatTTS
|
||||||
{
|
{
|
||||||
public class TTS : IHostedService
|
public class TTS : IHostedService
|
||||||
{
|
{
|
||||||
public const int MAJOR_VERSION = 3;
|
public const int MAJOR_VERSION = 4;
|
||||||
public const int MINOR_VERSION = 10;
|
public const int MINOR_VERSION = 0;
|
||||||
|
|
||||||
private readonly User _user;
|
private readonly User _user;
|
||||||
private readonly HermesApiClient _hermesApiClient;
|
private readonly HermesApiClient _hermesApiClient;
|
||||||
private readonly SevenApiClient _sevenApiClient;
|
private readonly SevenApiClient _sevenApiClient;
|
||||||
|
private readonly HermesSocketClient _hermes;
|
||||||
private readonly OBSSocketClient _obs;
|
private readonly OBSSocketClient _obs;
|
||||||
private readonly SevenSocketClient _seven;
|
private readonly SevenSocketClient _seven;
|
||||||
private readonly HermesSocketClient _hermes;
|
private readonly TwitchWebsocketClient _twitch;
|
||||||
private readonly IEmoteDatabase _emotes;
|
private readonly IEmoteDatabase _emotes;
|
||||||
private readonly Configuration _configuration;
|
private readonly Configuration _configuration;
|
||||||
private readonly TTSPlayer _player;
|
private readonly TTSPlayer _player;
|
||||||
|
private readonly AudioPlaybackEngine _playback;
|
||||||
private readonly IServiceProvider _serviceProvider;
|
private readonly IServiceProvider _serviceProvider;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
@ -41,9 +42,11 @@ namespace TwitchChatTTS
|
|||||||
[FromKeyedServices("hermes")] SocketClient<WebSocketMessage> hermes,
|
[FromKeyedServices("hermes")] SocketClient<WebSocketMessage> hermes,
|
||||||
[FromKeyedServices("obs")] SocketClient<WebSocketMessage> obs,
|
[FromKeyedServices("obs")] SocketClient<WebSocketMessage> obs,
|
||||||
[FromKeyedServices("7tv")] SocketClient<WebSocketMessage> seven,
|
[FromKeyedServices("7tv")] SocketClient<WebSocketMessage> seven,
|
||||||
|
[FromKeyedServices("twitch")] SocketClient<TwitchWebsocketMessage> twitch,
|
||||||
IEmoteDatabase emotes,
|
IEmoteDatabase emotes,
|
||||||
Configuration configuration,
|
Configuration configuration,
|
||||||
TTSPlayer player,
|
TTSPlayer player,
|
||||||
|
AudioPlaybackEngine playback,
|
||||||
IServiceProvider serviceProvider,
|
IServiceProvider serviceProvider,
|
||||||
ILogger logger
|
ILogger logger
|
||||||
)
|
)
|
||||||
@ -54,9 +57,11 @@ namespace TwitchChatTTS
|
|||||||
_hermes = (hermes as HermesSocketClient)!;
|
_hermes = (hermes as HermesSocketClient)!;
|
||||||
_obs = (obs as OBSSocketClient)!;
|
_obs = (obs as OBSSocketClient)!;
|
||||||
_seven = (seven as SevenSocketClient)!;
|
_seven = (seven as SevenSocketClient)!;
|
||||||
|
_twitch = (twitch as TwitchWebsocketClient)!;
|
||||||
_emotes = emotes;
|
_emotes = emotes;
|
||||||
_configuration = configuration;
|
_configuration = configuration;
|
||||||
_player = player;
|
_player = player;
|
||||||
|
_playback = playback;
|
||||||
_serviceProvider = serviceProvider;
|
_serviceProvider = serviceProvider;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
@ -89,21 +94,29 @@ namespace TwitchChatTTS
|
|||||||
await InitializeHermesWebsocket();
|
await InitializeHermesWebsocket();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await FetchUserData(_user, _hermesApiClient);
|
var hermesAccount = await _hermesApiClient.FetchHermesAccountDetails();
|
||||||
|
_user.HermesUserId = hermesAccount.Id;
|
||||||
|
_user.HermesUsername = hermesAccount.Username;
|
||||||
|
_user.TwitchUsername = hermesAccount.Username;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.Error(ex, "Failed to initialize properly. Restart app please.");
|
_logger.Error(ex, "Failed to initialize properly. Restart app please.");
|
||||||
await Task.Delay(30 * 1000);
|
await Task.Delay(30 * 1000);
|
||||||
}
|
|
||||||
|
|
||||||
var twitchapiclient = await InitializeTwitchApiClient(_user.TwitchUsername, _user.TwitchUserId.ToString());
|
|
||||||
if (twitchapiclient == null)
|
|
||||||
{
|
|
||||||
await Task.Delay(30 * 1000);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await _hermesApiClient.AuthorizeTwitch();
|
||||||
|
var twitchBotToken = await _hermesApiClient.FetchTwitchBotToken();
|
||||||
|
_user.TwitchUserId = long.Parse(twitchBotToken.BroadcasterId!);
|
||||||
|
_logger.Information($"Username: {_user.TwitchUsername} [id: {_user.TwitchUserId}]");
|
||||||
|
|
||||||
|
var twitchapiclient2 = _serviceProvider.GetRequiredService<TwitchApiClient>();
|
||||||
|
twitchapiclient2.Initialize(twitchBotToken);
|
||||||
|
|
||||||
|
_twitch.Initialize();
|
||||||
|
await _twitch.Connect();
|
||||||
|
|
||||||
var emoteSet = await _sevenApiClient.FetchChannelEmoteSet(_user.TwitchUserId.ToString());
|
var emoteSet = await _sevenApiClient.FetchChannelEmoteSet(_user.TwitchUserId.ToString());
|
||||||
if (emoteSet != null)
|
if (emoteSet != null)
|
||||||
_user.SevenEmoteSetId = emoteSet.Id;
|
_user.SevenEmoteSetId = emoteSet.Id;
|
||||||
@ -112,9 +125,9 @@ namespace TwitchChatTTS
|
|||||||
await InitializeSevenTv();
|
await InitializeSevenTv();
|
||||||
await InitializeObs();
|
await InitializeObs();
|
||||||
|
|
||||||
AudioPlaybackEngine.Instance.AddOnMixerInputEnded((object? s, SampleProviderEventArgs e) =>
|
_playback.AddOnMixerInputEnded((object? s, SampleProviderEventArgs e) =>
|
||||||
{
|
{
|
||||||
if (e.SampleProvider == _player.Playing)
|
if (e.SampleProvider == _player.Playing?.Audio)
|
||||||
{
|
{
|
||||||
_player.Playing = null;
|
_player.Playing = null;
|
||||||
}
|
}
|
||||||
@ -142,8 +155,8 @@ namespace TwitchChatTTS
|
|||||||
string url = $"https://api.streamelements.com/kappa/v2/speech?voice={m.Voice}&text={HttpUtility.UrlEncode(m.Message)}";
|
string url = $"https://api.streamelements.com/kappa/v2/speech?voice={m.Voice}&text={HttpUtility.UrlEncode(m.Message)}";
|
||||||
var sound = new NetworkWavSound(url);
|
var sound = new NetworkWavSound(url);
|
||||||
var provider = new CachedWavProvider(sound);
|
var provider = new CachedWavProvider(sound);
|
||||||
var data = AudioPlaybackEngine.Instance.ConvertSound(provider);
|
var data = _playback.ConvertSound(provider);
|
||||||
var resampled = new WdlResamplingSampleProvider(data, AudioPlaybackEngine.Instance.SampleRate);
|
var resampled = new WdlResamplingSampleProvider(data, _playback.SampleRate);
|
||||||
_logger.Verbose("Fetched TTS audio data.");
|
_logger.Verbose("Fetched TTS audio data.");
|
||||||
|
|
||||||
m.Audio = resampled;
|
m.Audio = resampled;
|
||||||
@ -185,7 +198,7 @@ namespace TwitchChatTTS
|
|||||||
if (!string.IsNullOrWhiteSpace(m.File) && File.Exists(m.File))
|
if (!string.IsNullOrWhiteSpace(m.File) && File.Exists(m.File))
|
||||||
{
|
{
|
||||||
_logger.Debug("Playing audio file via TTS: " + m.File);
|
_logger.Debug("Playing audio file via TTS: " + m.File);
|
||||||
AudioPlaybackEngine.Instance.PlaySound(m.File);
|
_playback.PlaySound(m.File);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -193,8 +206,8 @@ namespace TwitchChatTTS
|
|||||||
|
|
||||||
if (m.Audio != null)
|
if (m.Audio != null)
|
||||||
{
|
{
|
||||||
_player.Playing = m.Audio;
|
_player.Playing = m;
|
||||||
AudioPlaybackEngine.Instance.AddMixerInput(m.Audio);
|
_playback.AddMixerInput(m.Audio);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
@ -203,9 +216,6 @@ namespace TwitchChatTTS
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
_logger.Information("Twitch websocket client connecting...");
|
|
||||||
await twitchapiclient.Connect();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task StopAsync(CancellationToken cancellationToken)
|
public async Task StopAsync(CancellationToken cancellationToken)
|
||||||
@ -216,18 +226,6 @@ namespace TwitchChatTTS
|
|||||||
_logger.Warning("Application has stopped.");
|
_logger.Warning("Application has stopped.");
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task FetchUserData(User user, HermesApiClient hermes)
|
|
||||||
{
|
|
||||||
var hermesAccount = await hermes.FetchHermesAccountDetails();
|
|
||||||
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}]");
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task InitializeHermesWebsocket()
|
private async Task InitializeHermesWebsocket()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@ -267,40 +265,42 @@ namespace TwitchChatTTS
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<TwitchApiClient?> InitializeTwitchApiClient(string username, string broadcasterId)
|
// private async Task<TwitchApiClient?> InitializeTwitchApiClient(string username)
|
||||||
{
|
// {
|
||||||
_logger.Debug("Initializing twitch client.");
|
// _logger.Debug("Initializing twitch client.");
|
||||||
var twitchapiclient = _serviceProvider.GetRequiredService<TwitchApiClient>();
|
|
||||||
if (!await twitchapiclient.Authorize(broadcasterId))
|
|
||||||
{
|
|
||||||
_logger.Error("Cannot connect to Twitch API.");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var channels = _configuration.Twitch?.Channels ?? [username];
|
// var hermesapiclient = _serviceProvider.GetRequiredService<HermesApiClient>();
|
||||||
_logger.Information("Twitch channels: " + string.Join(", ", channels));
|
// if (!await hermesapiclient.AuthorizeTwitch())
|
||||||
twitchapiclient.InitializeClient(username, channels);
|
// {
|
||||||
twitchapiclient.InitializePublisher();
|
// _logger.Error("Cannot connect to Twitch API.");
|
||||||
|
// return null;
|
||||||
|
// }
|
||||||
|
|
||||||
var handler = _serviceProvider.GetRequiredService<ChatMessageHandler>();
|
// var twitchapiclient = _serviceProvider.GetRequiredService<TwitchApiClient>();
|
||||||
twitchapiclient.AddOnNewMessageReceived(async (object? s, OnMessageReceivedArgs e) =>
|
// var channels = _configuration.Twitch?.Channels ?? [username];
|
||||||
{
|
// _logger.Information("Twitch channels: " + string.Join(", ", channels));
|
||||||
try
|
// twitchapiclient.InitializeClient(username, channels);
|
||||||
{
|
// twitchapiclient.InitializePublisher();
|
||||||
var result = await handler.Handle(e);
|
|
||||||
if (result.Status != MessageStatus.None || result.Emotes == null || !result.Emotes.Any())
|
|
||||||
return;
|
|
||||||
|
|
||||||
await _hermes.SendEmoteUsage(e.ChatMessage.Id, result.ChatterId, result.Emotes);
|
// var handler = _serviceProvider.GetRequiredService<ChatMessageHandler>();
|
||||||
}
|
// twitchapiclient.AddOnNewMessageReceived(async (object? s, OnMessageReceivedArgs e) =>
|
||||||
catch (Exception ex)
|
// {
|
||||||
{
|
// try
|
||||||
_logger.Error(ex, "Unable to either execute a command or to send emote usage message.");
|
// {
|
||||||
}
|
// var result = await handler.Handle(e);
|
||||||
});
|
// if (result.Status != MessageStatus.None || result.Emotes == null || !result.Emotes.Any())
|
||||||
|
// return;
|
||||||
|
|
||||||
return twitchapiclient;
|
// await _hermes.SendEmoteUsage(e.ChatMessage.Id, result.ChatterId, result.Emotes);
|
||||||
}
|
// }
|
||||||
|
// catch (Exception ex)
|
||||||
|
// {
|
||||||
|
// _logger.Error(ex, "Unable to either execute a command or to send emote usage message.");
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
// return twitchapiclient;
|
||||||
|
// }
|
||||||
|
|
||||||
private async Task InitializeEmotes(SevenApiClient sevenapi, EmoteSet? channelEmotes)
|
private async Task InitializeEmotes(SevenApiClient sevenapi, EmoteSet? channelEmotes)
|
||||||
{
|
{
|
||||||
|
@ -17,6 +17,7 @@ namespace TwitchChatTTS.Twitch.Redemptions
|
|||||||
private readonly User _user;
|
private readonly User _user;
|
||||||
private readonly OBSSocketClient _obs;
|
private readonly OBSSocketClient _obs;
|
||||||
private readonly HermesSocketClient _hermes;
|
private readonly HermesSocketClient _hermes;
|
||||||
|
private readonly AudioPlaybackEngine _playback;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private readonly Random _random;
|
private readonly Random _random;
|
||||||
private bool _isReady;
|
private bool _isReady;
|
||||||
@ -26,12 +27,14 @@ namespace TwitchChatTTS.Twitch.Redemptions
|
|||||||
User user,
|
User user,
|
||||||
[FromKeyedServices("obs")] SocketClient<WebSocketMessage> obs,
|
[FromKeyedServices("obs")] SocketClient<WebSocketMessage> obs,
|
||||||
[FromKeyedServices("hermes")] SocketClient<WebSocketMessage> hermes,
|
[FromKeyedServices("hermes")] SocketClient<WebSocketMessage> hermes,
|
||||||
|
AudioPlaybackEngine playback,
|
||||||
ILogger logger)
|
ILogger logger)
|
||||||
{
|
{
|
||||||
_store = new Dictionary<string, IList<RedeemableAction>>();
|
_store = new Dictionary<string, IList<RedeemableAction>>();
|
||||||
_user = user;
|
_user = user;
|
||||||
_obs = (obs as OBSSocketClient)!;
|
_obs = (obs as OBSSocketClient)!;
|
||||||
_hermes = (hermes as HermesSocketClient)!;
|
_hermes = (hermes as HermesSocketClient)!;
|
||||||
|
_playback = playback;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_random = new Random();
|
_random = new Random();
|
||||||
_isReady = false;
|
_isReady = false;
|
||||||
@ -185,7 +188,7 @@ namespace TwitchChatTTS.Twitch.Redemptions
|
|||||||
_logger.Warning($"Cannot find audio file for Twitch channel point redeem [file: {action.Data["file_path"]}][chatter: {senderDisplayName}][chatter id: {senderId}]");
|
_logger.Warning($"Cannot find audio file for Twitch channel point redeem [file: {action.Data["file_path"]}][chatter: {senderDisplayName}][chatter id: {senderId}]");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
AudioPlaybackEngine.Instance.PlaySound(action.Data["file_path"]);
|
_playback.PlaySound(action.Data["file_path"]);
|
||||||
_logger.Debug($"Played an audio file for channel point redeem [file: {action.Data["file_path"]}][chatter: {senderDisplayName}][chatter id: {senderId}]");
|
_logger.Debug($"Played an audio file for channel point redeem [file: {action.Data["file_path"]}][chatter: {senderDisplayName}][chatter id: {senderId}]");
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
26
Twitch/Socket/Handlers/ChannelBanHandler.cs
Normal file
26
Twitch/Socket/Handlers/ChannelBanHandler.cs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
using Serilog;
|
||||||
|
using TwitchChatTTS.Twitch.Socket.Messages;
|
||||||
|
|
||||||
|
namespace TwitchChatTTS.Twitch.Socket.Handlers
|
||||||
|
{
|
||||||
|
public class ChannelBanHandler : ITwitchSocketHandler
|
||||||
|
{
|
||||||
|
public string Name => "channel.ban";
|
||||||
|
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
|
public ChannelBanHandler(ILogger logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task Execute(TwitchWebsocketClient sender, object? data)
|
||||||
|
{
|
||||||
|
if (data is not ChannelBanMessage message)
|
||||||
|
return Task.CompletedTask;
|
||||||
|
|
||||||
|
_logger.Warning($"Chatter banned [chatter: {message.UserLogin}][chatter id: {message.UserId}][End: {(message.IsPermanent ? "Permanent" : message.EndsAt.ToString())}]");
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
37
Twitch/Socket/Handlers/ChannelChatClearHandler.cs
Normal file
37
Twitch/Socket/Handlers/ChannelChatClearHandler.cs
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
using Serilog;
|
||||||
|
using TwitchChatTTS.Twitch.Socket.Messages;
|
||||||
|
|
||||||
|
namespace TwitchChatTTS.Twitch.Socket.Handlers
|
||||||
|
{
|
||||||
|
public class ChannelChatClearHandler : ITwitchSocketHandler
|
||||||
|
{
|
||||||
|
public string Name => "channel.chat.clear";
|
||||||
|
|
||||||
|
private readonly TTSPlayer _player;
|
||||||
|
private readonly AudioPlaybackEngine _playback;
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
|
public ChannelChatClearHandler(TTSPlayer player, AudioPlaybackEngine playback, ILogger logger)
|
||||||
|
{
|
||||||
|
_player = player;
|
||||||
|
_playback = playback;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task Execute(TwitchWebsocketClient sender, object? data)
|
||||||
|
{
|
||||||
|
if (data is not ChannelChatClearMessage message)
|
||||||
|
return Task.CompletedTask;
|
||||||
|
|
||||||
|
_player.RemoveAll();
|
||||||
|
if (_player.Playing != null)
|
||||||
|
{
|
||||||
|
_playback.RemoveMixerInput(_player.Playing.Audio!);
|
||||||
|
_player.Playing = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.Information($"Chat cleared [broadcaster: {message.BroadcasterUserLogin}][broadcaster id: {message.BroadcasterUserId}]");
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
37
Twitch/Socket/Handlers/ChannelChatClearUserHandler.cs
Normal file
37
Twitch/Socket/Handlers/ChannelChatClearUserHandler.cs
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
using Serilog;
|
||||||
|
using TwitchChatTTS.Twitch.Socket.Messages;
|
||||||
|
|
||||||
|
namespace TwitchChatTTS.Twitch.Socket.Handlers
|
||||||
|
{
|
||||||
|
public class ChannelChatClearUserHandler : ITwitchSocketHandler
|
||||||
|
{
|
||||||
|
public string Name => "channel.chat.clear_user_messages";
|
||||||
|
|
||||||
|
private readonly TTSPlayer _player;
|
||||||
|
private readonly AudioPlaybackEngine _playback;
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
|
public ChannelChatClearUserHandler(TTSPlayer player, AudioPlaybackEngine playback, ILogger logger)
|
||||||
|
{
|
||||||
|
_player = player;
|
||||||
|
_playback = playback;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task Execute(TwitchWebsocketClient sender, object? data)
|
||||||
|
{
|
||||||
|
if (data is not ChannelChatClearUserMessage message)
|
||||||
|
return Task.CompletedTask;
|
||||||
|
|
||||||
|
long chatterId = long.Parse(message.TargetUserId);
|
||||||
|
_player.RemoveAll(chatterId);
|
||||||
|
if (_player.Playing?.ChatterId == chatterId) {
|
||||||
|
_playback.RemoveMixerInput(_player.Playing.Audio!);
|
||||||
|
_player.Playing = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.Information($"Cleared all messages by user [target chatter: {message.TargetUserLogin}][target chatter id: {chatterId}][broadcaster: {message.BroadcasterUserLogin}][broadcaster id: {message.BroadcasterUserId}]");
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
39
Twitch/Socket/Handlers/ChannelChatDeleteMessageHandler.cs
Normal file
39
Twitch/Socket/Handlers/ChannelChatDeleteMessageHandler.cs
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
using Serilog;
|
||||||
|
using TwitchChatTTS.Twitch.Socket.Messages;
|
||||||
|
|
||||||
|
namespace TwitchChatTTS.Twitch.Socket.Handlers
|
||||||
|
{
|
||||||
|
public class ChannelChatDeleteMessageHandler : ITwitchSocketHandler
|
||||||
|
{
|
||||||
|
public string Name => "channel.chat.message_delete";
|
||||||
|
|
||||||
|
private readonly TTSPlayer _player;
|
||||||
|
private readonly AudioPlaybackEngine _playback;
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
|
public ChannelChatDeleteMessageHandler(TTSPlayer player, AudioPlaybackEngine playback, ILogger logger)
|
||||||
|
{
|
||||||
|
_player = player;
|
||||||
|
_playback = playback;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task Execute(TwitchWebsocketClient sender, object? data)
|
||||||
|
{
|
||||||
|
if (data is not ChannelChatDeleteMessage message)
|
||||||
|
return Task.CompletedTask;
|
||||||
|
|
||||||
|
if (_player.Playing?.MessageId == message.MessageId)
|
||||||
|
{
|
||||||
|
_playback.RemoveMixerInput(_player.Playing.Audio!);
|
||||||
|
_player.Playing = null;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
_player.RemoveMessage(message.MessageId);
|
||||||
|
|
||||||
|
|
||||||
|
_logger.Information($"Deleted chat message [message id: {message.MessageId}][target chatter: {message.TargetUserLogin}][target chatter id: {message.TargetUserId}][broadcaster: {message.BroadcasterUserLogin}][broadcaster id: {message.BroadcasterUserId}]");
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
319
Twitch/Socket/Handlers/ChannelChatMessageHandler.cs
Normal file
319
Twitch/Socket/Handlers/ChannelChatMessageHandler.cs
Normal file
@ -0,0 +1,319 @@
|
|||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using CommonSocketLibrary.Abstract;
|
||||||
|
using CommonSocketLibrary.Common;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Serilog;
|
||||||
|
using TwitchChatTTS.Chat.Commands;
|
||||||
|
using TwitchChatTTS.Chat.Emotes;
|
||||||
|
using TwitchChatTTS.Chat.Groups;
|
||||||
|
using TwitchChatTTS.Chat.Groups.Permissions;
|
||||||
|
using TwitchChatTTS.Hermes.Socket;
|
||||||
|
using TwitchChatTTS.OBS.Socket;
|
||||||
|
using TwitchChatTTS.Twitch.Socket.Messages;
|
||||||
|
|
||||||
|
namespace TwitchChatTTS.Twitch.Socket.Handlers
|
||||||
|
{
|
||||||
|
public class ChannelChatMessageHandler : ITwitchSocketHandler
|
||||||
|
{
|
||||||
|
public string Name => "channel.chat.message";
|
||||||
|
|
||||||
|
private readonly User _user;
|
||||||
|
private readonly TTSPlayer _player;
|
||||||
|
private readonly CommandManager _commands;
|
||||||
|
private readonly IGroupPermissionManager _permissionManager;
|
||||||
|
private readonly IChatterGroupManager _chatterGroupManager;
|
||||||
|
private readonly IEmoteDatabase _emotes;
|
||||||
|
private readonly OBSSocketClient _obs;
|
||||||
|
private readonly HermesSocketClient _hermes;
|
||||||
|
private readonly Configuration _configuration;
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
|
private readonly Regex _sfxRegex;
|
||||||
|
|
||||||
|
|
||||||
|
public ChannelChatMessageHandler(
|
||||||
|
User user,
|
||||||
|
TTSPlayer player,
|
||||||
|
CommandManager commands,
|
||||||
|
IGroupPermissionManager permissionManager,
|
||||||
|
IChatterGroupManager chatterGroupManager,
|
||||||
|
IEmoteDatabase emotes,
|
||||||
|
[FromKeyedServices("hermes")] SocketClient<WebSocketMessage> hermes,
|
||||||
|
[FromKeyedServices("obs")] SocketClient<WebSocketMessage> obs,
|
||||||
|
Configuration configuration,
|
||||||
|
ILogger logger
|
||||||
|
)
|
||||||
|
{
|
||||||
|
_user = user;
|
||||||
|
_player = player;
|
||||||
|
_commands = commands;
|
||||||
|
_permissionManager = permissionManager;
|
||||||
|
_chatterGroupManager = chatterGroupManager;
|
||||||
|
_emotes = emotes;
|
||||||
|
_obs = (obs as OBSSocketClient)!;
|
||||||
|
_hermes = (hermes as HermesSocketClient)!;
|
||||||
|
_configuration = configuration;
|
||||||
|
_logger = logger;
|
||||||
|
|
||||||
|
_sfxRegex = new Regex(@"\(([A-Za-z0-9_-]+)\)", RegexOptions.Compiled);
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Execute(TwitchWebsocketClient sender, object? data)
|
||||||
|
{
|
||||||
|
if (sender == null)
|
||||||
|
return;
|
||||||
|
if (data == null)
|
||||||
|
{
|
||||||
|
_logger.Warning("Twitch websocket message data is null.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data is not ChannelChatMessage message)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (_hermes.Connected && !_hermes.Ready)
|
||||||
|
{
|
||||||
|
_logger.Debug($"TTS is not yet ready. Ignoring chat messages [message id: {message.MessageId}]");
|
||||||
|
return; // new MessageResult(MessageStatus.NotReady, -1, -1);
|
||||||
|
}
|
||||||
|
if (_configuration.Twitch?.TtsWhenOffline != true && !_obs.Streaming)
|
||||||
|
{
|
||||||
|
_logger.Debug($"OBS is not streaming. Ignoring chat messages [message id: {message.MessageId}]");
|
||||||
|
return; // new MessageResult(MessageStatus.NotReady, -1, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
var msg = message.Message.Text;
|
||||||
|
var chatterId = long.Parse(message.ChatterUserId);
|
||||||
|
var tasks = new List<Task>();
|
||||||
|
|
||||||
|
var defaultGroups = new string[] { "everyone" };
|
||||||
|
var badgesGroups = message.Badges.Select(b => b.SetId).Select(GetGroupNameByBadgeName);
|
||||||
|
var customGroups = _chatterGroupManager.GetGroupNamesFor(chatterId);
|
||||||
|
var groups = defaultGroups.Union(badgesGroups).Union(customGroups);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var commandResult = await _commands.Execute(msg, message, groups);
|
||||||
|
if (commandResult != ChatCommandResult.Unknown)
|
||||||
|
return; // new MessageResult(MessageStatus.Command, -1, -1);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Error(ex, $"Failed executing a chat command [message: {msg}][chatter: {message.ChatterUserLogin}][chatter id: {message.ChatterUserId}][message id: {message.MessageId}]");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.Reply != null)
|
||||||
|
msg = msg.Substring(message.Reply.ParentUserLogin.Length + 2);
|
||||||
|
|
||||||
|
var permissionPath = "tts.chat.messages.read";
|
||||||
|
if (!string.IsNullOrWhiteSpace(message.ChannelPointsCustomRewardId))
|
||||||
|
permissionPath = "tts.chat.redemptions.read";
|
||||||
|
|
||||||
|
var permission = chatterId == _user.OwnerId ? true : _permissionManager.CheckIfAllowed(groups, permissionPath);
|
||||||
|
if (permission != true)
|
||||||
|
{
|
||||||
|
_logger.Debug($"Blocked message by {message.ChatterUserLogin}: {msg}");
|
||||||
|
return; // new MessageResult(MessageStatus.Blocked, -1, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep track of emotes usage
|
||||||
|
var emotesUsed = new HashSet<string>();
|
||||||
|
var newEmotes = new Dictionary<string, string>();
|
||||||
|
foreach (var fragment in message.Message.Fragments)
|
||||||
|
{
|
||||||
|
if (fragment.Emote != null)
|
||||||
|
{
|
||||||
|
if (_emotes.Get(fragment.Text) == null)
|
||||||
|
{
|
||||||
|
newEmotes.Add(fragment.Text, fragment.Emote.Id);
|
||||||
|
_emotes.Add(fragment.Text, fragment.Emote.Id);
|
||||||
|
}
|
||||||
|
emotesUsed.Add(fragment.Emote.Id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fragment.Mention != null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var text = fragment.Text.Trim();
|
||||||
|
var textFragments = text.Split(' ');
|
||||||
|
foreach (var f in textFragments)
|
||||||
|
{
|
||||||
|
var emoteId = _emotes.Get(f);
|
||||||
|
if (emoteId != null)
|
||||||
|
{
|
||||||
|
emotesUsed.Add(emoteId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (_obs.Streaming)
|
||||||
|
{
|
||||||
|
if (newEmotes.Any())
|
||||||
|
tasks.Add(_hermes.SendEmoteDetails(newEmotes));
|
||||||
|
if (emotesUsed.Any())
|
||||||
|
tasks.Add(_hermes.SendEmoteUsage(message.MessageId, chatterId, emotesUsed));
|
||||||
|
if (!_user.Chatters.Contains(chatterId))
|
||||||
|
{
|
||||||
|
tasks.Add(_hermes.SendChatterDetails(chatterId, message.ChatterUserLogin));
|
||||||
|
_user.Chatters.Add(chatterId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace filtered words.
|
||||||
|
if (_user.RegexFilters != null)
|
||||||
|
{
|
||||||
|
foreach (var wf in _user.RegexFilters)
|
||||||
|
{
|
||||||
|
if (wf.Search == null || wf.Replace == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (wf.Regex != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
msg = wf.Regex.Replace(msg, wf.Replace);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
wf.Regex = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
msg = msg.Replace(wf.Search, wf.Replace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine the priority of this message
|
||||||
|
int priority = _chatterGroupManager.GetPriorityFor(groups); // + m.SubscribedMonthCount * (m.IsSubscriber ? 10 : 5);
|
||||||
|
|
||||||
|
// Determine voice selected.
|
||||||
|
string voiceSelected = _user.DefaultTTSVoice;
|
||||||
|
if (_user.VoicesSelected?.ContainsKey(chatterId) == true)
|
||||||
|
{
|
||||||
|
var voiceId = _user.VoicesSelected[chatterId];
|
||||||
|
if (_user.VoicesAvailable.TryGetValue(voiceId, out string? voiceName) && voiceName != null)
|
||||||
|
{
|
||||||
|
if (_user.VoicesEnabled.Contains(voiceName) || chatterId == _user.OwnerId)
|
||||||
|
voiceSelected = voiceName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine additional voices used
|
||||||
|
var matches = _user.VoiceNameRegex?.Matches(msg).ToArray();
|
||||||
|
if (matches == null || matches.FirstOrDefault() == null || matches.First().Index < 0)
|
||||||
|
{
|
||||||
|
HandlePartialMessage(priority, voiceSelected, msg.Trim(), message);
|
||||||
|
return; // new MessageResult(MessageStatus.None, _user.TwitchUserId, chatterId, emotesUsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
HandlePartialMessage(priority, voiceSelected, msg.Substring(0, matches.First().Index).Trim(), message);
|
||||||
|
foreach (Match match in matches)
|
||||||
|
{
|
||||||
|
var m = match.Groups[2].ToString();
|
||||||
|
if (string.IsNullOrWhiteSpace(m))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var voice = match.Groups[1].ToString();
|
||||||
|
voice = voice[0].ToString().ToUpper() + voice.Substring(1).ToLower();
|
||||||
|
HandlePartialMessage(priority, voice, m.Trim(), message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tasks.Any())
|
||||||
|
await Task.WhenAll(tasks);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandlePartialMessage(int priority, string voice, string message, ChannelChatMessage e)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(message))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var parts = _sfxRegex.Split(message);
|
||||||
|
var chatterId = long.Parse(e.ChatterUserId);
|
||||||
|
var badgesString = string.Join(", ", e.Badges.Select(b => b.SetId + '|' + b.Id + '=' + b.Info));
|
||||||
|
|
||||||
|
if (parts.Length == 1)
|
||||||
|
{
|
||||||
|
_logger.Information($"Username: {e.ChatterUserLogin}; User ID: {e.ChatterUserId}; Voice: {voice}; Priority: {priority}; Message: {message}; Reward Id: {e.ChannelPointsCustomRewardId}; {badgesString}");
|
||||||
|
_player.Add(new TTSMessage()
|
||||||
|
{
|
||||||
|
Voice = voice,
|
||||||
|
Message = message,
|
||||||
|
Timestamp = DateTime.UtcNow,
|
||||||
|
ChatterId = chatterId,
|
||||||
|
MessageId = e.MessageId,
|
||||||
|
Badges = e.Badges,
|
||||||
|
Priority = priority
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sfxMatches = _sfxRegex.Matches(message);
|
||||||
|
var sfxStart = sfxMatches.FirstOrDefault()?.Index ?? message.Length;
|
||||||
|
|
||||||
|
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"))
|
||||||
|
{
|
||||||
|
parts[i * 2 + 2] = parts[i * 2] + " (" + parts[i * 2 + 1] + ")" + parts[i * 2 + 2];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(parts[i * 2]))
|
||||||
|
{
|
||||||
|
_logger.Information($"Username: {e.ChatterUserLogin}; User ID: {e.ChatterUserId}; Voice: {voice}; Priority: {priority}; Message: {parts[i * 2]}; {badgesString}");
|
||||||
|
_player.Add(new TTSMessage()
|
||||||
|
{
|
||||||
|
Voice = voice,
|
||||||
|
Message = parts[i * 2],
|
||||||
|
Timestamp = DateTime.UtcNow,
|
||||||
|
ChatterId = chatterId,
|
||||||
|
MessageId = e.MessageId,
|
||||||
|
Badges = e.Badges,
|
||||||
|
Priority = priority
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.Information($"Username: {e.ChatterUserLogin}; User ID: {e.ChatterUserId}; Voice: {voice}; Priority: {priority}; SFX: {sfxName}; {badgesString}");
|
||||||
|
_player.Add(new TTSMessage()
|
||||||
|
{
|
||||||
|
Voice = voice,
|
||||||
|
File = $"sfx/{sfxName}.mp3",
|
||||||
|
Timestamp = DateTime.UtcNow,
|
||||||
|
ChatterId = chatterId,
|
||||||
|
MessageId = e.MessageId,
|
||||||
|
Badges = e.Badges,
|
||||||
|
Priority = priority
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(parts.Last()))
|
||||||
|
{
|
||||||
|
_logger.Information($"Username: {e.ChatterUserLogin}; User ID: {e.ChatterUserId}; Voice: {voice}; Priority: {priority}; Message: {parts.Last()}; {badgesString}");
|
||||||
|
_player.Add(new TTSMessage()
|
||||||
|
{
|
||||||
|
Voice = voice,
|
||||||
|
Message = parts.Last(),
|
||||||
|
Timestamp = DateTime.UtcNow,
|
||||||
|
ChatterId = chatterId,
|
||||||
|
MessageId = e.MessageId,
|
||||||
|
Badges = e.Badges,
|
||||||
|
Priority = priority
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetGroupNameByBadgeName(string badgeName)
|
||||||
|
{
|
||||||
|
if (badgeName == "subscriber")
|
||||||
|
return "subscribers";
|
||||||
|
if (badgeName == "moderator")
|
||||||
|
return "moderators";
|
||||||
|
return badgeName.ToLower();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
56
Twitch/Socket/Handlers/ChannelCustomRedemptionHandler.cs
Normal file
56
Twitch/Socket/Handlers/ChannelCustomRedemptionHandler.cs
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
using Serilog;
|
||||||
|
using TwitchChatTTS.Twitch.Redemptions;
|
||||||
|
using TwitchChatTTS.Twitch.Socket.Messages;
|
||||||
|
|
||||||
|
namespace TwitchChatTTS.Twitch.Socket.Handlers
|
||||||
|
{
|
||||||
|
public class ChannelCustomRedemptionHandler : ITwitchSocketHandler
|
||||||
|
{
|
||||||
|
public string Name => "channel.channel_points_custom_reward_redemption.add";
|
||||||
|
|
||||||
|
private readonly RedemptionManager _redemptionManager;
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
|
public ChannelCustomRedemptionHandler(
|
||||||
|
RedemptionManager redemptionManager,
|
||||||
|
ILogger logger
|
||||||
|
)
|
||||||
|
{
|
||||||
|
_redemptionManager = redemptionManager;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Execute(TwitchWebsocketClient sender, object? data)
|
||||||
|
{
|
||||||
|
if (data is not ChannelCustomRedemptionMessage message)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_logger.Information($"Channel Point Reward Redeemed [redeem: {message.Reward.Title}][redeem id: {message.Reward.Id}][transaction: {message.Id}]");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var actions = _redemptionManager.Get(message.Reward.Id);
|
||||||
|
if (!actions.Any())
|
||||||
|
{
|
||||||
|
_logger.Debug($"No redemable actions for this redeem was found [redeem: {message.Reward.Title}][redeem id: {message.Reward.Id}][transaction: {message.Id}]");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_logger.Debug($"Found {actions.Count} actions for this Twitch channel point redemption [redeem: {message.Reward.Title}][redeem id: {message.Reward.Id}][transaction: {message.Id}]");
|
||||||
|
|
||||||
|
foreach (var action in actions)
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _redemptionManager.Execute(action, message.UserName, long.Parse(message.UserId));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Error(ex, $"Failed to execute redeeemable action [action: {action.Name}][action type: {action.Type}][redeem: {message.Reward.Title}][redeem id: {message.Reward.Id}][transaction: {message.Id}]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Error(ex, $"Failed to fetch the redeemable actions for a redemption [redeem: {message.Reward.Title}][redeem id: {message.Reward.Id}][transaction: {message.Id}]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
33
Twitch/Socket/Handlers/ChannelSubscriptionHandler.cs
Normal file
33
Twitch/Socket/Handlers/ChannelSubscriptionHandler.cs
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
using Serilog;
|
||||||
|
using TwitchChatTTS.Twitch.Socket.Messages;
|
||||||
|
|
||||||
|
namespace TwitchChatTTS.Twitch.Socket.Handlers
|
||||||
|
{
|
||||||
|
public class ChannelSubscriptionHandler : ITwitchSocketHandler
|
||||||
|
{
|
||||||
|
public string Name => "channel.subscription.message";
|
||||||
|
|
||||||
|
private readonly TTSPlayer _player;
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
|
public ChannelSubscriptionHandler(TTSPlayer player, ILogger logger) {
|
||||||
|
_player = player;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Execute(TwitchWebsocketClient sender, object? data)
|
||||||
|
{
|
||||||
|
if (sender == null)
|
||||||
|
return;
|
||||||
|
if (data == null)
|
||||||
|
{
|
||||||
|
_logger.Warning("Twitch websocket message data is null.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data is not ChannelSubscriptionMessage message)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_logger.Debug("Subscription occured.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
8
Twitch/Socket/Handlers/ITwitchSocketHandler.cs
Normal file
8
Twitch/Socket/Handlers/ITwitchSocketHandler.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
namespace TwitchChatTTS.Twitch.Socket.Handlers
|
||||||
|
{
|
||||||
|
public interface ITwitchSocketHandler
|
||||||
|
{
|
||||||
|
string Name { get; }
|
||||||
|
Task Execute(TwitchWebsocketClient sender, object? data);
|
||||||
|
}
|
||||||
|
}
|
69
Twitch/Socket/Handlers/NotificationHandler.cs
Normal file
69
Twitch/Socket/Handlers/NotificationHandler.cs
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Serilog;
|
||||||
|
using TwitchChatTTS.Twitch.Socket.Messages;
|
||||||
|
|
||||||
|
namespace TwitchChatTTS.Twitch.Socket.Handlers
|
||||||
|
{
|
||||||
|
public sealed class NotificationHandler : ITwitchSocketHandler
|
||||||
|
{
|
||||||
|
public string Name => "notification";
|
||||||
|
|
||||||
|
private IDictionary<string, ITwitchSocketHandler> _handlers;
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
|
private IDictionary<string, Type> _messageTypes;
|
||||||
|
private readonly JsonSerializerOptions _options;
|
||||||
|
|
||||||
|
public NotificationHandler(
|
||||||
|
[FromKeyedServices("twitch-notifications")] IEnumerable<ITwitchSocketHandler> handlers,
|
||||||
|
ILogger logger
|
||||||
|
)
|
||||||
|
{
|
||||||
|
_handlers = handlers.ToDictionary(h => h.Name, h => h);
|
||||||
|
_logger = logger;
|
||||||
|
|
||||||
|
_options = new JsonSerializerOptions() {
|
||||||
|
PropertyNameCaseInsensitive = false,
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
|
||||||
|
};
|
||||||
|
|
||||||
|
_messageTypes = new Dictionary<string, Type>();
|
||||||
|
_messageTypes.Add("channel.ban", typeof(ChannelBanMessage));
|
||||||
|
_messageTypes.Add("channel.chat.message", typeof(ChannelChatMessage));
|
||||||
|
_messageTypes.Add("channel.chat.clear_user_messages", typeof(ChannelChatClearUserMessage));
|
||||||
|
_messageTypes.Add("channel.chat.clear", typeof(ChannelChatClearMessage));
|
||||||
|
_messageTypes.Add("channel.chat.message_delete", typeof(ChannelChatDeleteMessage));
|
||||||
|
_messageTypes.Add("channel.channel_points_custom_reward_redemption.add", typeof(ChannelCustomRedemptionMessage));
|
||||||
|
_messageTypes.Add("channel.subscription.message", typeof(ChannelSubscriptionMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Execute(TwitchWebsocketClient sender, object? data)
|
||||||
|
{
|
||||||
|
if (sender == null)
|
||||||
|
return;
|
||||||
|
if (data == null)
|
||||||
|
{
|
||||||
|
_logger.Warning("Twitch websocket message data is null.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data is not NotificationMessage message)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!_messageTypes.TryGetValue(message.Subscription.Type, out var type) || type == null)
|
||||||
|
{
|
||||||
|
_logger.Warning($"Could not find Twitch notification type [message type: {message.Subscription.Type}]");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_handlers.TryGetValue(message.Subscription.Type, out ITwitchSocketHandler? handler) || handler == null)
|
||||||
|
{
|
||||||
|
_logger.Warning($"Could not find Twitch notification handler [message type: {message.Subscription.Type}]");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var d = JsonSerializer.Deserialize(message.Event.ToString()!, type, _options);
|
||||||
|
await handler.Execute(sender, d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
47
Twitch/Socket/Handlers/SessionReconnectHandler.cs
Normal file
47
Twitch/Socket/Handlers/SessionReconnectHandler.cs
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
using CommonSocketLibrary.Abstract;
|
||||||
|
using Serilog;
|
||||||
|
using TwitchChatTTS.Twitch.Socket.Messages;
|
||||||
|
|
||||||
|
namespace TwitchChatTTS.Twitch.Socket.Handlers
|
||||||
|
{
|
||||||
|
public class SessionReconnectHandler : ITwitchSocketHandler
|
||||||
|
{
|
||||||
|
public string Name => "session_reconnect";
|
||||||
|
|
||||||
|
private readonly TwitchApiClient _api;
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
|
public SessionReconnectHandler(TwitchApiClient api, ILogger logger)
|
||||||
|
{
|
||||||
|
_api = api;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Execute(TwitchWebsocketClient sender, object? data)
|
||||||
|
{
|
||||||
|
if (sender == null)
|
||||||
|
return;
|
||||||
|
if (data == null)
|
||||||
|
{
|
||||||
|
_logger.Warning("Twitch websocket message data is null.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data is not SessionWelcomeMessage message)
|
||||||
|
return;
|
||||||
|
if (_api == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(message.Session.Id))
|
||||||
|
{
|
||||||
|
_logger.Warning($"No session info provided by Twitch [status: {message.Session.Status}]");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Be able to handle multiple websocket connections.
|
||||||
|
sender.URL = message.Session.ReconnectUrl;
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(29));
|
||||||
|
await sender.DisconnectAsync(new SocketDisconnectionEventArgs("Close", "Twitch asking to reconnect."));
|
||||||
|
await sender.Connect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
94
Twitch/Socket/Handlers/SessionWelcomeHandler.cs
Normal file
94
Twitch/Socket/Handlers/SessionWelcomeHandler.cs
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
using CommonSocketLibrary.Abstract;
|
||||||
|
using Serilog;
|
||||||
|
using TwitchChatTTS.Twitch.Socket.Messages;
|
||||||
|
|
||||||
|
namespace TwitchChatTTS.Twitch.Socket.Handlers
|
||||||
|
{
|
||||||
|
public class SessionWelcomeHandler : ITwitchSocketHandler
|
||||||
|
{
|
||||||
|
public string Name => "session_welcome";
|
||||||
|
|
||||||
|
private readonly TwitchApiClient _api;
|
||||||
|
private readonly User _user;
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
|
public SessionWelcomeHandler(TwitchApiClient api, User user, ILogger logger)
|
||||||
|
{
|
||||||
|
_api = api;
|
||||||
|
_user = user;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Execute(TwitchWebsocketClient sender, object? data)
|
||||||
|
{
|
||||||
|
if (sender == null)
|
||||||
|
return;
|
||||||
|
if (data == null)
|
||||||
|
{
|
||||||
|
_logger.Warning("Twitch websocket message data is null.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data is not SessionWelcomeMessage message)
|
||||||
|
return;
|
||||||
|
if (_api == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(message.Session.Id))
|
||||||
|
{
|
||||||
|
_logger.Warning($"No session info provided by Twitch [status: {message.Session.Status}]");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string[] subscriptionsv1 = [
|
||||||
|
"channel.chat.message",
|
||||||
|
"channel.chat.message_delete",
|
||||||
|
"channel.chat.notification",
|
||||||
|
"channel.chat.clear",
|
||||||
|
"channel.chat.clear_user_messages",
|
||||||
|
"channel.ad_break.begin",
|
||||||
|
"channel.subscription.message",
|
||||||
|
"channel.ban",
|
||||||
|
"channel.channel_points_custom_reward_redemption.add"
|
||||||
|
];
|
||||||
|
string[] subscriptionsv2 = [
|
||||||
|
"channel.follow",
|
||||||
|
];
|
||||||
|
string broadcasterId = _user.TwitchUserId.ToString();
|
||||||
|
foreach (var subscription in subscriptionsv1)
|
||||||
|
await Subscribe(subscription, message.Session.Id, broadcasterId, "1");
|
||||||
|
foreach (var subscription in subscriptionsv2)
|
||||||
|
await Subscribe(subscription, message.Session.Id, broadcasterId, "2");
|
||||||
|
|
||||||
|
sender.SessionId = message.Session.Id;
|
||||||
|
sender.Identified = sender.SessionId != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task Subscribe(string subscriptionName, string sessionId, string broadcasterId, string version)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var response = await _api.CreateEventSubscription(subscriptionName, version, sessionId, broadcasterId);
|
||||||
|
if (response == null)
|
||||||
|
{
|
||||||
|
_logger.Error($"Failed to create an event subscription [subscription type: {subscriptionName}][reason: response is null]");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (response.Data == null)
|
||||||
|
{
|
||||||
|
_logger.Error($"Failed to create an event subscription [subscription type: {subscriptionName}][reason: data is null]");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!response.Data.Any())
|
||||||
|
{
|
||||||
|
_logger.Error($"Failed to create an event subscription [subscription type: {subscriptionName}][reason: data is empty]");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_logger.Information($"Sucessfully added subscription to Twitch websockets [subscription type: {subscriptionName}]");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Error(ex, $"Failed to create an event subscription [subscription type: {subscriptionName}][reason: exception]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
19
Twitch/Socket/Messages/ChannelBanMessage.cs
Normal file
19
Twitch/Socket/Messages/ChannelBanMessage.cs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
namespace TwitchChatTTS.Twitch.Socket.Messages
|
||||||
|
{
|
||||||
|
public class ChannelBanMessage
|
||||||
|
{
|
||||||
|
public string UserId { get; set; }
|
||||||
|
public string UserLogin { get; set; }
|
||||||
|
public string UserName { get; set; }
|
||||||
|
public string BroadcasterUserId { get; set; }
|
||||||
|
public string BroadcasterUserLogin { get; set; }
|
||||||
|
public string BroadcasterUserName { get; set; }
|
||||||
|
public string ModeratorUserId { get; set; }
|
||||||
|
public string ModeratorUserLogin { get; set; }
|
||||||
|
public string ModeratorUserName { get; set; }
|
||||||
|
public string Reason { get; set; }
|
||||||
|
public DateTime BannedAt { get; set; }
|
||||||
|
public DateTime? EndsAt { get; set; }
|
||||||
|
public bool IsPermanent { get; set; }
|
||||||
|
}
|
||||||
|
}
|
9
Twitch/Socket/Messages/ChannelChatClearMessage.cs
Normal file
9
Twitch/Socket/Messages/ChannelChatClearMessage.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
namespace TwitchChatTTS.Twitch.Socket.Messages
|
||||||
|
{
|
||||||
|
public class ChannelChatClearMessage
|
||||||
|
{
|
||||||
|
public string BroadcasterUserId { get; set; }
|
||||||
|
public string BroadcasterUserLogin { get; set; }
|
||||||
|
public string BroadcasterUserName { get; set; }
|
||||||
|
}
|
||||||
|
}
|
9
Twitch/Socket/Messages/ChannelChatClearUserMessage.cs
Normal file
9
Twitch/Socket/Messages/ChannelChatClearUserMessage.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
namespace TwitchChatTTS.Twitch.Socket.Messages
|
||||||
|
{
|
||||||
|
public class ChannelChatClearUserMessage : ChannelChatClearMessage
|
||||||
|
{
|
||||||
|
public string TargetUserId { get; set; }
|
||||||
|
public string TargetUserLogin { get; set; }
|
||||||
|
public string TargetUserName { get; set; }
|
||||||
|
}
|
||||||
|
}
|
7
Twitch/Socket/Messages/ChannelChatDeleteMessage.cs
Normal file
7
Twitch/Socket/Messages/ChannelChatDeleteMessage.cs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
namespace TwitchChatTTS.Twitch.Socket.Messages
|
||||||
|
{
|
||||||
|
public class ChannelChatDeleteMessage : ChannelChatClearUserMessage
|
||||||
|
{
|
||||||
|
public string MessageId { get; set; }
|
||||||
|
}
|
||||||
|
}
|
75
Twitch/Socket/Messages/ChannelChatMessage.cs
Normal file
75
Twitch/Socket/Messages/ChannelChatMessage.cs
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
namespace TwitchChatTTS.Twitch.Socket.Messages
|
||||||
|
{
|
||||||
|
public class ChannelChatMessage
|
||||||
|
{
|
||||||
|
public string BroadcasterUserId { get; set; }
|
||||||
|
public string BroadcasterUserLogin { get; set; }
|
||||||
|
public string BroadcasterUserName { get; set; }
|
||||||
|
public string ChatterUserId { get; set; }
|
||||||
|
public string ChatterUserLogin { get; set; }
|
||||||
|
public string ChatterUserName { get; set; }
|
||||||
|
public string MessageId { get; set; }
|
||||||
|
public TwitchChatMessageInfo Message { get; set; }
|
||||||
|
public string MessageType { get; set; }
|
||||||
|
public TwitchBadge[] Badges { get; set; }
|
||||||
|
public TwitchReplyInfo? Reply { get; set; }
|
||||||
|
public string? ChannelPointsCustomRewardId { get; set; }
|
||||||
|
public string? ChannelPointsAnimationId { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TwitchChatMessageInfo
|
||||||
|
{
|
||||||
|
public string Text { get; set; }
|
||||||
|
public TwitchChatFragment[] Fragments { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TwitchChatFragment
|
||||||
|
{
|
||||||
|
public string Type { get; set; }
|
||||||
|
public string Text { get; set; }
|
||||||
|
public TwitchCheerInfo? Cheermote { get; set; }
|
||||||
|
public TwitchEmoteInfo? Emote { get; set; }
|
||||||
|
public TwitchMentionInfo? Mention { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TwitchCheerInfo
|
||||||
|
{
|
||||||
|
public string Prefix { get; set; }
|
||||||
|
public int Bits { get; set; }
|
||||||
|
public int Tier { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TwitchEmoteInfo
|
||||||
|
{
|
||||||
|
public string Id { get; set; }
|
||||||
|
public string EmoteSetId { get; set; }
|
||||||
|
public string OwnerId { get; set; }
|
||||||
|
public string[] Format { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TwitchMentionInfo
|
||||||
|
{
|
||||||
|
public string UserId { get; set; }
|
||||||
|
public string UserName { get; set; }
|
||||||
|
public string UserLogin { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TwitchBadge
|
||||||
|
{
|
||||||
|
public string SetId { get; set; }
|
||||||
|
public string Id { get; set; }
|
||||||
|
public string Info { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TwitchReplyInfo
|
||||||
|
{
|
||||||
|
public string ParentMessageId { get; set; }
|
||||||
|
public string ParentMessageBody { get; set; }
|
||||||
|
public string ParentUserId { get; set; }
|
||||||
|
public string ParentUserName { get; set; }
|
||||||
|
public string ParentUserLogin { get; set; }
|
||||||
|
public string ThreadMessageId { get; set; }
|
||||||
|
public string ThreadUserName { get; set; }
|
||||||
|
public string ThreadUserLogin { get; set; }
|
||||||
|
}
|
||||||
|
}
|
24
Twitch/Socket/Messages/ChannelCustomRedemptionMessage.cs
Normal file
24
Twitch/Socket/Messages/ChannelCustomRedemptionMessage.cs
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
namespace TwitchChatTTS.Twitch.Socket.Messages
|
||||||
|
{
|
||||||
|
public class ChannelCustomRedemptionMessage
|
||||||
|
{
|
||||||
|
public string BroadcasterUserId { get; set; }
|
||||||
|
public string BroadcasterUserLogin { get; set; }
|
||||||
|
public string BroadcasterUserName { get; set; }
|
||||||
|
public string Id { get; set; }
|
||||||
|
public string UserId { get; set; }
|
||||||
|
public string UserLogin { get; set; }
|
||||||
|
public string UserName { get; set; }
|
||||||
|
public string Status { get; set; }
|
||||||
|
public DateTime RedeemedAt { get; set; }
|
||||||
|
public RedemptionReward Reward { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class RedemptionReward
|
||||||
|
{
|
||||||
|
public string Id { get; set; }
|
||||||
|
public string Title { get; set; }
|
||||||
|
public string Prompt { get; set; }
|
||||||
|
public int Cost { get; set; }
|
||||||
|
}
|
||||||
|
}
|
17
Twitch/Socket/Messages/ChannelSubscriptionMessage.cs
Normal file
17
Twitch/Socket/Messages/ChannelSubscriptionMessage.cs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
namespace TwitchChatTTS.Twitch.Socket.Messages
|
||||||
|
{
|
||||||
|
public class ChannelSubscriptionMessage
|
||||||
|
{
|
||||||
|
public string BroadcasterUserId { get; set; }
|
||||||
|
public string BroadcasterUserLogin { get; set; }
|
||||||
|
public string BroadcasterUserName { get; set; }
|
||||||
|
public string ChatterUserId { get; set; }
|
||||||
|
public string ChatterUserLogin { get; set; }
|
||||||
|
public string ChatterUserName { get; set; }
|
||||||
|
public string Tier { get; set; }
|
||||||
|
public TwitchChatMessageInfo Message { get; set; }
|
||||||
|
public int CumulativeMonths { get; set; }
|
||||||
|
public int StreakMonths { get; set; }
|
||||||
|
public int DurationMonths { get; set; }
|
||||||
|
}
|
||||||
|
}
|
10
Twitch/Socket/Messages/EventResponse.cs
Normal file
10
Twitch/Socket/Messages/EventResponse.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
namespace TwitchChatTTS.Twitch.Socket.Messages
|
||||||
|
{
|
||||||
|
public class EventResponse<T>
|
||||||
|
{
|
||||||
|
public T[]? Data { get; set; }
|
||||||
|
public int Total { get; set; }
|
||||||
|
public int TotalCost { get; set; }
|
||||||
|
public int MaxTotalCost { get; set; }
|
||||||
|
}
|
||||||
|
}
|
66
Twitch/Socket/Messages/EventSubscriptionMessage.cs
Normal file
66
Twitch/Socket/Messages/EventSubscriptionMessage.cs
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace TwitchChatTTS.Twitch.Socket.Messages
|
||||||
|
{
|
||||||
|
public class EventSubscriptionMessage : IVersionedMessage
|
||||||
|
{
|
||||||
|
public string Type { get; set; }
|
||||||
|
public string Version { get; set; }
|
||||||
|
public IDictionary<string, string> Condition { get; set; }
|
||||||
|
public EventSubTransport Transport { get; set; }
|
||||||
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
|
public int? Cost { get; set; }
|
||||||
|
|
||||||
|
public EventSubscriptionMessage() {
|
||||||
|
Type = string.Empty;
|
||||||
|
Version = string.Empty;
|
||||||
|
Condition = new Dictionary<string, string>();
|
||||||
|
Transport = new EventSubTransport();
|
||||||
|
}
|
||||||
|
|
||||||
|
public EventSubscriptionMessage(string type, string version, string callback, string secret, IDictionary<string, string>? conditions = null)
|
||||||
|
{
|
||||||
|
Type = type;
|
||||||
|
Version = version;
|
||||||
|
Condition = conditions ?? new Dictionary<string, string>();
|
||||||
|
Transport = new EventSubTransport("webhook", callback, secret);
|
||||||
|
}
|
||||||
|
|
||||||
|
public EventSubscriptionMessage(string type, string version, string sessionId, IDictionary<string, string>? conditions = null)
|
||||||
|
{
|
||||||
|
Type = type;
|
||||||
|
Version = version;
|
||||||
|
Condition = conditions ?? new Dictionary<string, string>();
|
||||||
|
Transport = new EventSubTransport("websocket", sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public class EventSubTransport
|
||||||
|
{
|
||||||
|
public string Method { get; }
|
||||||
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
|
public string? Callback { get; }
|
||||||
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
|
public string? Secret { get; }
|
||||||
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
|
public string? SessionId { get; }
|
||||||
|
|
||||||
|
public EventSubTransport() {
|
||||||
|
Method = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public EventSubTransport(string method, string callback, string secret)
|
||||||
|
{
|
||||||
|
Method = method;
|
||||||
|
Callback = callback;
|
||||||
|
Secret = secret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public EventSubTransport(string method, string sessionId)
|
||||||
|
{
|
||||||
|
Method = method;
|
||||||
|
SessionId = sessionId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
16
Twitch/Socket/Messages/NotificationMessage.cs
Normal file
16
Twitch/Socket/Messages/NotificationMessage.cs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
namespace TwitchChatTTS.Twitch.Socket.Messages
|
||||||
|
{
|
||||||
|
public class NotificationMessage
|
||||||
|
{
|
||||||
|
public NotificationInfo Subscription { get; set; }
|
||||||
|
public object Event { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class NotificationInfo : EventSubscriptionMessage
|
||||||
|
{
|
||||||
|
public string Id { get; set; }
|
||||||
|
public string Status { get; set; }
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
public object Event { get; set; }
|
||||||
|
}
|
||||||
|
}
|
16
Twitch/Socket/Messages/SessionWelcomeMessage.cs
Normal file
16
Twitch/Socket/Messages/SessionWelcomeMessage.cs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
namespace TwitchChatTTS.Twitch.Socket.Messages
|
||||||
|
{
|
||||||
|
public class SessionWelcomeMessage
|
||||||
|
{
|
||||||
|
public TwitchSocketSession Session { get; set; }
|
||||||
|
|
||||||
|
public class TwitchSocketSession {
|
||||||
|
public string Id { get; set; }
|
||||||
|
public string Status { get; set; }
|
||||||
|
public DateTime ConnectedAt { get; set; }
|
||||||
|
public int KeepaliveTimeoutSeconds { get; set; }
|
||||||
|
public string? ReconnectUrl { get; set; }
|
||||||
|
public string? RecoveryUrl { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
18
Twitch/Socket/Messages/TwitchWebsocketMessage.cs
Normal file
18
Twitch/Socket/Messages/TwitchWebsocketMessage.cs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
namespace TwitchChatTTS.Twitch.Socket.Messages
|
||||||
|
{
|
||||||
|
public class TwitchWebsocketMessage
|
||||||
|
{
|
||||||
|
public TwitchMessageMetadata Metadata { get; set; }
|
||||||
|
public object? Payload { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TwitchMessageMetadata {
|
||||||
|
public string MessageId { get; set; }
|
||||||
|
public string MessageType { get; set; }
|
||||||
|
public DateTime MessageTimestamp { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IVersionedMessage {
|
||||||
|
string Version { get; set; }
|
||||||
|
}
|
||||||
|
}
|
196
Twitch/Socket/TwitchWebsocketClient.cs
Normal file
196
Twitch/Socket/TwitchWebsocketClient.cs
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
using CommonSocketLibrary.Abstract;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Serilog;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Net.WebSockets;
|
||||||
|
using TwitchChatTTS.Twitch.Socket.Messages;
|
||||||
|
using System.Text;
|
||||||
|
using TwitchChatTTS.Twitch.Socket.Handlers;
|
||||||
|
|
||||||
|
namespace TwitchChatTTS.Twitch.Socket
|
||||||
|
{
|
||||||
|
public class TwitchWebsocketClient : SocketClient<TwitchWebsocketMessage>
|
||||||
|
{
|
||||||
|
public string URL;
|
||||||
|
|
||||||
|
private IDictionary<string, ITwitchSocketHandler> _handlers;
|
||||||
|
private IDictionary<string, Type> _messageTypes;
|
||||||
|
private readonly Configuration _configuration;
|
||||||
|
private System.Timers.Timer _reconnectTimer;
|
||||||
|
|
||||||
|
public bool Connected { get; set; }
|
||||||
|
public bool Identified { get; set; }
|
||||||
|
public string SessionId { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
public TwitchWebsocketClient(
|
||||||
|
Configuration configuration,
|
||||||
|
[FromKeyedServices("twitch")] IEnumerable<ITwitchSocketHandler> handlers,
|
||||||
|
ILogger logger
|
||||||
|
) : base(logger, new JsonSerializerOptions()
|
||||||
|
{
|
||||||
|
PropertyNameCaseInsensitive = false,
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
|
||||||
|
})
|
||||||
|
{
|
||||||
|
_handlers = handlers.ToDictionary(h => h.Name, h => h);
|
||||||
|
_configuration = configuration;
|
||||||
|
|
||||||
|
_reconnectTimer = new System.Timers.Timer(TimeSpan.FromSeconds(30));
|
||||||
|
_reconnectTimer.AutoReset = false;
|
||||||
|
_reconnectTimer.Elapsed += async (sender, e) => await Reconnect();
|
||||||
|
_reconnectTimer.Enabled = false;
|
||||||
|
|
||||||
|
_messageTypes = new Dictionary<string, Type>();
|
||||||
|
_messageTypes.Add("session_welcome", typeof(SessionWelcomeMessage));
|
||||||
|
_messageTypes.Add("session_reconnect", typeof(SessionWelcomeMessage));
|
||||||
|
_messageTypes.Add("notification", typeof(NotificationMessage));
|
||||||
|
|
||||||
|
URL = "wss://eventsub.wss.twitch.tv/ws";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void Initialize()
|
||||||
|
{
|
||||||
|
_logger.Information($"Initializing OBS websocket client.");
|
||||||
|
OnConnected += (sender, e) =>
|
||||||
|
{
|
||||||
|
Connected = true;
|
||||||
|
_reconnectTimer.Enabled = false;
|
||||||
|
_logger.Information("Twitch websocket client connected.");
|
||||||
|
};
|
||||||
|
|
||||||
|
OnDisconnected += (sender, e) =>
|
||||||
|
{
|
||||||
|
_reconnectTimer.Enabled = Identified;
|
||||||
|
_logger.Information($"Twitch websocket client disconnected [status: {e.Status}][reason: {e.Reason}] " + (Identified ? "Will be attempting to reconnect every 30 seconds." : "Will not be attempting to reconnect."));
|
||||||
|
|
||||||
|
Connected = false;
|
||||||
|
Identified = false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Connect()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(URL))
|
||||||
|
{
|
||||||
|
_logger.Warning("Lacking connection info for Twitch websockets. Not connecting to Twitch.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.Debug($"Twitch websocket client attempting to connect to {URL}");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await ConnectAsync(URL);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
_logger.Warning("Connecting to twitch failed. Skipping Twitch websockets.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task Reconnect()
|
||||||
|
{
|
||||||
|
if (Connected)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await DisconnectAsync(new SocketDisconnectionEventArgs(WebSocketCloseStatus.Empty.ToString(), ""));
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
_logger.Error("Failed to disconnect from Twitch websocket server.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Connect();
|
||||||
|
}
|
||||||
|
catch (WebSocketException wse) when (wse.Message.Contains("502"))
|
||||||
|
{
|
||||||
|
_logger.Error("Twitch websocket server cannot be found.");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Error(ex, "Failed to reconnect to Twitch websocket server.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected TwitchWebsocketMessage GenerateMessage<T>(string messageType, T data)
|
||||||
|
{
|
||||||
|
var metadata = new TwitchMessageMetadata()
|
||||||
|
{
|
||||||
|
MessageId = Guid.NewGuid().ToString(),
|
||||||
|
MessageType = messageType,
|
||||||
|
MessageTimestamp = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
return new TwitchWebsocketMessage()
|
||||||
|
{
|
||||||
|
Metadata = metadata,
|
||||||
|
Payload = data
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task OnResponseReceived(TwitchWebsocketMessage? message)
|
||||||
|
{
|
||||||
|
if (message == null || message.Metadata == null) {
|
||||||
|
_logger.Information("Twitch message is null");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string content = message.Payload?.ToString() ?? string.Empty;
|
||||||
|
if (message.Metadata.MessageType != "session_keepalive")
|
||||||
|
_logger.Information("Twitch RX #" + message.Metadata.MessageType + ": " + content);
|
||||||
|
|
||||||
|
if (!_messageTypes.TryGetValue(message.Metadata.MessageType, out var type) || type == null)
|
||||||
|
{
|
||||||
|
_logger.Debug($"Could not find Twitch message type [message type: {message.Metadata.MessageType}]");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_handlers.TryGetValue(message.Metadata.MessageType, out ITwitchSocketHandler? handler) || handler == null)
|
||||||
|
{
|
||||||
|
_logger.Debug($"Could not find Twitch handler [message type: {message.Metadata.MessageType}]");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var data = JsonSerializer.Deserialize(content, type, _options);
|
||||||
|
await handler.Execute(this, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Send<T>(string type, T data)
|
||||||
|
{
|
||||||
|
if (_socket == null || type == null || data == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var message = GenerateMessage(type, data);
|
||||||
|
var content = JsonSerializer.Serialize(message, _options);
|
||||||
|
|
||||||
|
var bytes = Encoding.UTF8.GetBytes(content);
|
||||||
|
var array = new ArraySegment<byte>(bytes);
|
||||||
|
var total = bytes.Length;
|
||||||
|
var current = 0;
|
||||||
|
|
||||||
|
while (current < total)
|
||||||
|
{
|
||||||
|
var size = Encoding.UTF8.GetBytes(content.Substring(current), array);
|
||||||
|
await _socket!.SendAsync(array, WebSocketMessageType.Text, current + size >= total, _cts!.Token);
|
||||||
|
current += size;
|
||||||
|
}
|
||||||
|
_logger.Information("TX #" + type + ": " + content);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
if (_socket.State.ToString().Contains("Close") || _socket.State == WebSocketState.Aborted)
|
||||||
|
{
|
||||||
|
await DisconnectAsync(new SocketDisconnectionEventArgs(_socket.CloseStatus.ToString()!, _socket.CloseStatusDescription ?? string.Empty));
|
||||||
|
_logger.Warning($"Socket state on closing = {_socket.State} | {_socket.CloseStatus?.ToString()} | {_socket.CloseStatusDescription}");
|
||||||
|
}
|
||||||
|
_logger.Error(e, $"Failed to send a websocket message [message type: {type}]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,226 +1,59 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using TwitchChatTTS.Helpers;
|
using TwitchChatTTS.Helpers;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
using TwitchChatTTS;
|
using TwitchChatTTS.Twitch.Socket.Messages;
|
||||||
using TwitchLib.Api.Core.Exceptions;
|
using System.Net.Http.Json;
|
||||||
using TwitchLib.Client.Events;
|
using System.Net;
|
||||||
using TwitchLib.Client.Models;
|
|
||||||
using TwitchLib.Communication.Events;
|
|
||||||
using TwitchLib.PubSub.Interfaces;
|
|
||||||
using TwitchLib.Client.Interfaces;
|
|
||||||
using TwitchChatTTS.Twitch.Redemptions;
|
|
||||||
|
|
||||||
public class TwitchApiClient
|
public class TwitchApiClient
|
||||||
{
|
{
|
||||||
private readonly RedemptionManager _redemptionManager;
|
|
||||||
private readonly HermesApiClient _hermesApiClient;
|
|
||||||
private readonly ITwitchClient _client;
|
|
||||||
private readonly ITwitchPubSub _publisher;
|
|
||||||
private readonly User _user;
|
|
||||||
private readonly Configuration _configuration;
|
|
||||||
private readonly TwitchBotAuth _token;
|
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private readonly WebClientWrap _web;
|
private readonly WebClientWrap _web;
|
||||||
private bool _initialized;
|
|
||||||
private string _broadcasterId;
|
|
||||||
|
|
||||||
|
|
||||||
public TwitchApiClient(
|
public TwitchApiClient(
|
||||||
ITwitchClient twitchClient,
|
|
||||||
ITwitchPubSub twitchPublisher,
|
|
||||||
RedemptionManager redemptionManager,
|
|
||||||
HermesApiClient hermesApiClient,
|
|
||||||
User user,
|
|
||||||
Configuration configuration,
|
|
||||||
TwitchBotAuth token,
|
|
||||||
ILogger logger
|
ILogger logger
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
_redemptionManager = redemptionManager;
|
|
||||||
_hermesApiClient = hermesApiClient;
|
|
||||||
_client = twitchClient;
|
|
||||||
_publisher = twitchPublisher;
|
|
||||||
_user = user;
|
|
||||||
_configuration = configuration;
|
|
||||||
_token = token;
|
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_initialized = false;
|
|
||||||
_broadcasterId = string.Empty;
|
|
||||||
|
|
||||||
_web = new WebClientWrap(new JsonSerializerOptions()
|
_web = new WebClientWrap(new JsonSerializerOptions()
|
||||||
{
|
{
|
||||||
PropertyNameCaseInsensitive = false,
|
PropertyNameCaseInsensitive = false,
|
||||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
|
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
|
||||||
});
|
});
|
||||||
if (!string.IsNullOrWhiteSpace(_configuration.Hermes?.Token))
|
|
||||||
_web.AddHeader("x-api-key", _configuration.Hermes.Token.Trim());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> Authorize(string broadcasterId)
|
public async Task<EventResponse<EventSubscriptionMessage>?> CreateEventSubscription(string type, string version, string userId)
|
||||||
{
|
{
|
||||||
try
|
var conditions = new Dictionary<string, string>() { { "user_id", userId }, { "broadcaster_user_id", userId }, { "moderator_user_id", userId } };
|
||||||
|
var subscriptionData = new EventSubscriptionMessage(type, version, "https://hermes.goblincaves.com/api/account/authorize", "isdnmjfopsdfmsf4390", conditions);
|
||||||
|
var response = await _web.Post("https://api.twitch.tv/helix/eventsub/subscriptions", subscriptionData);
|
||||||
|
if (response.StatusCode == HttpStatusCode.Accepted)
|
||||||
{
|
{
|
||||||
_logger.Debug($"Attempting to authorize Twitch API [id: {broadcasterId}]");
|
_logger.Debug("Twitch API call [type: create event subscription]: " + await response.Content.ReadAsStringAsync());
|
||||||
var authorize = await _web.GetJson<TwitchBotAuth>($"https://{HermesApiClient.BASE_URL}/api/account/reauthorize");
|
return await response.Content.ReadFromJsonAsync(typeof(EventResponse<EventSubscriptionMessage>)) as EventResponse<EventSubscriptionMessage>;
|
||||||
if (authorize != null && broadcasterId == authorize.BroadcasterId)
|
|
||||||
{
|
|
||||||
_token.AccessToken = authorize.AccessToken;
|
|
||||||
_token.RefreshToken = authorize.RefreshToken;
|
|
||||||
_token.UserId = authorize.UserId;
|
|
||||||
_token.BroadcasterId = authorize.BroadcasterId;
|
|
||||||
_token.ExpiresIn = authorize.ExpiresIn;
|
|
||||||
_token.UpdatedAt = DateTime.Now;
|
|
||||||
_logger.Information("Updated Twitch API tokens.");
|
|
||||||
_logger.Debug($"Twitch API Auth data [user id: {_token.UserId}][id: {_token.BroadcasterId}][expires in: {_token.ExpiresIn}][expires at: {_token.ExpiresAt.ToShortTimeString()}]");
|
|
||||||
}
|
}
|
||||||
else if (authorize != null)
|
_logger.Warning("Twitch api failed to create event subscription: " + await response.Content.ReadAsStringAsync());
|
||||||
{
|
return null;
|
||||||
_logger.Error("Twitch API Authorization failed: " + authorize.AccessToken + " | " + authorize.RefreshToken + " | " + authorize.UserId + " | " + authorize.BroadcasterId);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
_broadcasterId = broadcasterId;
|
|
||||||
_logger.Debug($"Authorized Twitch API [id: {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)
|
|
||||||
{
|
|
||||||
_logger.Debug($"Failed to Authorize Twitch API due to JSON error [id: {broadcasterId}]");
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
_logger.Error(e, "Failed to authorize to Twitch API.");
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Connect()
|
public async Task<EventResponse<EventSubscriptionMessage>?> CreateEventSubscription(string type, string version, string sessionId, string userId)
|
||||||
{
|
{
|
||||||
_client.Connect();
|
var conditions = new Dictionary<string, string>() { { "user_id", userId }, { "broadcaster_user_id", userId }, { "moderator_user_id", userId } };
|
||||||
await _publisher.ConnectAsync();
|
var subscriptionData = new EventSubscriptionMessage(type, version, sessionId, conditions);
|
||||||
|
var response = await _web.Post("https://api.twitch.tv/helix/eventsub/subscriptions", subscriptionData);
|
||||||
|
if (response.StatusCode == HttpStatusCode.Accepted)
|
||||||
|
{
|
||||||
|
_logger.Debug("Twitch API call [type: create event subscription]: " + await response.Content.ReadAsStringAsync());
|
||||||
|
return await response.Content.ReadFromJsonAsync(typeof(EventResponse<EventSubscriptionMessage>)) as EventResponse<EventSubscriptionMessage>;
|
||||||
|
}
|
||||||
|
_logger.Error("Twitch api failed to create event subscription: " + await response.Content.ReadAsStringAsync());
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void InitializeClient(string username, IEnumerable<string> channels)
|
public void Initialize(TwitchBotToken token) {
|
||||||
{
|
_web.AddHeader("Authorization", "Bearer " + token.AccessToken);
|
||||||
ConnectionCredentials credentials = new ConnectionCredentials(username, _token!.AccessToken);
|
_web.AddHeader("Client-Id", token.ClientId);
|
||||||
_client.Initialize(credentials, channels.Distinct().ToList());
|
|
||||||
|
|
||||||
if (_initialized)
|
|
||||||
{
|
|
||||||
_logger.Debug("Twitch API client has already been initialized.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_initialized = true;
|
|
||||||
|
|
||||||
_client.OnJoinedChannel += async Task (object? s, OnJoinedChannelArgs e) =>
|
|
||||||
{
|
|
||||||
_logger.Information("Joined channel: " + e.Channel);
|
|
||||||
};
|
|
||||||
|
|
||||||
_client.OnConnected += async Task (object? s, OnConnectedArgs e) =>
|
|
||||||
{
|
|
||||||
_logger.Information("Twitch API client connected.");
|
|
||||||
};
|
|
||||||
|
|
||||||
_client.OnIncorrectLogin += async Task (object? s, OnIncorrectLoginArgs e) =>
|
|
||||||
{
|
|
||||||
_logger.Error(e.Exception, "Incorrect Login on Twitch API client.");
|
|
||||||
|
|
||||||
_logger.Information("Attempting to re-authorize.");
|
|
||||||
await Authorize(_broadcasterId);
|
|
||||||
_client.SetConnectionCredentials(new ConnectionCredentials(_user.TwitchUsername, _token!.AccessToken));
|
|
||||||
|
|
||||||
await Task.Delay(TimeSpan.FromSeconds(3));
|
|
||||||
await _client.ReconnectAsync();
|
|
||||||
};
|
|
||||||
|
|
||||||
_client.OnConnectionError += async Task (object? s, OnConnectionErrorArgs e) =>
|
|
||||||
{
|
|
||||||
_logger.Error("Connection Error: " + e.Error.Message + " (" + e.Error.GetType().Name + ")");
|
|
||||||
|
|
||||||
_logger.Information("Attempting to re-authorize.");
|
|
||||||
await Authorize(_broadcasterId);
|
|
||||||
};
|
|
||||||
|
|
||||||
_client.OnError += async Task (object? s, OnErrorEventArgs e) =>
|
|
||||||
{
|
|
||||||
_logger.Error(e.Exception, "Twitch API client error.");
|
|
||||||
};
|
|
||||||
|
|
||||||
_client.OnDisconnected += async Task (s, e) => _logger.Warning("Twitch API client disconnected.");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void InitializePublisher()
|
|
||||||
{
|
|
||||||
_publisher.OnPubSubServiceConnected += async (s, e) =>
|
|
||||||
{
|
|
||||||
_publisher.ListenToChannelPoints(_token.BroadcasterId);
|
|
||||||
_publisher.ListenToFollows(_token.BroadcasterId);
|
|
||||||
|
|
||||||
await _publisher.SendTopicsAsync(_token.AccessToken);
|
|
||||||
_logger.Information("Twitch PubSub has been connected.");
|
|
||||||
};
|
|
||||||
|
|
||||||
_publisher.OnFollow += (s, e) =>
|
|
||||||
{
|
|
||||||
_logger.Information($"New Follower [name: {e.DisplayName}][username: {e.Username}]");
|
|
||||||
};
|
|
||||||
|
|
||||||
_publisher.OnChannelPointsRewardRedeemed += async (s, e) =>
|
|
||||||
{
|
|
||||||
_logger.Information($"Channel Point Reward Redeemed [redeem: {e.RewardRedeemed.Redemption.Reward.Title}][redeem id: {e.RewardRedeemed.Redemption.Reward.Id}][transaction: {e.RewardRedeemed.Redemption.Id}]");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var actions = _redemptionManager.Get(e.RewardRedeemed.Redemption.Reward.Id);
|
|
||||||
if (!actions.Any())
|
|
||||||
{
|
|
||||||
_logger.Debug($"No redemable actions for this redeem was found [redeem: {e.RewardRedeemed.Redemption.Reward.Title}][redeem id: {e.RewardRedeemed.Redemption.Reward.Id}][transaction: {e.RewardRedeemed.Redemption.Id}]");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_logger.Debug($"Found {actions.Count} actions for this Twitch channel point redemption [redeem: {e.RewardRedeemed.Redemption.Reward.Title}][redeem id: {e.RewardRedeemed.Redemption.Reward.Id}][transaction: {e.RewardRedeemed.Redemption.Id}]");
|
|
||||||
|
|
||||||
foreach (var action in actions)
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _redemptionManager.Execute(action, e.RewardRedeemed.Redemption.User.DisplayName, long.Parse(e.RewardRedeemed.Redemption.User.Id));
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.Error(ex, $"Failed to execute redeeemable action [action: {action.Name}][action type: {action.Type}][redeem: {e.RewardRedeemed.Redemption.Reward.Title}][redeem id: {e.RewardRedeemed.Redemption.Reward.Id}][transaction: {e.RewardRedeemed.Redemption.Id}]");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.Error(ex, $"Failed to fetch the redeemable actions for a redemption [redeem: {e.RewardRedeemed.Redemption.Reward.Title}][redeem id: {e.RewardRedeemed.Redemption.Reward.Id}][transaction: {e.RewardRedeemed.Redemption.Id}]");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
_publisher.OnPubSubServiceClosed += async (s, e) =>
|
|
||||||
{
|
|
||||||
_logger.Warning("Twitch PubSub ran into a service close. Attempting to connect again.");
|
|
||||||
//await Task.Delay(Math.Min(3000 + (1 << psConnectionFailures), 120000));
|
|
||||||
var authorized = await Authorize(_broadcasterId);
|
|
||||||
|
|
||||||
var twitchBotData = await _hermesApiClient.FetchTwitchBotToken();
|
|
||||||
if (twitchBotData == null)
|
|
||||||
{
|
|
||||||
Console.WriteLine("The API is down. Contact the owner.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await _publisher.ConnectAsync();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AddOnNewMessageReceived(AsyncEventHandler<OnMessageReceivedArgs> handler)
|
|
||||||
{
|
|
||||||
_client.OnMessageReceived += handler;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -14,33 +14,14 @@
|
|||||||
<PackageReference Include="NAudio.Extras" Version="2.2.1" />
|
<PackageReference Include="NAudio.Extras" Version="2.2.1" />
|
||||||
<PackageReference Include="Serilog" Version="4.0.0" />
|
<PackageReference Include="Serilog" Version="4.0.0" />
|
||||||
<PackageReference Include="Serilog.AspNetCore" Version="8.0.2-dev-00338" />
|
<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.Formatting.Compact" Version="3.0.0" />
|
||||||
<PackageReference Include="Serilog.Settings.Configuration" Version="8.0.1" />
|
<PackageReference Include="Serilog.Settings.Configuration" Version="8.0.2" />
|
||||||
<PackageReference Include="Serilog.Sinks.Async" Version="2.0.0" />
|
|
||||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||||
<PackageReference Include="Serilog.Sinks.Debug" Version="3.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.File" Version="6.0.0" />
|
||||||
<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.4" />
|
<PackageReference Include="System.Text.Json" Version="8.0.4" />
|
||||||
<PackageReference Include="System.Threading.Tasks.Dataflow" Version="8.0.0" />
|
|
||||||
<PackageReference Include="TwitchLib.Api.Core" Version="3.10.0-preview-e47ba7f" />
|
|
||||||
<PackageReference Include="TwitchLib.Api.Core.Enums" Version="3.10.0-preview-e47ba7f" />
|
|
||||||
<PackageReference Include="TwitchLib.Api.Core.Interfaces" Version="3.10.0-preview-e47ba7f" />
|
|
||||||
<PackageReference Include="TwitchLib.Api.Helix" Version="3.10.0-preview-e47ba7f" />
|
|
||||||
<PackageReference Include="TwitchLib.Api.Helix.Models" Version="3.10.0-preview-e47ba7f" />
|
|
||||||
<PackageReference Include="TwitchLib.Client" Version="4.0.0-preview-fd131763416cb9f1a31705ca609566d7e7e7fac8" />
|
|
||||||
<PackageReference Include="TwitchLib.Client.Enums" Version="4.0.0-preview-fd131763416cb9f1a31705ca609566d7e7e7fac8" />
|
|
||||||
<PackageReference Include="TwitchLib.Client.Models" Version="4.0.0-preview-fd131763416cb9f1a31705ca609566d7e7e7fac8" />
|
|
||||||
<PackageReference Include="TwitchLib.Communication" Version="2.0.1" />
|
|
||||||
<PackageReference Include="TwitchLib.EventSub.Core" Version="2.5.3-preview-e1a92de" />
|
|
||||||
<PackageReference Include="TwitchLib.PubSub" Version="4.0.0-preview-f833b1ab1ebef37618dba3fbb1e0a661ff183af5" />
|
|
||||||
<PackageReference Include="NAudio.Core" Version="2.2.1" />
|
<PackageReference Include="NAudio.Core" Version="2.2.1" />
|
||||||
<PackageReference Include="TwitchLib.Api" Version="3.10.0-preview-e47ba7f" />
|
<PackageReference Include="YamlDotNet" Version="16.0.0" />
|
||||||
<PackageReference Include="YamlDotNet" Version="15.1.2" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
12
User.cs
12
User.cs
@ -16,16 +16,16 @@ namespace TwitchChatTTS
|
|||||||
|
|
||||||
public string DefaultTTSVoice { get; set; }
|
public string DefaultTTSVoice { get; set; }
|
||||||
// voice id -> voice name
|
// voice id -> voice name
|
||||||
public IDictionary<string, string> VoicesAvailable { get => _voicesAvailable; set { _voicesAvailable = value; WordFilterRegex = GenerateEnabledVoicesRegex(); } }
|
public IDictionary<string, string> VoicesAvailable { get => _voicesAvailable; set { _voicesAvailable = value; VoiceNameRegex = GenerateEnabledVoicesRegex(); } }
|
||||||
// chatter/twitch id -> voice id
|
// chatter/twitch id -> voice id
|
||||||
public IDictionary<long, string> VoicesSelected { get; set; }
|
public IDictionary<long, string> VoicesSelected { get; set; }
|
||||||
// voice names
|
// voice names
|
||||||
public HashSet<string> VoicesEnabled { get => _voicesEnabled; set { _voicesEnabled = value; WordFilterRegex = GenerateEnabledVoicesRegex(); } }
|
public HashSet<string> VoicesEnabled { get => _voicesEnabled; set { _voicesEnabled = value; VoiceNameRegex = GenerateEnabledVoicesRegex(); } }
|
||||||
|
|
||||||
public IDictionary<string, TTSUsernameFilter> ChatterFilters { get; set; }
|
public HashSet<long> Chatters { get; set; }
|
||||||
public IList<TTSWordFilter> RegexFilters { get; set; }
|
public TTSWordFilter[] RegexFilters { get; set; }
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public Regex? WordFilterRegex { get; set; }
|
public Regex? VoiceNameRegex { get; set; }
|
||||||
|
|
||||||
private IDictionary<string, string> _voicesAvailable;
|
private IDictionary<string, string> _voicesAvailable;
|
||||||
private HashSet<string> _voicesEnabled;
|
private HashSet<string> _voicesEnabled;
|
||||||
@ -37,7 +37,7 @@ namespace TwitchChatTTS
|
|||||||
return null;
|
return null;
|
||||||
|
|
||||||
var enabledVoicesString = string.Join("|", VoicesAvailable.Where(v => VoicesEnabled == null || !VoicesEnabled.Any() || VoicesEnabled.Contains(v.Value)).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);
|
return new Regex($@"\b({enabledVoicesString})\:(.*?)(?=\Z|\b(?:{enabledVoicesString})\:)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user