diff --git a/.gitignore b/.gitignore index e6eb292..f629320 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ -TwitchChatTTS/bin/ -TwitchChatTTS/obj/ -.redeems -.token -.twitchchannels \ No newline at end of file +appsettings.json +tts.config.yml +obj/ +bin/ diff --git a/TwitchChatTTS/Message/MessageHandler.cs b/Chat/ChatMessageHandler.cs similarity index 54% rename from TwitchChatTTS/Message/MessageHandler.cs rename to Chat/ChatMessageHandler.cs index 74d11b0..8034e44 100644 --- a/TwitchChatTTS/Message/MessageHandler.cs +++ b/Chat/ChatMessageHandler.cs @@ -1,25 +1,44 @@ using System.Text.RegularExpressions; using TwitchLib.Client.Events; -using TwitchChatTTS.Hermes; +using TwitchChatTTS.OBS.Socket; +using CommonSocketLibrary.Abstract; +using CommonSocketLibrary.Common; +using Microsoft.Extensions.Logging; +using TwitchChatTTS.Twitch; +using Microsoft.Extensions.DependencyInjection; +using TwitchChatTTS; +using TwitchChatTTS.Seven; public class ChatMessageHandler { + private ILogger Logger { get; } + private Configuration Configuration { get; } + public EmoteCounter EmoteCounter { get; } + private EmoteDatabase Emotes { get; } private TTSPlayer Player { get; } - public string DefaultVoice { get; set; } - public IEnumerable EnabledVoices { get; } - public Dictionary UsernameFilters { get; } - public IEnumerable WordFilters { get; } + private OBSSocketClient? Client { get; } + private TTSContext Context { get; } - private Regex voicesRegex; + private Regex? voicesRegex; private Regex sfxRegex; - public ChatMessageHandler(TTSPlayer player, string defaultVoice, IEnumerable enabledVoices, Dictionary usernameFilters, IEnumerable wordFilters) { + public ChatMessageHandler( + ILogger logger, + Configuration configuration, + EmoteCounter emoteCounter, + EmoteDatabase emotes, + TTSPlayer player, + [FromKeyedServices("obs")] SocketClient client, + TTSContext context + ) { + Logger = logger; + Configuration = configuration; + EmoteCounter = emoteCounter; + Emotes = emotes; Player = player; - DefaultVoice = defaultVoice; - EnabledVoices = enabledVoices; - UsernameFilters = usernameFilters; - WordFilters = wordFilters; + Client = client as OBSSocketClient; + Context = context; voicesRegex = GenerateEnabledVoicesRegex(); sfxRegex = new Regex(@"\(([A-Za-z0-9_-]+)\)"); @@ -27,23 +46,48 @@ public class ChatMessageHandler { public MessageResult Handle(OnMessageReceivedArgs e) { + if (Configuration.Twitch?.TtsWhenOffline != true && Client?.Live != true) + return MessageResult.Blocked; + var m = e.ChatMessage; var msg = e.ChatMessage.Message; - + // Skip TTS messages - if ((m.IsVip || m.IsModerator || m.IsBroadcaster) && (msg.ToLower().StartsWith("!skip ") || msg.ToLower() == "!skip")) { - return MessageResult.Skip; + if (m.IsVip || m.IsModerator || m.IsBroadcaster) { + if (msg.ToLower().StartsWith("!skip ") || msg.ToLower() == "!skip") + return MessageResult.Skip; + + if (msg.ToLower().StartsWith("!skipall ") || msg.ToLower() == "!skipall") + return MessageResult.SkipAll; } - if (UsernameFilters.TryGetValue(m.Username, out TTSUsernameFilter filter) && filter.tag == "blacklisted") { + if (Context.UsernameFilters.TryGetValue(m.Username, out TTSUsernameFilter? filter) && filter.Tag == "blacklisted") { + Logger.LogTrace($"Blocked message by {m.Username}: {msg}"); return MessageResult.Blocked; } - // Ensure we can send it via the web. - var alphanumeric = new Regex(@"[^a-zA-Z0-9!@#$%&\^*+\-_(),+':;?.,\[\]\s\\/~`]"); - msg = alphanumeric.Replace(msg, ""); + // Replace filtered words. + if (Context.WordFilters is not null) { + foreach (var wf in Context.WordFilters) { + if (wf.Search == null || wf.Replace == null) + continue; + + if (wf.IsRegex) { + try { + var regex = new Regex(wf.Search); + msg = regex.Replace(msg, wf.Replace); + continue; + } catch (Exception) { + wf.IsRegex = false; + } + } + + msg = msg.Replace(wf.Search, wf.Replace); + } + } // Filter highly repetitive words (like emotes) from the message. + var emotesUsed = new HashSet(); var words = msg.Split(" "); var wordCounter = new Dictionary(); string filteredMsg = string.Empty; @@ -54,30 +98,28 @@ public class ChatMessageHandler { wordCounter.Add(w, 1); } - if (wordCounter[w] < 5) { + var emoteId = Emotes?.Get(w); + if (emoteId != null) + emotesUsed.Add("7tv-" + emoteId); + + if (wordCounter[w] <= 4 && (emoteId == null || emotesUsed.Count <= 4)) filteredMsg += w + " "; - } } msg = filteredMsg; - foreach (var wf in WordFilters) { - if (wf.IsRegex) { - try { - var regex = new Regex(wf.search); - msg = regex.Replace(msg, wf.replace); - continue; - } catch (Exception ex) { - wf.IsRegex = false; - } - } - - msg = msg.Replace(wf.search, wf.replace); - } + // Adding twitch emotes to the counter. + foreach (var emote in e.ChatMessage.EmoteSet.Emotes) + emotesUsed.Add("twitch-" + emote.Id); + + if (long.TryParse(e.ChatMessage.UserId, out long userId)) + EmoteCounter.Add(userId, emotesUsed); + if (emotesUsed.Any()) + Logger.LogDebug("Emote counters for user #" + userId + ": " + string.Join(" | ", emotesUsed.Select(e => e + "=" + EmoteCounter.Get(userId, e)))); int priority = 0; if (m.IsStaff) { priority = int.MinValue; - } else if (filter?.tag == "priority") { + } else if (filter?.Tag == "priority") { priority = int.MinValue + 1; } else if (m.IsModerator) { priority = -100; @@ -88,12 +130,12 @@ public class ChatMessageHandler { } else if (m.IsHighlighted) { priority = -1; } - priority = (int) Math.Round(Math.Min(priority, -m.SubscribedMonthCount * (m.Badges.Any(b => b.Key == "subscriber" && b.Value == "1") ? 1.2 : 1))); + priority = (int) Math.Round(Math.Min(priority, -m.SubscribedMonthCount * (m.Badges.Any(b => b.Key == "subscriber") ? 1.2 : 1))); - var matches = voicesRegex.Matches(msg); + var matches = voicesRegex?.Matches(msg).ToArray() ?? new Match[0]; int defaultEnd = matches.FirstOrDefault()?.Index ?? msg.Length; if (defaultEnd > 0) { - HandlePartialMessage(priority, DefaultVoice, msg.Substring(0, defaultEnd).Trim(), e); + HandlePartialMessage(priority, Context.DefaultVoice, msg.Substring(0, defaultEnd).Trim(), e); } foreach (Match match in matches) { @@ -120,7 +162,7 @@ public class ChatMessageHandler { var badgesString = string.Join(", ", e.ChatMessage.Badges.Select(b => b.Key + " = " + b.Value)); if (parts.Length == 1) { - Console.WriteLine($"Voice: {voice}; Priority: {priority}; Message: {message}; Month: {m.SubscribedMonthCount}; {badgesString}"); + Logger.LogInformation($"Voice: {voice}; Priority: {priority}; Message: {message}; Month: {m.SubscribedMonthCount}; {badgesString}"); Player.Add(new TTSMessage() { Voice = voice, Message = message, @@ -147,7 +189,7 @@ public class ChatMessageHandler { } if (!string.IsNullOrWhiteSpace(parts[i * 2])) { - Console.WriteLine($"Voice: {voice}; Priority: {priority}; Message: {parts[i * 2]}; Month: {m.SubscribedMonthCount}; {badgesString}"); + Logger.LogInformation($"Voice: {voice}; Priority: {priority}; Message: {parts[i * 2]}; Month: {m.SubscribedMonthCount}; {badgesString}"); Player.Add(new TTSMessage() { Voice = voice, Message = parts[i * 2], @@ -160,7 +202,7 @@ public class ChatMessageHandler { }); } - Console.WriteLine($"Voice: {voice}; Priority: {priority}; SFX: {sfxName}; Month: {m.SubscribedMonthCount}; {badgesString}"); + Logger.LogInformation($"Voice: {voice}; Priority: {priority}; SFX: {sfxName}; Month: {m.SubscribedMonthCount}; {badgesString}"); Player.Add(new TTSMessage() { Voice = voice, Message = sfxName, @@ -175,7 +217,7 @@ public class ChatMessageHandler { } if (!string.IsNullOrWhiteSpace(parts.Last())) { - Console.WriteLine($"Voice: {voice}; Priority: {priority}; Message: {parts.Last()}; Month: {m.SubscribedMonthCount}; {badgesString}"); + Logger.LogInformation($"Voice: {voice}; Priority: {priority}; Message: {parts.Last()}; Month: {m.SubscribedMonthCount}; {badgesString}"); Player.Add(new TTSMessage() { Voice = voice, Message = parts.Last(), @@ -189,12 +231,12 @@ public class ChatMessageHandler { } } - private Regex GenerateEnabledVoicesRegex() { - if (EnabledVoices == null || EnabledVoices.Count() <= 0) { + private Regex? GenerateEnabledVoicesRegex() { + if (Context.EnabledVoices == null || Context.EnabledVoices.Count() <= 0) { return null; } - var enabledVoicesString = string.Join("|", EnabledVoices.Select(v => v.label)); + var enabledVoicesString = string.Join("|", Context.EnabledVoices.Select(v => v.Label)); return new Regex($@"\b({enabledVoicesString})\:(.*?)(?=\Z|\b(?:{enabledVoicesString})\:)", RegexOptions.IgnoreCase); } } \ No newline at end of file diff --git a/TwitchChatTTS/Message/MessageResult.cs b/Chat/MessageResult.cs similarity index 62% rename from TwitchChatTTS/Message/MessageResult.cs rename to Chat/MessageResult.cs index 5103a4b..bd392b9 100644 --- a/TwitchChatTTS/Message/MessageResult.cs +++ b/Chat/MessageResult.cs @@ -1,5 +1,6 @@ public enum MessageResult { Skip = 1, - Blocked = 2, + SkipAll = 2, + Blocked = 3, None = 0 } \ No newline at end of file diff --git a/TwitchChatTTS/Speech/AudioPlaybackEngine.cs b/Chat/Speech/AudioPlaybackEngine.cs similarity index 95% rename from TwitchChatTTS/Speech/AudioPlaybackEngine.cs rename to Chat/Speech/AudioPlaybackEngine.cs index de68930..0920178 100644 --- a/TwitchChatTTS/Speech/AudioPlaybackEngine.cs +++ b/Chat/Speech/AudioPlaybackEngine.cs @@ -22,8 +22,11 @@ public class AudioPlaybackEngine : IDisposable outputDevice.Play(); } - private ISampleProvider ConvertToRightChannelCount(ISampleProvider input) + private ISampleProvider ConvertToRightChannelCount(ISampleProvider? input) { + if (input is null) + throw new NullReferenceException(nameof(input)); + if (input.WaveFormat.Channels == mixer.WaveFormat.Channels) { return input; @@ -47,7 +50,7 @@ public class AudioPlaybackEngine : IDisposable } public ISampleProvider ConvertSound(IWaveProvider provider) { - ISampleProvider converted = null; + ISampleProvider? converted = null; if (provider.WaveFormat.Encoding == WaveFormatEncoding.Pcm) { if (provider.WaveFormat.BitsPerSample == 8) { converted = new Pcm8BitToSampleProvider(provider); diff --git a/TwitchChatTTS/Speech/NetworkCachedSound.cs b/Chat/Speech/NetworkCachedSound.cs similarity index 60% rename from TwitchChatTTS/Speech/NetworkCachedSound.cs rename to Chat/Speech/NetworkCachedSound.cs index 878cbc7..0af2b8e 100644 --- a/TwitchChatTTS/Speech/NetworkCachedSound.cs +++ b/Chat/Speech/NetworkCachedSound.cs @@ -1,5 +1,4 @@ using NAudio.Wave; -using System; public class NetworkWavSound { @@ -10,7 +9,6 @@ public class NetworkWavSound { using (var mfr = new MediaFoundationReader(uri)) { WaveFormat = mfr.WaveFormat; - //Console.WriteLine("W: " + WaveFormat.SampleRate + " C: " + WaveFormat.Channels + " B: " + WaveFormat.BitsPerSample + " E: " + WaveFormat.Encoding); byte[] buffer = new byte[4096]; int read = 0; @@ -25,21 +23,20 @@ public class NetworkWavSound public class CachedWavProvider : IWaveProvider { - private readonly NetworkWavSound sound; - private long position; - private readonly RawSourceWaveStream stream; + private readonly NetworkWavSound _sound; + private readonly RawSourceWaveStream _stream; - public WaveFormat WaveFormat { get => sound.WaveFormat; } + public WaveFormat WaveFormat { get => _sound.WaveFormat; } public CachedWavProvider(NetworkWavSound cachedSound) { - sound = cachedSound; - stream = new RawSourceWaveStream(new MemoryStream(sound.AudioData), sound.WaveFormat); + _sound = cachedSound; + _stream = new RawSourceWaveStream(new MemoryStream(_sound.AudioData), _sound.WaveFormat); } public int Read(byte[] buffer, int offset, int count) { - return stream.Read(buffer, offset, count); + return _stream.Read(buffer, offset, count); } } \ No newline at end of file diff --git a/TwitchChatTTS/Speech/TTSPlayer.cs b/Chat/Speech/TTSPlayer.cs similarity index 63% rename from TwitchChatTTS/Speech/TTSPlayer.cs rename to Chat/Speech/TTSPlayer.cs index 7f9e5c4..293c4f3 100644 --- a/TwitchChatTTS/Speech/TTSPlayer.cs +++ b/Chat/Speech/TTSPlayer.cs @@ -22,10 +22,10 @@ public class TTSPlayer { } } - public TTSMessage ReceiveReady() { + public TTSMessage? ReceiveReady() { try { _mutex.WaitOne(); - if (_messages.TryDequeue(out TTSMessage message, out int _)) { + if (_messages.TryDequeue(out TTSMessage? message, out int _)) { return message; } return null; @@ -34,10 +34,10 @@ public class TTSPlayer { } } - public TTSMessage ReceiveBuffer() { + public TTSMessage? ReceiveBuffer() { try { _mutex2.WaitOne(); - if (_buffer.TryDequeue(out TTSMessage message, out int _)) { + if (_buffer.TryDequeue(out TTSMessage? message, out int _)) { return message; } return null; @@ -55,22 +55,38 @@ public class TTSPlayer { } } + public void RemoveAll() { + try { + _mutex2.WaitOne(); + _buffer.Clear(); + } finally { + _mutex2.ReleaseMutex(); + } + + try { + _mutex.WaitOne(); + _messages.Clear(); + } finally { + _mutex.ReleaseMutex(); + } + } + public bool IsEmpty() { return _messages.Count == 0; } } public class TTSMessage { - public string Voice { get; set; } - public string Channel { get; set; } - public string Username { get; set; } - public string Message { get; set; } - public string File { get; set; } + public string? Voice { get; set; } + public string? Channel { get; set; } + public string? Username { get; set; } + public string? Message { get; set; } + public string? File { get; set; } public DateTime Timestamp { get; set; } public bool Moderator { get; set; } public bool Bot { get; set; } - public IEnumerable> Badges { get; set; } + public IEnumerable>? Badges { get; set; } public int Bits { get; set; } public int Priority { get; set; } - public ISampleProvider Audio { get; set; } + public ISampleProvider? Audio { get; set; } } \ No newline at end of file diff --git a/Configuration.cs b/Configuration.cs new file mode 100644 index 0000000..ebc9b9f --- /dev/null +++ b/Configuration.cs @@ -0,0 +1,48 @@ +using TwitchChatTTS.Seven.Socket.Context; + +namespace TwitchChatTTS +{ + public class Configuration + { + public HermesConfiguration? Hermes; + public TwitchConfiguration? Twitch; + public EmotesConfiguration? Emotes; + public OBSConfiguration? Obs; + public SevenConfiguration? Seven; + + + public class HermesConfiguration { + public string? Token; + } + + public class TwitchConfiguration { + public IEnumerable? Channels; + public IDictionary? Redeems; + public bool? TtsWhenOffline; + } + + public class RedeemConfiguration { + public string? AudioFilePath; + public string? OutputFilePath; + public string? OutputContent; + public bool? OutputAppend; + } + + public class EmotesConfiguration { + public string? CounterFilePath; + } + + public class OBSConfiguration { + public string? Host; + public short? Port; + public string? Password; + } + + public class SevenConfiguration { + public string? Protocol; + public string? Url; + + public IEnumerable? InitialSubscriptions; + } + } +} \ No newline at end of file diff --git a/Helpers/WebClientWrap.cs b/Helpers/WebClientWrap.cs new file mode 100644 index 0000000..3cbf114 --- /dev/null +++ b/Helpers/WebClientWrap.cs @@ -0,0 +1,39 @@ +using System.Net.Http.Json; +using System.Text.Json; + +namespace TwitchChatTTS.Helpers { + public class WebClientWrap { + private HttpClient _client; + private JsonSerializerOptions _options; + + + public WebClientWrap(JsonSerializerOptions options) { + _client = new HttpClient(); + _options = options; + } + + + public void AddHeader(string key, string? value) { + if (_client.DefaultRequestHeaders.Contains(key)) + _client.DefaultRequestHeaders.Remove(key); + _client.DefaultRequestHeaders.Add(key, value); + } + + public async Task GetJson(string uri) { + var response = await _client.GetAsync(uri); + return JsonSerializer.Deserialize(await response.Content.ReadAsStreamAsync(), _options); + } + + public async Task Get(string uri) { + return await _client.GetAsync(uri); + } + + public async Task Post(string uri, T data) { + return await _client.PostAsJsonAsync(uri, data); + } + + public async Task Post(string uri) { + return await _client.PostAsJsonAsync(uri, new object()); + } + } +} \ No newline at end of file diff --git a/TwitchChatTTS/Hermes/Account.cs b/Hermes/Account.cs similarity index 59% rename from TwitchChatTTS/Hermes/Account.cs rename to Hermes/Account.cs index 87e3fcc..6b97727 100644 --- a/TwitchChatTTS/Hermes/Account.cs +++ b/Hermes/Account.cs @@ -3,7 +3,7 @@ using System.Diagnostics.CodeAnalysis; [Serializable] public class Account { [AllowNull] - public string id { get; set; } + public string Id { get; set; } [AllowNull] - public string username { get; set; } + public string Username { get; set; } } \ No newline at end of file diff --git a/TwitchChatTTS/Hermes/HermesClient.cs b/Hermes/HermesClient.cs similarity index 69% rename from TwitchChatTTS/Hermes/HermesClient.cs rename to Hermes/HermesClient.cs index a6761ab..72a40bb 100644 --- a/TwitchChatTTS/Hermes/HermesClient.cs +++ b/Hermes/HermesClient.cs @@ -1,34 +1,36 @@ -using System; +using TwitchChatTTS.Helpers; +using TwitchChatTTS; using TwitchChatTTS.Hermes; +using System.Text.Json; public class HermesClient { - private Account account; - private string key; - private WebHelper _web; + private Account? account; + private WebClientWrap _web; + private Configuration Configuration { get; } - public string Id { get => account?.id; } - public string Username { get => account?.username; } + public string? Id { get => account?.Id; } + public string? Username { get => account?.Username; } - public HermesClient() { - // Read API Key from file. - if (!File.Exists(".token")) { + public HermesClient(Configuration configuration) { + Configuration = configuration; + + if (string.IsNullOrWhiteSpace(Configuration.Hermes?.Token)) { throw new Exception("Ensure you have written your API key in \".token\" file, in the same folder as this application."); } - key = File.ReadAllText(".token")?.Trim(); - _web = new WebHelper(); - _web.AddHeader("x-api-key", key); + _web = new WebClientWrap(new JsonSerializerOptions() { + PropertyNameCaseInsensitive = false, + PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower + }); + _web.AddHeader("x-api-key", Configuration.Hermes.Token); } - public async Task UpdateHermesAccount() { - ValidateKey(); + public async Task FetchHermesAccountDetails() { account = await _web.GetJson("https://hermes.goblincaves.com/api/account"); } public async Task FetchTwitchBotToken() { - ValidateKey(); - var token = await _web.GetJson("https://hermes.goblincaves.com/api/token/bot"); if (token == null) { throw new Exception("Failed to fetch Twitch API token from Hermes."); @@ -38,8 +40,6 @@ public class HermesClient { } public async Task> FetchTTSUsernameFilters() { - ValidateKey(); - var filters = await _web.GetJson>("https://hermes.goblincaves.com/api/settings/tts/filter/users"); if (filters == null) { throw new Exception("Failed to fetch TTS username filters from Hermes."); @@ -48,20 +48,16 @@ public class HermesClient { return filters; } - public async Task FetchTTSDefaultVoice() { - ValidateKey(); - + public async Task FetchTTSDefaultVoice() { var data = await _web.GetJson("https://hermes.goblincaves.com/api/settings/tts/default"); if (data == null) { throw new Exception("Failed to fetch TTS default voice from Hermes."); } - return data.label; + return data.Label; } public async Task> FetchTTSEnabledVoices() { - ValidateKey(); - var voices = await _web.GetJson>("https://hermes.goblincaves.com/api/settings/tts"); if (voices == null) { throw new Exception("Failed to fetch TTS enabled voices from Hermes."); @@ -71,8 +67,6 @@ public class HermesClient { } public async Task> FetchTTSWordFilters() { - ValidateKey(); - var filters = await _web.GetJson>("https://hermes.goblincaves.com/api/settings/tts/filter/words"); if (filters == null) { throw new Exception("Failed to fetch TTS word filters from Hermes."); @@ -80,10 +74,4 @@ public class HermesClient { return filters; } - - private void ValidateKey() { - if (string.IsNullOrWhiteSpace(key)) { - throw new InvalidOperationException("Hermes API key not provided."); - } - } } \ No newline at end of file diff --git a/Hermes/TTSUsernameFilter.cs b/Hermes/TTSUsernameFilter.cs new file mode 100644 index 0000000..bf2030e --- /dev/null +++ b/Hermes/TTSUsernameFilter.cs @@ -0,0 +1,5 @@ +public class TTSUsernameFilter { + public string? Username { get; set; } + public string? Tag { get; set; } + public string? UserId { get; set; } +} \ No newline at end of file diff --git a/Hermes/TTSVoice.cs b/Hermes/TTSVoice.cs new file mode 100644 index 0000000..286f145 --- /dev/null +++ b/Hermes/TTSVoice.cs @@ -0,0 +1,6 @@ +public class TTSVoice { + public string? Label { get; set; } + public int Value { get; set; } + public string? Gender { get; set; } + public string? Language { get; set; } +} \ No newline at end of file diff --git a/TwitchChatTTS/Hermes/TTSWordFilter.cs b/Hermes/TTSWordFilter.cs similarity index 62% rename from TwitchChatTTS/Hermes/TTSWordFilter.cs rename to Hermes/TTSWordFilter.cs index 327b982..68faecc 100644 --- a/TwitchChatTTS/Hermes/TTSWordFilter.cs +++ b/Hermes/TTSWordFilter.cs @@ -7,10 +7,10 @@ namespace TwitchChatTTS.Hermes { public class TTSWordFilter { - public string id { get; set; } - public string search { get; set; } - public string replace { get; set; } - public string userId { get; set; } + public string? Id { get; set; } + public string? Search { get; set; } + public string? Replace { get; set; } + public string? UserId { get; set; } public bool IsRegex { get; set; } diff --git a/Hermes/TwitchBotAuth.cs b/Hermes/TwitchBotAuth.cs new file mode 100644 index 0000000..e0ed6ef --- /dev/null +++ b/Hermes/TwitchBotAuth.cs @@ -0,0 +1,7 @@ +[Serializable] +public class TwitchBotAuth { + public string? UserId { get; set; } + public string? AccessToken { get; set; } + public string? RefreshToken { get; set; } + public string? BroadcasterId { get; set; } +} \ No newline at end of file diff --git a/Hermes/TwitchBotToken.cs b/Hermes/TwitchBotToken.cs new file mode 100644 index 0000000..dff611c --- /dev/null +++ b/Hermes/TwitchBotToken.cs @@ -0,0 +1,8 @@ +[Serializable] +public class TwitchBotToken { + public string? ClientId { get; set; } + public string? ClientSecret { get; set; } + public string? AccessToken { get; set; } + public string? RefreshToken { get; set; } + public string? BroadcasterId { get; set; } +} \ No newline at end of file diff --git a/Hermes/TwitchConnection.cs b/Hermes/TwitchConnection.cs new file mode 100644 index 0000000..7c179ab --- /dev/null +++ b/Hermes/TwitchConnection.cs @@ -0,0 +1,8 @@ +[Serializable] +public class TwitchConnection { + public string? Id { get; set; } + public string? Secret { get; set; } + public string? BroadcasterId { get; set; } + public string? Username { get; set; } + public string? UserId { get; set; } +} \ No newline at end of file diff --git a/OBS/Socket/Context/HelloContext.cs b/OBS/Socket/Context/HelloContext.cs new file mode 100644 index 0000000..194f0cf --- /dev/null +++ b/OBS/Socket/Context/HelloContext.cs @@ -0,0 +1,10 @@ +namespace TwitchChatTTS.OBS.Socket.Context +{ + [Serializable] + public class HelloContext + { + public string? Host { get; set; } + public short? Port { get; set; } + public string? Password { get; set; } + } +} \ No newline at end of file diff --git a/OBS/Socket/Data/EventMessage.cs b/OBS/Socket/Data/EventMessage.cs new file mode 100644 index 0000000..2f34557 --- /dev/null +++ b/OBS/Socket/Data/EventMessage.cs @@ -0,0 +1,10 @@ +namespace TwitchChatTTS.OBS.Socket.Data +{ + [Serializable] + public class EventMessage + { + public string eventType { get; set; } + public int eventIntent { get; set; } + public Dictionary eventData { get; set; } + } +} \ No newline at end of file diff --git a/OBS/Socket/Data/HelloMessage.cs b/OBS/Socket/Data/HelloMessage.cs new file mode 100644 index 0000000..f576c28 --- /dev/null +++ b/OBS/Socket/Data/HelloMessage.cs @@ -0,0 +1,15 @@ +namespace TwitchChatTTS.OBS.Socket.Data +{ + [Serializable] + public class HelloMessage + { + public string obsWebSocketVersion { get; set; } + public int rpcVersion { get; set; } + public AuthenticationMessage authentication { get; set; } + } + + public class AuthenticationMessage { + public string challenge { get; set; } + public string salt { get; set; } + } +} \ No newline at end of file diff --git a/OBS/Socket/Data/IdentifiedMessage.cs b/OBS/Socket/Data/IdentifiedMessage.cs new file mode 100644 index 0000000..eb07559 --- /dev/null +++ b/OBS/Socket/Data/IdentifiedMessage.cs @@ -0,0 +1,8 @@ +namespace TwitchChatTTS.OBS.Socket.Data +{ + [Serializable] + public class IdentifiedMessage + { + public int negotiatedRpcVersion { get; set; } + } +} \ No newline at end of file diff --git a/OBS/Socket/Data/IdentifyMessage.cs b/OBS/Socket/Data/IdentifyMessage.cs new file mode 100644 index 0000000..f3b1594 --- /dev/null +++ b/OBS/Socket/Data/IdentifyMessage.cs @@ -0,0 +1,16 @@ +namespace TwitchChatTTS.OBS.Socket.Data +{ + [Serializable] + public class IdentifyMessage + { + public int rpcVersion { get; set; } + public string? authentication { get; set; } + public int eventSubscriptions { get; set; } + + public IdentifyMessage(int version, string auth, int subscriptions) { + rpcVersion = version; + authentication = auth; + eventSubscriptions = subscriptions; + } + } +} \ No newline at end of file diff --git a/OBS/Socket/Data/RequestMessage.cs b/OBS/Socket/Data/RequestMessage.cs new file mode 100644 index 0000000..5212c12 --- /dev/null +++ b/OBS/Socket/Data/RequestMessage.cs @@ -0,0 +1,16 @@ +namespace TwitchChatTTS.OBS.Socket.Data +{ + [Serializable] + public class RequestMessage + { + public string requestType { get; set; } + public string requestId { get; set; } + public Dictionary requestData { get; set; } + + public RequestMessage(string type, string id, Dictionary data) { + requestType = type; + requestId = id; + requestData = data; + } + } +} \ No newline at end of file diff --git a/OBS/Socket/Data/RequestResponseMessage.cs b/OBS/Socket/Data/RequestResponseMessage.cs new file mode 100644 index 0000000..657e1c6 --- /dev/null +++ b/OBS/Socket/Data/RequestResponseMessage.cs @@ -0,0 +1,11 @@ +namespace TwitchChatTTS.OBS.Socket.Data +{ + [Serializable] + public class RequestResponseMessage + { + public string requestType { get; set; } + public string requestId { get; set; } + public object requestStatus { get; set; } + public Dictionary responseData { get; set; } + } +} \ No newline at end of file diff --git a/OBS/Socket/Handlers/EventMessageHandler.cs b/OBS/Socket/Handlers/EventMessageHandler.cs new file mode 100644 index 0000000..a8d08d4 --- /dev/null +++ b/OBS/Socket/Handlers/EventMessageHandler.cs @@ -0,0 +1,48 @@ +using CommonSocketLibrary.Abstract; +using CommonSocketLibrary.Common; +using Microsoft.Extensions.Logging; +using TwitchChatTTS.OBS.Socket.Data; + +namespace TwitchChatTTS.OBS.Socket.Handlers +{ + public class EventMessageHandler : IWebSocketHandler + { + private ILogger Logger { get; } + private IServiceProvider ServiceProvider { get; } + public int OperationCode { get; set; } = 5; + + public EventMessageHandler(ILogger logger, IServiceProvider serviceProvider) { + Logger = logger; + ServiceProvider = serviceProvider; + } + + public async Task Execute(SocketClient sender, Data message) + { + if (message is not EventMessage obj || obj == null) + return; + + switch (obj.eventType) { + case "StreamStateChanged": + case "RecordStateChanged": + if (sender is not OBSSocketClient client) + return; + + string? raw_state = obj.eventData["outputState"].ToString(); + string? state = raw_state?.Substring(21).ToLower(); + client.Live = obj.eventData["outputActive"].ToString() == "True"; + Logger.LogWarning("Stream " + (state != null && state.EndsWith("ing") ? "is " : "has ") + state + "."); + + if (client.Live == false && state != null && !state.EndsWith("ing")) { + OnStreamEnd(); + } + break; + default: + Logger.LogDebug(obj.eventType + " EVENT: " + string.Join(" | ", obj.eventData?.Select(x => x.Key + "=" + x.Value?.ToString()) ?? new string[0])); + break; + } + } + + private void OnStreamEnd() { + } + } +} \ No newline at end of file diff --git a/OBS/Socket/Handlers/HelloHandler.cs b/OBS/Socket/Handlers/HelloHandler.cs new file mode 100644 index 0000000..81a0645 --- /dev/null +++ b/OBS/Socket/Handlers/HelloHandler.cs @@ -0,0 +1,55 @@ +using System.Security.Cryptography; +using System.Text; +using CommonSocketLibrary.Abstract; +using CommonSocketLibrary.Common; +using Microsoft.Extensions.Logging; +using TwitchChatTTS.OBS.Socket.Data; +using TwitchChatTTS.OBS.Socket.Context; + +namespace TwitchChatTTS.OBS.Socket.Handlers +{ + public class HelloHandler : IWebSocketHandler + { + private ILogger Logger { get; } + public int OperationCode { get; set; } = 0; + private HelloContext Context { get; } + + public HelloHandler(ILogger logger, HelloContext context) { + Logger = logger; + Context = context; + } + + public async Task Execute(SocketClient sender, Data message) + { + if (message is not HelloMessage obj || obj == null) + return; + + Logger.LogTrace("OBS websocket password: " + Context.Password); + if (obj.authentication is null || Context.Password is null) // TODO: send re-identify message. + return; + + var salt = obj.authentication.salt; + var challenge = obj.authentication.challenge; + Logger.LogTrace("Salt: " + salt); + Logger.LogTrace("Challenge: " + challenge); + + + string secret = Context.Password + salt; + byte[] bytes = Encoding.UTF8.GetBytes(secret); + string hash = null; + using (var sha = SHA256.Create()) { + bytes = sha.ComputeHash(bytes); + hash = Convert.ToBase64String(bytes); + + secret = hash + challenge; + bytes = Encoding.UTF8.GetBytes(secret); + bytes = sha.ComputeHash(bytes); + hash = Convert.ToBase64String(bytes); + } + + Logger.LogTrace("Final hash: " + hash); + //await sender.Send(1, new IdentifyMessage(obj.rpcVersion, hash, 1023 | 262144 | 524288)); + await sender.Send(1, new IdentifyMessage(obj.rpcVersion, hash, 1023 | 262144)); + } + } +} \ No newline at end of file diff --git a/OBS/Socket/Handlers/IdentifiedHandler.cs b/OBS/Socket/Handlers/IdentifiedHandler.cs new file mode 100644 index 0000000..a7f6ae1 --- /dev/null +++ b/OBS/Socket/Handlers/IdentifiedHandler.cs @@ -0,0 +1,26 @@ +using CommonSocketLibrary.Abstract; +using CommonSocketLibrary.Common; +using Microsoft.Extensions.Logging; +using TwitchChatTTS.OBS.Socket.Data; + +namespace TwitchChatTTS.OBS.Socket.Handlers +{ + public class IdentifiedHandler : IWebSocketHandler + { + private ILogger Logger { get; } + public int OperationCode { get; set; } = 2; + + public IdentifiedHandler(ILogger logger) { + Logger = logger; + } + + public async Task Execute(SocketClient sender, Data message) + { + if (message is not IdentifiedMessage obj || obj == null) + return; + + sender.Connected = true; + Logger.LogInformation("Connected to OBS via rpc version " + obj.negotiatedRpcVersion + "."); + } + } +} \ No newline at end of file diff --git a/OBS/Socket/Handlers/RequestResponseHandler.cs b/OBS/Socket/Handlers/RequestResponseHandler.cs new file mode 100644 index 0000000..bcc1d95 --- /dev/null +++ b/OBS/Socket/Handlers/RequestResponseHandler.cs @@ -0,0 +1,35 @@ +using CommonSocketLibrary.Abstract; +using CommonSocketLibrary.Common; +using Microsoft.Extensions.Logging; +using TwitchChatTTS.OBS.Socket.Data; + +namespace TwitchChatTTS.OBS.Socket.Handlers +{ + public class RequestResponseHandler : IWebSocketHandler + { + private ILogger Logger { get; } + public int OperationCode { get; set; } = 7; + + public RequestResponseHandler(ILogger logger) { + Logger = logger; + } + + public async Task Execute(SocketClient sender, Data message) + { + if (message is not RequestResponseMessage obj || obj == null) + return; + + switch (obj.requestType) { + case "GetOutputStatus": + if (sender is not OBSSocketClient client) + return; + + if (obj.requestId == "stream") { + client.Live = obj.responseData["outputActive"].ToString() == "True"; + Logger.LogWarning("Updated stream's live status to " + client.Live); + } + break; + } + } + } +} \ No newline at end of file diff --git a/OBS/Socket/Manager/OBSHandlerManager.cs b/OBS/Socket/Manager/OBSHandlerManager.cs new file mode 100644 index 0000000..125ede9 --- /dev/null +++ b/OBS/Socket/Manager/OBSHandlerManager.cs @@ -0,0 +1,31 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.DependencyInjection; +using CommonSocketLibrary.Socket.Manager; +using CommonSocketLibrary.Common; + +namespace TwitchChatTTS.OBS.Socket.Manager +{ + public class OBSHandlerManager : WebSocketHandlerManager + { + public OBSHandlerManager(ILogger logger, IServiceProvider provider) : base(logger) { + var basetype = typeof(IWebSocketHandler); + var assembly = GetType().Assembly; + var types = assembly.GetTypes().Where(t => t.IsClass && basetype.IsAssignableFrom(t) && t.AssemblyQualifiedName?.Contains(".OBS.") == true); + + foreach (var type in types) { + var key = "obs-" + type.Name.Replace("Handlers", "Hand#lers") + .Replace("Handler", "") + .Replace("Hand#lers", "Handlers") + .ToLower(); + var handler = provider.GetKeyedService(key); + if (handler == null) { + logger.LogError("Failed to find obs websocket handler: " + type.AssemblyQualifiedName); + continue; + } + + Logger.LogDebug($"Linked type {type.AssemblyQualifiedName} to obs websocket handler {handler.GetType().AssemblyQualifiedName}."); + Add(handler); + } + } + } +} \ No newline at end of file diff --git a/OBS/Socket/Manager/OBSHandlerTypeManager.cs b/OBS/Socket/Manager/OBSHandlerTypeManager.cs new file mode 100644 index 0000000..60d1c15 --- /dev/null +++ b/OBS/Socket/Manager/OBSHandlerTypeManager.cs @@ -0,0 +1,19 @@ +using CommonSocketLibrary.Abstract; +using CommonSocketLibrary.Common; +using CommonSocketLibrary.Socket.Manager; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace TwitchChatTTS.OBS.Socket.Manager +{ + public class OBSHandlerTypeManager : WebSocketHandlerTypeManager + { + public OBSHandlerTypeManager( + ILogger factory, + [FromKeyedServices("obs")] HandlerManager handlers + ) : base(factory, handlers) + { + } + } +} \ No newline at end of file diff --git a/OBS/Socket/OBSSocketClient.cs b/OBS/Socket/OBSSocketClient.cs new file mode 100644 index 0000000..dddf36b --- /dev/null +++ b/OBS/Socket/OBSSocketClient.cs @@ -0,0 +1,31 @@ +using TwitchChatTTS.OBS.Socket.Manager; +using CommonSocketLibrary.Common; +using CommonSocketLibrary.Abstract; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using System.Text.Json; + +namespace TwitchChatTTS.OBS.Socket +{ + public class OBSSocketClient : WebSocketClient { + private bool _live; + public bool? Live { + get => Connected ? _live : null; + set { + if (value.HasValue) + _live = value.Value; + } + } + + public OBSSocketClient( + ILogger logger, + [FromKeyedServices("obs")] HandlerManager handlerManager, + [FromKeyedServices("obs")] HandlerTypeManager typeManager + ) : base(logger, handlerManager, typeManager, new JsonSerializerOptions() { + PropertyNameCaseInsensitive = false, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }) { + _live = false; + } + } +} \ No newline at end of file diff --git a/Projects.sln b/Projects.sln deleted file mode 100644 index c1911c6..0000000 --- a/Projects.sln +++ /dev/null @@ -1,22 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.31903.59 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TwitchChatTTS", "TwitchChatTTS\TwitchChatTTS.csproj", "{7A371F54-F9D5-49C9-BE2D-819C60A0D621}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {7A371F54-F9D5-49C9-BE2D-819C60A0D621}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7A371F54-F9D5-49C9-BE2D-819C60A0D621}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7A371F54-F9D5-49C9-BE2D-819C60A0D621}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7A371F54-F9D5-49C9-BE2D-819C60A0D621}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection -EndGlobal diff --git a/Seven/Emotes.cs b/Seven/Emotes.cs new file mode 100644 index 0000000..458ed54 --- /dev/null +++ b/Seven/Emotes.cs @@ -0,0 +1,84 @@ +using System.Collections.Concurrent; + +namespace TwitchChatTTS.Seven +{ + public class EmoteCounter { + public IDictionary> Counters { get; set; } + + public EmoteCounter() { + Counters = new ConcurrentDictionary>(); + } + + public void Add(long userId, IEnumerable emoteIds) { + foreach (var emote in emoteIds) { + if (Counters.TryGetValue(emote, out IDictionary? subcounters)) { + if (subcounters.TryGetValue(userId, out int counter)) + subcounters[userId] = counter + 1; + else + subcounters.Add(userId, 1); + } else { + Counters.Add(emote, new ConcurrentDictionary()); + Counters[emote].Add(userId, 1); + } + } + } + + public void Clear() { + Counters.Clear(); + } + + public int Get(long userId, string emoteId) { + if (Counters.TryGetValue(emoteId, out IDictionary? subcounters)) { + if (subcounters.TryGetValue(userId, out int counter)) + return counter; + } + return -1; + } + } + + public class EmoteDatabase { + private IDictionary Emotes { get; } + + public EmoteDatabase() { + Emotes = new Dictionary(); + } + + public void Add(string emoteName, string emoteId) { + if (Emotes.ContainsKey(emoteName)) + Emotes[emoteName] = emoteId; + else + Emotes.Add(emoteName, emoteId); + } + + public void Clear() { + Emotes.Clear(); + } + + public string? Get(string emoteName) { + return Emotes.TryGetValue(emoteName, out string? emoteId) ? emoteId : null; + } + + public void Remove(string emoteName) { + if (Emotes.ContainsKey(emoteName)) + Emotes.Remove(emoteName); + } + } + + public class EmoteSet { + public string Id { get; set; } + public string Name { get; set; } + public int Flags { get; set; } + public bool Immutable { get; set; } + public bool Privileged { get; set; } + public IList Emotes { get; set; } + public int EmoteCount { get; set; } + public int Capacity { get; set; } + + } + + public class Emote { + public string Id { get; set; } + public string Name { get; set; } + public int Flags { get; set; } + } +} \ No newline at end of file diff --git a/Seven/SevenApiClient.cs b/Seven/SevenApiClient.cs new file mode 100644 index 0000000..4487c20 --- /dev/null +++ b/Seven/SevenApiClient.cs @@ -0,0 +1,47 @@ +using System.Text.Json; +using TwitchChatTTS.Helpers; +using Microsoft.Extensions.Logging; +using TwitchChatTTS; +using TwitchChatTTS.Seven; + +public class SevenApiClient { + private WebClientWrap Web { get; } + private Configuration Configuration { get; } + private ILogger Logger { get; } + private long? Id { get; } + + + public SevenApiClient(Configuration configuration, ILogger logger, TwitchBotToken token) { + Configuration = configuration; + Logger = logger; + Id = long.TryParse(token?.BroadcasterId, out long id) ? id : -1; + + Web = new WebClientWrap(new JsonSerializerOptions() { + PropertyNameCaseInsensitive = false, + PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower + }); + } + + public async Task GetSevenEmotes() { + if (Id is null) + throw new NullReferenceException(nameof(Id)); + + try { + var details = await Web.GetJson("https://7tv.io/v3/users/twitch/" + Id); + if (details is null) + return null; + + var emotes = new EmoteDatabase(); + if (details.EmoteSet is not null) + foreach (var emote in details.EmoteSet.Emotes) + emotes.Add(emote.Name, emote.Id); + Logger.LogInformation($"Loaded {details.EmoteSet?.Emotes.Count() ?? 0} emotes from 7tv."); + return emotes; + } catch (JsonException e) { + Logger.LogError(e, "Failed to fetch emotes from 7tv. 2"); + } catch (Exception e) { + Logger.LogError(e, "Failed to fetch emotes from 7tv."); + } + return null; + } +} \ No newline at end of file diff --git a/Seven/Socket/Context/ReconnectContext.cs b/Seven/Socket/Context/ReconnectContext.cs new file mode 100644 index 0000000..f1e6840 --- /dev/null +++ b/Seven/Socket/Context/ReconnectContext.cs @@ -0,0 +1,9 @@ +namespace TwitchChatTTS.Seven.Socket.Context +{ + public class ReconnectContext + { + public string? Protocol; + public string Url; + public string? SessionId; + } +} \ No newline at end of file diff --git a/Seven/Socket/Context/SevenHelloContext.cs b/Seven/Socket/Context/SevenHelloContext.cs new file mode 100644 index 0000000..ee0bc9a --- /dev/null +++ b/Seven/Socket/Context/SevenHelloContext.cs @@ -0,0 +1,12 @@ +namespace TwitchChatTTS.Seven.Socket.Context +{ + public class SevenHelloContext + { + public IEnumerable? Subscriptions; + } + + public class SevenSubscriptionConfiguration { + public string? Type; + public IDictionary? Condition; + } +} \ No newline at end of file diff --git a/Seven/Socket/Data/ChangeMapMessage.cs b/Seven/Socket/Data/ChangeMapMessage.cs new file mode 100644 index 0000000..f329fe4 --- /dev/null +++ b/Seven/Socket/Data/ChangeMapMessage.cs @@ -0,0 +1,30 @@ +namespace TwitchChatTTS.Seven.Socket.Data +{ + public class ChangeMapMessage + { + public object Id { get; set; } + public byte Kind { get; set; } + public bool? Contextual { get; set; } + public object Actor { get; set; } + public IEnumerable? Added { get; set; } + public IEnumerable? Updated { get; set; } + public IEnumerable? Removed { get; set; } + public IEnumerable? Pushed { get; set; } + public IEnumerable? Pulled { get; set; } + } + + public class ChangeField { + public string Key { get; set; } + public int? Index { get; set; } + public bool Nested { get; set; } + public object OldValue { get; set; } + public object Value { get; set; } + } + + public class EmoteField { + public string Id { get; set; } + public string Name { get; set; } + public string ActorId { get; set; } + public int Flags { get; set; } + } +} \ No newline at end of file diff --git a/Seven/Socket/Data/DispatchMessage.cs b/Seven/Socket/Data/DispatchMessage.cs new file mode 100644 index 0000000..6742f8d --- /dev/null +++ b/Seven/Socket/Data/DispatchMessage.cs @@ -0,0 +1,8 @@ +namespace TwitchChatTTS.Seven.Socket.Data +{ + public class DispatchMessage + { + public object EventType { get; set; } + public ChangeMapMessage Body { get; set; } + } +} \ No newline at end of file diff --git a/Seven/Socket/Data/EndOfStreamMessage.cs b/Seven/Socket/Data/EndOfStreamMessage.cs new file mode 100644 index 0000000..e25b1da --- /dev/null +++ b/Seven/Socket/Data/EndOfStreamMessage.cs @@ -0,0 +1,8 @@ +namespace TwitchChatTTS.Seven.Socket.Data +{ + public class EndOfStreamMessage + { + public int Code { get; set; } + public string Message { get; set; } + } +} \ No newline at end of file diff --git a/Seven/Socket/Data/ErrorMessage.cs b/Seven/Socket/Data/ErrorMessage.cs new file mode 100644 index 0000000..8f5f122 --- /dev/null +++ b/Seven/Socket/Data/ErrorMessage.cs @@ -0,0 +1,7 @@ +namespace TwitchChatTTS.Seven.Socket.Data +{ + public class ErrorMessage + { + + } +} \ No newline at end of file diff --git a/Seven/Socket/Data/IdentifyMessage.cs b/Seven/Socket/Data/IdentifyMessage.cs new file mode 100644 index 0000000..bd6ccfb --- /dev/null +++ b/Seven/Socket/Data/IdentifyMessage.cs @@ -0,0 +1,7 @@ +namespace TwitchChatTTS.Seven.Socket.Data +{ + public class IdentifyMessage + { + + } +} \ No newline at end of file diff --git a/Seven/Socket/Data/ReconnectMessage.cs b/Seven/Socket/Data/ReconnectMessage.cs new file mode 100644 index 0000000..c32c3e6 --- /dev/null +++ b/Seven/Socket/Data/ReconnectMessage.cs @@ -0,0 +1,7 @@ +namespace TwitchChatTTS.Seven.Socket.Data +{ + public class ReconnectMessage + { + public string Reason { get; set; } + } +} \ No newline at end of file diff --git a/Seven/Socket/Data/ResumeMessage.cs b/Seven/Socket/Data/ResumeMessage.cs new file mode 100644 index 0000000..3161b60 --- /dev/null +++ b/Seven/Socket/Data/ResumeMessage.cs @@ -0,0 +1,7 @@ +namespace TwitchChatTTS.Seven.Socket.Data +{ + public class ResumeMessage + { + public string SessionId { get; set; } + } +} \ No newline at end of file diff --git a/Seven/Socket/Data/SevenHelloMessage.cs b/Seven/Socket/Data/SevenHelloMessage.cs new file mode 100644 index 0000000..3fc6dd0 --- /dev/null +++ b/Seven/Socket/Data/SevenHelloMessage.cs @@ -0,0 +1,9 @@ +namespace TwitchChatTTS.Seven.Socket.Data +{ + public class SevenHelloMessage + { + public uint HeartbeatInterval { get; set; } + public string SessionId { get; set; } + public int SubscriptionLimit { get; set; } + } +} \ No newline at end of file diff --git a/Seven/Socket/Data/SubscribeMessage.cs b/Seven/Socket/Data/SubscribeMessage.cs new file mode 100644 index 0000000..51418d8 --- /dev/null +++ b/Seven/Socket/Data/SubscribeMessage.cs @@ -0,0 +1,8 @@ +namespace TwitchChatTTS.Seven.Socket.Data +{ + public class SubscribeMessage + { + public string? Type { get; set; } + public IDictionary? Condition { get; set; } + } +} \ No newline at end of file diff --git a/Seven/Socket/Data/UnsubscribeMessage.cs b/Seven/Socket/Data/UnsubscribeMessage.cs new file mode 100644 index 0000000..c303562 --- /dev/null +++ b/Seven/Socket/Data/UnsubscribeMessage.cs @@ -0,0 +1,8 @@ +namespace TwitchChatTTS.Seven.Socket.Data +{ + public class UnsubscribeMessage + { + public string Type { get; set; } + public IDictionary? Condition { get; set; } + } +} \ No newline at end of file diff --git a/Seven/Socket/Handlers/DispatchHandler.cs b/Seven/Socket/Handlers/DispatchHandler.cs new file mode 100644 index 0000000..40ec74d --- /dev/null +++ b/Seven/Socket/Handlers/DispatchHandler.cs @@ -0,0 +1,46 @@ +using System.Text.Json; +using CommonSocketLibrary.Abstract; +using CommonSocketLibrary.Common; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using TwitchChatTTS.Seven.Socket.Data; + +namespace TwitchChatTTS.Seven.Socket.Handlers +{ + public class DispatchHandler : IWebSocketHandler + { + private ILogger Logger { get; } + private IServiceProvider ServiceProvider { get; } + public int OperationCode { get; set; } = 0; + + public DispatchHandler(ILogger logger, IServiceProvider serviceProvider) { + Logger = logger; + ServiceProvider = serviceProvider; + } + + public async Task Execute(SocketClient sender, Data message) + { + if (message is not DispatchMessage obj || obj == null) + return; + + Do(obj?.Body?.Pulled, cf => cf.OldValue); + Do(obj?.Body?.Pushed, cf => cf.Value); + } + + private void Do(IEnumerable? fields, Func getter) { + if (fields is null) + return; + + //ServiceProvider.GetRequiredService() + foreach (var val in fields) { + if (getter(val) == null) + continue; + + var o = JsonSerializer.Deserialize(val.OldValue.ToString(), new JsonSerializerOptions() { + PropertyNameCaseInsensitive = false, + PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower + }); + } + } + } +} \ No newline at end of file diff --git a/Seven/Socket/Handlers/EndOfStreamHandler.cs b/Seven/Socket/Handlers/EndOfStreamHandler.cs new file mode 100644 index 0000000..dfe1511 --- /dev/null +++ b/Seven/Socket/Handlers/EndOfStreamHandler.cs @@ -0,0 +1,88 @@ +using CommonSocketLibrary.Abstract; +using CommonSocketLibrary.Common; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using TwitchChatTTS.Seven.Socket.Context; +using TwitchChatTTS.Seven.Socket.Data; + +namespace TwitchChatTTS.Seven.Socket.Handlers +{ + public class EndOfStreamHandler : IWebSocketHandler + { + private ILogger Logger { get; } + private IServiceProvider ServiceProvider { get; } + private string[] ErrorCodes { get; } + private int[] ReconnectDelay { get; } + + public int OperationCode { get; set; } = 7; + + + public EndOfStreamHandler(ILogger logger, IServiceProvider serviceProvider) { + Logger = logger; + ServiceProvider = serviceProvider; + + ErrorCodes = [ + "Server Error", + "Unknown Operation", + "Invalid Payload", + "Auth Failure", + "Already Identified", + "Rate Limited", + "Restart", + "Maintenance", + "Timeout", + "Already Subscribed", + "Not Subscribed", + "Insufficient Privilege", + "Inactivity?" + ]; + ReconnectDelay = [ + 1000, + -1, + -1, + -1, + -1, + 3000, + 1000, + 300000, + 1000, + -1, + -1, + 1000, + 1000 + ]; + } + + public async Task Execute(SocketClient sender, Data message) + { + if (message is not EndOfStreamMessage obj || obj == null) + return; + + var code = obj.Code - 4000; + if (code >= 0 && code < ErrorCodes.Length) + Logger.LogWarning($"Received end of stream message (reason: {ErrorCodes[code]}, code: {obj.Code}, message: {obj.Message})."); + else + Logger.LogWarning($"Received end of stream message (code: {obj.Code}, message: {obj.Message})."); + + await sender.DisconnectAsync(); + + if (code >= 0 && code < ReconnectDelay.Length && ReconnectDelay[code] < 0) { + Logger.LogError($"7tv client will remain disconnected due to a bad client implementation."); + return; + } + + var context = ServiceProvider.GetRequiredService(); + await Task.Delay(ReconnectDelay[code]); + + Logger.LogInformation($"7tv client reconnecting."); + await sender.ConnectAsync($"{context.Protocol ?? "wss"}://{context.Url}"); + if (context.SessionId is null) { + await sender.Send(33, new object()); + } else { + await sender.Send(34, new ResumeMessage() { + SessionId = context.SessionId + }); + } + } + } +} \ No newline at end of file diff --git a/Seven/Socket/Handlers/ErrorHandler.cs b/Seven/Socket/Handlers/ErrorHandler.cs new file mode 100644 index 0000000..394ab40 --- /dev/null +++ b/Seven/Socket/Handlers/ErrorHandler.cs @@ -0,0 +1,23 @@ +using CommonSocketLibrary.Abstract; +using CommonSocketLibrary.Common; +using Microsoft.Extensions.Logging; +using TwitchChatTTS.Seven.Socket.Data; + +namespace TwitchChatTTS.Seven.Socket.Handlers +{ + public class ErrorHandler : IWebSocketHandler + { + private ILogger Logger { get; } + public int OperationCode { get; set; } = 6; + + public ErrorHandler(ILogger logger) { + Logger = logger; + } + + public async Task Execute(SocketClient sender, Data message) + { + if (message is not ErrorMessage obj || obj == null) + return; + } + } +} \ No newline at end of file diff --git a/Seven/Socket/Handlers/ReconnectHandler.cs b/Seven/Socket/Handlers/ReconnectHandler.cs new file mode 100644 index 0000000..4742ad9 --- /dev/null +++ b/Seven/Socket/Handlers/ReconnectHandler.cs @@ -0,0 +1,25 @@ +using CommonSocketLibrary.Abstract; +using CommonSocketLibrary.Common; +using Microsoft.Extensions.Logging; +using TwitchChatTTS.Seven.Socket.Data; + +namespace TwitchChatTTS.Seven.Socket.Handlers +{ + public class ReconnectHandler : IWebSocketHandler + { + private ILogger Logger { get; } + public int OperationCode { get; set; } = 4; + + public ReconnectHandler(ILogger logger) { + Logger = logger; + } + + public async Task Execute(SocketClient sender, Data message) + { + if (message is not ReconnectMessage obj || obj == null) + return; + + Logger.LogInformation($"7tv server wants us to reconnect (reason: {obj.Reason})."); + } + } +} \ No newline at end of file diff --git a/Seven/Socket/Handlers/SevenHelloHandler.cs b/Seven/Socket/Handlers/SevenHelloHandler.cs new file mode 100644 index 0000000..13b23e7 --- /dev/null +++ b/Seven/Socket/Handlers/SevenHelloHandler.cs @@ -0,0 +1,56 @@ +using CommonSocketLibrary.Abstract; +using CommonSocketLibrary.Common; +using Microsoft.Extensions.Logging; +using TwitchChatTTS.Seven.Socket.Context; +using TwitchChatTTS.Seven.Socket.Data; + +namespace TwitchChatTTS.Seven.Socket.Handlers +{ + public class SevenHelloHandler : IWebSocketHandler + { + private ILogger Logger { get; } + private SevenHelloContext Context { get; } + public int OperationCode { get; set; } = 1; + + public SevenHelloHandler(ILogger logger, SevenHelloContext context) { + Logger = logger; + Context = context; + } + + public async Task Execute(SocketClient sender, Data message) + { + if (message is not SevenHelloMessage obj || obj == null) + return; + + if (sender is not SevenSocketClient seven || seven == null) + return; + + seven.Connected = true; + seven.ConnectionDetails = obj; + + // if (Context.Subscriptions == null || !Context.Subscriptions.Any()) { + // Logger.LogWarning("No subscriptions have been set for the 7tv websocket client."); + // return; + // } + + //await Task.Delay(TimeSpan.FromMilliseconds(1000)); + //await sender.Send(33, new IdentifyMessage()); + //await Task.Delay(TimeSpan.FromMilliseconds(5000)); + //await sender.SendRaw("{\"op\":35,\"d\":{\"type\":\"emote_set.*\",\"condition\":{\"object_id\":\"64505914b9fc508169ffe7cc\"}}}"); + //await sender.SendRaw(File.ReadAllText("test.txt")); + + // foreach (var sub in Context.Subscriptions) { + // if (string.IsNullOrWhiteSpace(sub.Type)) { + // Logger.LogWarning("Non-existent or empty subscription type found on the 7tv websocket client."); + // continue; + // } + + // Logger.LogDebug($"Subscription Type: {sub.Type} | Condition: {string.Join(", ", sub.Condition?.Select(e => e.Key + "=" + e.Value) ?? new string[0])}"); + // await sender.Send(35, new SubscribeMessage() { + // Type = sub.Type, + // Condition = sub.Condition + // }); + // } + } + } +} \ No newline at end of file diff --git a/Seven/Socket/Manager/SevenHandlerManager.cs b/Seven/Socket/Manager/SevenHandlerManager.cs new file mode 100644 index 0000000..e7cb97d --- /dev/null +++ b/Seven/Socket/Manager/SevenHandlerManager.cs @@ -0,0 +1,35 @@ +using Microsoft.Extensions.Logging; +using CommonSocketLibrary.Socket.Manager; +using CommonSocketLibrary.Common; +using Microsoft.Extensions.DependencyInjection; + +namespace TwitchChatTTS.Seven.Socket.Manager +{ + public class SevenHandlerManager : WebSocketHandlerManager + { + public SevenHandlerManager(ILogger logger, IServiceProvider provider) : base(logger) { + try { + var basetype = typeof(IWebSocketHandler); + var assembly = GetType().Assembly; + var types = assembly.GetTypes().Where(t => t.IsClass && basetype.IsAssignableFrom(t) && t.AssemblyQualifiedName?.Contains(".Seven.") == true); + + foreach (var type in types) { + var key = "7tv-" + type.Name.Replace("Handlers", "Hand#lers") + .Replace("Handler", "") + .Replace("Hand#lers", "Handlers") + .ToLower(); + var handler = provider.GetKeyedService(key); + if (handler == null) { + logger.LogError("Failed to find 7tv websocket handler: " + type.AssemblyQualifiedName); + continue; + } + + Logger.LogDebug($"Linked type {type.AssemblyQualifiedName} to 7tv websocket handler {handler.GetType().AssemblyQualifiedName}."); + Add(handler); + } + } catch (Exception e) { + Logger.LogError(e, "Failed to load 7tv websocket handler types."); + } + } + } +} \ No newline at end of file diff --git a/Seven/Socket/Manager/SevenHandlerTypeManager.cs b/Seven/Socket/Manager/SevenHandlerTypeManager.cs new file mode 100644 index 0000000..c140c10 --- /dev/null +++ b/Seven/Socket/Manager/SevenHandlerTypeManager.cs @@ -0,0 +1,19 @@ +using CommonSocketLibrary.Abstract; +using CommonSocketLibrary.Common; +using CommonSocketLibrary.Socket.Manager; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace TwitchChatTTS.Seven.Socket.Manager +{ + public class SevenHandlerTypeManager : WebSocketHandlerTypeManager + { + public SevenHandlerTypeManager( + ILogger factory, + [FromKeyedServices("7tv")] HandlerManager handlers + ) : base(factory, handlers) + { + } + } +} \ No newline at end of file diff --git a/Seven/Socket/SevenSocketClient.cs b/Seven/Socket/SevenSocketClient.cs new file mode 100644 index 0000000..e350219 --- /dev/null +++ b/Seven/Socket/SevenSocketClient.cs @@ -0,0 +1,24 @@ +using CommonSocketLibrary.Common; +using CommonSocketLibrary.Abstract; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using TwitchChatTTS.Seven.Socket.Data; +using System.Text.Json; + +namespace TwitchChatTTS.Seven.Socket +{ + public class SevenSocketClient : WebSocketClient { + public SevenHelloMessage? ConnectionDetails { get; set; } + + public SevenSocketClient( + ILogger logger, + [FromKeyedServices("7tv")] HandlerManager handlerManager, + [FromKeyedServices("7tv")] HandlerTypeManager typeManager + ) : base(logger, handlerManager, typeManager, new JsonSerializerOptions() { + PropertyNameCaseInsensitive = false, + PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower + }) { + ConnectionDetails = null; + } + } +} \ No newline at end of file diff --git a/Seven/UserDetails.cs b/Seven/UserDetails.cs new file mode 100644 index 0000000..9704930 --- /dev/null +++ b/Seven/UserDetails.cs @@ -0,0 +1,12 @@ +namespace TwitchChatTTS.Seven +{ + public class UserDetails + { + public string Id { get; set; } + public string Platform { get; set; } + public string Username { get; set; } + public int EmoteCapacity { get; set; } + public int? EmoteSetId { get; set; } + public EmoteSet EmoteSet { get; set; } + } +} \ No newline at end of file diff --git a/Startup.cs b/Startup.cs new file mode 100644 index 0000000..490a484 --- /dev/null +++ b/Startup.cs @@ -0,0 +1,183 @@ +using TwitchChatTTS.OBS.Socket.Manager; +using TwitchChatTTS.OBS.Socket; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.DependencyInjection; +using TwitchChatTTS; +using CommonSocketLibrary.Abstract; +using CommonSocketLibrary.Common; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; +using TwitchChatTTS.Twitch; +using Microsoft.Extensions.Logging; +using TwitchChatTTS.Seven.Socket.Manager; +using TwitchChatTTS.Seven.Socket; +using TwitchChatTTS.OBS.Socket.Handlers; +using TwitchChatTTS.Seven.Socket.Handlers; +using TwitchChatTTS.Seven.Socket.Context; +using TwitchChatTTS.Seven; +using TwitchChatTTS.OBS.Socket.Context; + +/** +Future handshake/connection procedure: +- GET all tts config data +- Continuous connection to server to receive commands from tom & send logs/errors (med priority, though tough task) + +Ideas: +- Filter messages by badges. +- Speed up TTS based on message queue size? +- Cut TTS off shortly after raid (based on size of raid)? +- Limit duration of TTS +**/ + +// dotnet publish -r linux-x64 -p:PublishSingleFile=true --self-contained true +// dotnet publish -r win-x64 -p:PublishSingleFile=true --self-contained true +// SE voices: https://api.streamelements.com/kappa/v2/speech?voice=brian&text=hello + +// TODO: +// Fix OBS/7tv websocket connections when not available. +// Make it possible to do things at end of streams. +// Update emote database with twitch emotes. +// Event Subscription for emote usage? + +HostApplicationBuilder builder = Host.CreateApplicationBuilder(args); +var s = builder.Services; + +var deserializer = new DeserializerBuilder() + .WithNamingConvention(HyphenatedNamingConvention.Instance) + .Build(); + +var configContent = File.ReadAllText("tts.config.yml"); +var configuration = deserializer.Deserialize(configContent); +var redeemKeys = configuration.Twitch?.Redeems?.Keys; +if (redeemKeys is not null) { + foreach (var key in redeemKeys) { + if (key != key.ToLower() && configuration.Twitch?.Redeems != null) + configuration.Twitch.Redeems.Add(key.ToLower(), configuration.Twitch.Redeems[key]); + } +} +s.AddSingleton(configuration); + +s.AddLogging(); + +s.AddSingleton(sp => { + var context = new TTSContext(); + var logger = sp.GetRequiredService>(); + var hermes = sp.GetRequiredService(); + + logger.LogInformation("Fetching TTS username filters..."); + var usernameFiltersList = hermes.FetchTTSUsernameFilters(); + usernameFiltersList.Wait(); + context.UsernameFilters = usernameFiltersList.Result.Where(x => x.Username != null).ToDictionary(x => x.Username ?? "", x => x); + logger.LogInformation($"{context.UsernameFilters.Where(f => f.Value.Tag == "blacklisted").Count()} username(s) have been blocked."); + logger.LogInformation($"{context.UsernameFilters.Where(f => f.Value.Tag == "priority").Count()} user(s) have been prioritized."); + + var enabledVoices = hermes.FetchTTSEnabledVoices(); + enabledVoices.Wait(); + context.EnabledVoices = enabledVoices.Result; + logger.LogInformation($"{context.EnabledVoices.Count()} TTS voices enabled."); + + var wordFilters = hermes.FetchTTSWordFilters(); + wordFilters.Wait(); + context.WordFilters = wordFilters.Result; + logger.LogInformation($"{context.WordFilters.Count()} TTS word filters."); + + var defaultVoice = hermes.FetchTTSDefaultVoice(); + defaultVoice.Wait(); + context.DefaultVoice = defaultVoice.Result ?? "Brian"; + logger.LogInformation("Default Voice: " + context.DefaultVoice); + + return context; +}); +s.AddSingleton(); +s.AddSingleton(); +s.AddSingleton(); +s.AddTransient(sp => { + var hermes = sp.GetRequiredService(); + var task = hermes.FetchTwitchBotToken(); + task.Wait(); + return task.Result; +}); +s.AddSingleton(); + +s.AddSingleton(); +s.AddSingleton(sp => { + var api = sp.GetRequiredService(); + var task = api.GetSevenEmotes(); + task.Wait(); + return task.Result; +}); +var emoteCounter = new EmoteCounter(); +if (!string.IsNullOrWhiteSpace(configuration.Emotes?.CounterFilePath) && File.Exists(configuration.Emotes.CounterFilePath.Trim())) { + var d = new DeserializerBuilder() + .WithNamingConvention(HyphenatedNamingConvention.Instance) + .Build(); + emoteCounter = deserializer.Deserialize(File.ReadAllText(configuration.Emotes.CounterFilePath.Trim())); +} +s.AddSingleton(emoteCounter); + +// OBS websocket +s.AddSingleton(sp => + new HelloContext() { + Host = string.IsNullOrWhiteSpace(configuration.Obs?.Host) ? null : configuration.Obs.Host.Trim(), + Port = configuration.Obs?.Port, + Password = string.IsNullOrWhiteSpace(configuration.Obs?.Password) ? null : configuration.Obs.Password.Trim() + } +); +s.AddKeyedSingleton("obs-hello"); +s.AddKeyedSingleton("obs-identified"); +s.AddKeyedSingleton("obs-requestresponse"); +s.AddKeyedSingleton("obs-eventmessage"); + +s.AddKeyedSingleton, OBSHandlerManager>("obs"); +s.AddKeyedSingleton, OBSHandlerTypeManager>("obs"); +s.AddKeyedSingleton, OBSSocketClient>("obs"); + +// 7tv websocket +s.AddTransient(sp => { + var logger = sp.GetRequiredService>(); + var configuration = sp.GetRequiredService(); + var client = sp.GetRequiredKeyedService>("7tv") as SevenSocketClient; + if (client == null) { + logger.LogError("7tv client is null."); + return new ReconnectContext() { + Protocol = configuration.Seven?.Protocol, + Url = configuration.Seven?.Url, + SessionId = null + }; + } + if (client.ConnectionDetails == null) { + logger.LogError("Connection details in 7tv client is null."); + return new ReconnectContext() { + Protocol = configuration.Seven?.Protocol, + Url = configuration.Seven?.Url, + SessionId = null + }; + } + return new ReconnectContext() { + Protocol = configuration.Seven?.Protocol, + Url = configuration.Seven?.Url, + SessionId = client.ConnectionDetails.SessionId + }; +}); +s.AddSingleton(sp => { + return new SevenHelloContext() { + Subscriptions = configuration.Seven?.InitialSubscriptions + }; +}); +s.AddKeyedSingleton("7tv-sevenhello"); +s.AddKeyedSingleton("7tv-hello"); +s.AddKeyedSingleton("7tv-dispatch"); +s.AddKeyedSingleton("7tv-reconnect"); +s.AddKeyedSingleton("7tv-error"); +s.AddKeyedSingleton("7tv-endofstream"); + +s.AddKeyedSingleton, SevenHandlerManager>("7tv"); +s.AddKeyedSingleton, SevenHandlerTypeManager>("7tv"); +s.AddKeyedSingleton, SevenSocketClient>("7tv"); + +s.AddHostedService(); + +using IHost host = builder.Build(); +using IServiceScope scope = host.Services.CreateAsyncScope(); +IServiceProvider provider = scope.ServiceProvider; +await host.RunAsync(); \ No newline at end of file diff --git a/TTS.cs b/TTS.cs new file mode 100644 index 0000000..49f4f3c --- /dev/null +++ b/TTS.cs @@ -0,0 +1,219 @@ +using System.Runtime.InteropServices; +using System.Web; +using CommonSocketLibrary.Abstract; +using CommonSocketLibrary.Common; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using NAudio.Wave; +using NAudio.Wave.SampleProviders; +using TwitchLib.Client.Events; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; + +namespace TwitchChatTTS +{ + public class TTS : IHostedService + { + private ILogger Logger { get; } + private Configuration Configuration { get; } + private TTSPlayer Player { get; } + private IServiceProvider ServiceProvider { get; } + private ISampleProvider? Playing { get; set; } + + public TTS(ILogger logger, Configuration configuration, TTSPlayer player, IServiceProvider serviceProvider) { + Logger = logger; + Configuration = configuration; + Player = player; + ServiceProvider = serviceProvider; + } + + public async Task StartAsync(CancellationToken cancellationToken) { + Console.Title = "TTS - Twitch Chat"; + + await InitializeSevenTv(); + await InitializeObs(); + + try { + var hermes = await InitializeHermes(); + var twitchapiclient = await InitializeTwitchApiClient(hermes); + + AudioPlaybackEngine.Instance.AddOnMixerInputEnded((object? s, SampleProviderEventArgs e) => { + if (e.SampleProvider == Playing) { + Playing = null; + } + }); + + Task.Run(async () => { + while (true) { + try { + if (cancellationToken.IsCancellationRequested) { + Logger.LogWarning("TTS Buffer - Cancellation token was canceled."); + return; + } + + var m = Player.ReceiveBuffer(); + if (m == null) { + await Task.Delay(200); + continue; + } + + string url = $"https://api.streamelements.com/kappa/v2/speech?voice={m.Voice}&text={HttpUtility.UrlEncode(m.Message)}"; + var sound = new NetworkWavSound(url); + var provider = new CachedWavProvider(sound); + var data = AudioPlaybackEngine.Instance.ConvertSound(provider); + var resampled = new WdlResamplingSampleProvider(data, AudioPlaybackEngine.Instance.SampleRate); + Logger.LogDebug("Fetched TTS audio data."); + + m.Audio = resampled; + Player.Ready(m); + } catch (COMException e) { + Logger.LogError(e, "Failed to send request for TTS (HResult: " + e.HResult + ")."); + } catch (Exception e) { + Logger.LogError(e, "Failed to send request for TTS."); + } + } + }); + + Task.Run(async () => { + while (true) { + try { + if (cancellationToken.IsCancellationRequested) { + Logger.LogWarning("TTS Queue - Cancellation token was canceled."); + return; + } + while (Player.IsEmpty() || Playing != null) { + await Task.Delay(200); + continue; + } + var m = Player.ReceiveReady(); + if (m == null) { + continue; + } + + if (!string.IsNullOrWhiteSpace(m.File) && File.Exists(m.File)) { + Logger.LogInformation("Playing message: " + m.File); + AudioPlaybackEngine.Instance.PlaySound(m.File); + continue; + } + + Logger.LogInformation("Playing message: " + m.Message); + Playing = m.Audio; + if (m.Audio != null) + AudioPlaybackEngine.Instance.AddMixerInput(m.Audio); + } catch (Exception e) { + Logger.LogError(e, "Failed to play a TTS audio message"); + } + } + }); + + StartSavingEmoteCounter(); + + Logger.LogInformation("Twitch API client connecting..."); + await twitchapiclient.Connect(); + } catch (Exception e) { + Logger.LogError(e, "Failed to initialize."); + } + Console.ReadLine(); + } + + public async Task StopAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + Logger.LogWarning("Application has stopped due to cancellation token."); + else + Logger.LogWarning("Application has stopped."); + } + + private async Task InitializeSevenTv() { + Logger.LogInformation("Initializing 7tv client."); + var sevenClient = ServiceProvider.GetRequiredKeyedService>("7tv"); + if (Configuration.Seven is not null && !string.IsNullOrWhiteSpace(Configuration.Seven.Url)) { + var base_url = "@" + string.Join(",", Configuration.Seven.InitialSubscriptions.Select(sub => sub.Type + "<" + string.Join(",", sub.Condition?.Select(e => e.Key + "=" + e.Value) ?? new string[0]) + ">")); + Logger.LogDebug($"Attempting to connect to {Configuration.Seven.Protocol?.Trim() ?? "wss"}://{Configuration.Seven.Url.Trim()}{base_url}"); + await sevenClient.ConnectAsync($"{Configuration.Seven.Protocol?.Trim() ?? "wss"}://{Configuration.Seven.Url.Trim()}{base_url}"); + } + } + + private async Task InitializeObs() { + Logger.LogInformation("Initializing obs client."); + var obsClient = ServiceProvider.GetRequiredKeyedService>("obs"); + if (Configuration.Obs is not null && !string.IsNullOrWhiteSpace(Configuration.Obs.Host) && Configuration.Obs.Port.HasValue && Configuration.Obs.Port.Value >= 0) { + Logger.LogDebug($"Attempting to connect to ws://{Configuration.Obs.Host.Trim()}:{Configuration.Obs.Port}"); + await obsClient.ConnectAsync($"ws://{Configuration.Obs.Host.Trim()}:{Configuration.Obs.Port}"); + await Task.Delay(500); + } + } + + private async Task InitializeHermes() { + // Fetch id and username based on api key given. + Logger.LogInformation("Initializing hermes client."); + var hermes = ServiceProvider.GetRequiredService(); + await hermes.FetchHermesAccountDetails(); + + if (hermes.Username == null) + throw new Exception("Username fetched from Hermes is invalid."); + + Logger.LogInformation("Username: " + hermes.Username); + return hermes; + } + + private async Task InitializeTwitchApiClient(HermesClient hermes) { + Logger.LogInformation("Initializing twitch client."); + var twitchapiclient = ServiceProvider.GetRequiredService(); + await twitchapiclient.Authorize(); + + var channels = Configuration.Twitch?.Channels ?? [hermes.Username]; + Logger.LogInformation("Twitch channels: " + string.Join(", ", channels)); + twitchapiclient.InitializeClient(hermes, channels); + twitchapiclient.InitializePublisher(); + + var handler = ServiceProvider.GetRequiredService(); + twitchapiclient.AddOnNewMessageReceived(async Task (object? s, OnMessageReceivedArgs e) => { + var result = handler.Handle(e); + + switch (result) { + case MessageResult.Skip: + if (Playing != null) { + AudioPlaybackEngine.Instance.RemoveMixerInput(Playing); + Playing = null; + } + break; + case MessageResult.SkipAll: + Player.RemoveAll(); + if (Playing != null) { + AudioPlaybackEngine.Instance.RemoveMixerInput(Playing); + Playing = null; + } + break; + default: + break; + } + }); + + return twitchapiclient; + } + + private async Task StartSavingEmoteCounter() { + Task.Run(async () => { + while (true) { + try { + await Task.Delay(TimeSpan.FromSeconds(300)); + + var serializer = new SerializerBuilder() + .WithNamingConvention(HyphenatedNamingConvention.Instance) + .Build(); + + var chathandler = ServiceProvider.GetRequiredService(); + using (TextWriter writer = File.CreateText(Configuration.Emotes.CounterFilePath.Trim())) + { + await writer.WriteAsync(serializer.Serialize(chathandler.EmoteCounter)); + } + } catch (Exception e) { + Logger.LogError(e, "Failed to save the emote counter."); + } + } + }); + } + } +} \ No newline at end of file diff --git a/Twitch/TTSContext.cs b/Twitch/TTSContext.cs new file mode 100644 index 0000000..0340edf --- /dev/null +++ b/Twitch/TTSContext.cs @@ -0,0 +1,12 @@ +using TwitchChatTTS.Hermes; + +namespace TwitchChatTTS.Twitch +{ + public class TTSContext + { + public string DefaultVoice; + public IEnumerable? EnabledVoices; + public IDictionary? UsernameFilters; + public IEnumerable? WordFilters; + } +} \ No newline at end of file diff --git a/Twitch/TwitchApiClient.cs b/Twitch/TwitchApiClient.cs new file mode 100644 index 0000000..423e05e --- /dev/null +++ b/Twitch/TwitchApiClient.cs @@ -0,0 +1,184 @@ +using System.Text.Json; +using TwitchChatTTS.Helpers; +using Microsoft.Extensions.Logging; +using TwitchChatTTS; +using TwitchLib.Api.Core.Exceptions; +using TwitchLib.Client; +using TwitchLib.Client.Events; +using TwitchLib.Client.Models; +using TwitchLib.Communication.Clients; +using TwitchLib.Communication.Events; +using TwitchLib.PubSub; +using static TwitchChatTTS.Configuration; + +public class TwitchApiClient { + private TwitchBotToken Token { get; } + private TwitchClient Client { get; } + private TwitchPubSub Publisher { get; } + private WebClientWrap Web { get; } + private Configuration Configuration { get; } + private ILogger Logger { get; } + private bool Initialized { get; set; } + + + public TwitchApiClient(Configuration configuration, ILogger logger, TwitchBotToken token) { + Configuration = configuration; + Logger = logger; + Client = new TwitchClient(new WebSocketClient()); + Publisher = new TwitchPubSub(); + Initialized = false; + Token = token; + + Web = new WebClientWrap(new JsonSerializerOptions() { + PropertyNameCaseInsensitive = false, + PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower + }); + if (!string.IsNullOrWhiteSpace(Configuration.Hermes?.Token)) + Web.AddHeader("x-api-key", Configuration.Hermes?.Token); + } + + public async Task Authorize() { + try { + var authorize = await Web.GetJson("https://hermes.goblincaves.com/api/account/reauthorize"); + if (authorize != null && Token.BroadcasterId == authorize.BroadcasterId) { + Token.AccessToken = authorize.AccessToken; + Token.RefreshToken = authorize.RefreshToken; + Logger.LogInformation("Updated Twitch API tokens."); + } else if (authorize != null) { + Logger.LogError("Twitch API Authorization failed."); + } + } catch (HttpResponseException e) { + if (string.IsNullOrWhiteSpace(Configuration.Hermes?.Token)) + Logger.LogError("No Hermes API key found. Enter it into the configuration file."); + else + Logger.LogError("Invalid Hermes API key. Double check the token. HTTP Error Code: " + e.HttpResponse.StatusCode); + } catch (JsonException) { + } catch (Exception e) { + Logger.LogError(e, "Failed to authorize to Twitch API."); + } + } + + public async Task Connect() { + Client.Connect(); + await Publisher.ConnectAsync(); + } + + public void InitializeClient(HermesClient hermes, IEnumerable channels) { + ConnectionCredentials credentials = new ConnectionCredentials(hermes.Username, Token?.AccessToken); + Client.Initialize(credentials, channels.Distinct().ToList()); + + if (Initialized) { + Logger.LogDebug("Twitch API client has already been initialized."); + return; + } + + Initialized = true; + + Client.OnJoinedChannel += async Task (object? s, OnJoinedChannelArgs e) => { + Logger.LogInformation("Joined channel: " + e.Channel); + }; + + Client.OnConnected += async Task (object? s, OnConnectedArgs e) => { + Logger.LogInformation("-----------------------------------------------------------"); + }; + + Client.OnIncorrectLogin += async Task (object? s, OnIncorrectLoginArgs e) => { + Logger.LogError(e.Exception, "Incorrect Login on Twitch API client."); + + Logger.LogInformation("Attempting to re-authorize."); + await Authorize(); + }; + + Client.OnConnectionError += async Task (object? s, OnConnectionErrorArgs e) => { + Logger.LogError("Connection Error: " + e.Error.Message + " (" + e.Error.GetType().Name + ")"); + }; + + Client.OnError += async Task (object? s, OnErrorEventArgs e) => { + Logger.LogError(e.Exception, "Twitch API client error."); + }; + } + + public void InitializePublisher() { + Publisher.OnPubSubServiceConnected += async (s, e) => { + Publisher.ListenToChannelPoints(Token.BroadcasterId); + Publisher.ListenToFollows(Token.BroadcasterId); + + await Publisher.SendTopicsAsync(Token.AccessToken); + Logger.LogInformation("Twitch PubSub has been connected."); + }; + + Publisher.OnFollow += (s, e) => { + Logger.LogInformation("Follow: " + e.DisplayName); + }; + + Publisher.OnChannelPointsRewardRedeemed += (s, e) => { + Logger.LogInformation($"Channel Point Reward Redeemed: {e.RewardRedeemed.Redemption.Reward.Title} (id: {e.RewardRedeemed.Redemption.Id})"); + + if (Configuration.Twitch?.Redeems is null) { + Logger.LogDebug("No redeems found in the configuration."); + return; + } + + var redeemName = e.RewardRedeemed.Redemption.Reward.Title.ToLower().Trim().Replace(" ", "-"); + if (!Configuration.Twitch.Redeems.TryGetValue(redeemName, out RedeemConfiguration? redeem)) + return; + + if (redeem is null) + return; + + // Write or append to file if needed. + var outputFile = string.IsNullOrWhiteSpace(redeem.OutputFilePath) ? null : redeem.OutputFilePath.Trim(); + if (outputFile is null) { + Logger.LogDebug($"No output file was provided for redeem '{e.RewardRedeemed.Redemption.Reward.Title}'."); + } else { + var outputContent = string.IsNullOrWhiteSpace(redeem.OutputContent) ? null : redeem.OutputContent.Trim().Replace("%USER%", e.RewardRedeemed.Redemption.User.DisplayName).Replace("\\n", "\n"); + if (outputContent is null) { + Logger.LogWarning($"No output content was provided for redeem '{e.RewardRedeemed.Redemption.Reward.Title}'."); + } else { + if (redeem.OutputAppend == true) { + File.AppendAllText(outputFile, outputContent + "\n"); + } else { + File.WriteAllText(outputFile, outputContent); + } + } + } + + // Play audio file if needed. + var audioFile = string.IsNullOrWhiteSpace(redeem.AudioFilePath) ? null : redeem.AudioFilePath.Trim(); + if (audioFile is null) { + Logger.LogDebug($"No audio file was provided for redeem '{e.RewardRedeemed.Redemption.Reward.Title}'."); + return; + } + if (!File.Exists(audioFile)) { + Logger.LogWarning($"Cannot find audio file @ {audioFile} for redeem '{e.RewardRedeemed.Redemption.Reward.Title}'."); + return; + } + + AudioPlaybackEngine.Instance.PlaySound(audioFile); + }; + + /*int psConnectionFailures = 0; + publisher.OnPubSubServiceError += async (s, e) => { + Console.WriteLine("PubSub ran into a service error. Attempting to connect again."); + await Task.Delay(Math.Min(3000 + (1 << psConnectionFailures), 120000)); + var connect = await WebHelper.Get("https://hermes.goblincaves.com/api/account/reauthorize"); + if ((int) connect.StatusCode == 200 || (int) connect.StatusCode == 201) { + psConnectionFailures = 0; + } else { + psConnectionFailures++; + } + + var twitchBotData2 = await WebHelper.GetJson("https://hermes.goblincaves.com/api/token/bot"); + if (twitchBotData2 == null) { + Console.WriteLine("The API is down. Contact the owner."); + return; + } + twitchBotData.access_token = twitchBotData2.access_token; + await pubsub.ConnectAsync(); + };*/ + } + + public void AddOnNewMessageReceived(AsyncEventHandler handler) { + Client.OnMessageReceived += handler; + } +} \ No newline at end of file diff --git a/TwitchChatTTS/TwitchChatTTS.csproj b/TwitchChatTTS.csproj similarity index 73% rename from TwitchChatTTS/TwitchChatTTS.csproj rename to TwitchChatTTS.csproj index 69bc46c..ef5ad9f 100644 --- a/TwitchChatTTS/TwitchChatTTS.csproj +++ b/TwitchChatTTS.csproj @@ -2,15 +2,20 @@ Exe - net6.0 + net8.0 enable enable + + + + + @@ -25,6 +30,10 @@ + + + + diff --git a/TwitchChatTTS/Hermes/TTSUsernameFilter.cs b/TwitchChatTTS/Hermes/TTSUsernameFilter.cs deleted file mode 100644 index af2404a..0000000 --- a/TwitchChatTTS/Hermes/TTSUsernameFilter.cs +++ /dev/null @@ -1,5 +0,0 @@ -public class TTSUsernameFilter { - public string username { get; set; } - public string tag { get; set; } - public string userId { get; set; } -} \ No newline at end of file diff --git a/TwitchChatTTS/Hermes/TTSVoice.cs b/TwitchChatTTS/Hermes/TTSVoice.cs deleted file mode 100644 index d759b31..0000000 --- a/TwitchChatTTS/Hermes/TTSVoice.cs +++ /dev/null @@ -1,6 +0,0 @@ -public class TTSVoice { - public string label { get; set; } - public int value { get; set; } - public string gender { get; set; } - public string language { get; set; } -} \ No newline at end of file diff --git a/TwitchChatTTS/Hermes/TwitchBotToken.cs b/TwitchChatTTS/Hermes/TwitchBotToken.cs deleted file mode 100644 index 603ac9d..0000000 --- a/TwitchChatTTS/Hermes/TwitchBotToken.cs +++ /dev/null @@ -1,8 +0,0 @@ -[Serializable] -public class TwitchBotToken { - public string client_id { get; set; } - public string client_secret { get; set; } - public string access_token { get; set; } - public string refresh_token { get; set; } - public string broadcaster_id { get; set; } -} \ No newline at end of file diff --git a/TwitchChatTTS/Hermes/TwitchConnection.cs b/TwitchChatTTS/Hermes/TwitchConnection.cs deleted file mode 100644 index 5554457..0000000 --- a/TwitchChatTTS/Hermes/TwitchConnection.cs +++ /dev/null @@ -1,8 +0,0 @@ -[Serializable] -public class TwitchConnection { - public string id { get; set; } - public string secret { get; set; } - public string broadcasterId { get; set; } - public string username { get; set; } - public string userId { get; set; } -} \ No newline at end of file diff --git a/TwitchChatTTS/Program.cs b/TwitchChatTTS/Program.cs deleted file mode 100644 index da06bb3..0000000 --- a/TwitchChatTTS/Program.cs +++ /dev/null @@ -1,146 +0,0 @@ -using System.Runtime.InteropServices; -using System.Text.RegularExpressions; -using NAudio.Wave; -using TwitchLib.Api; -using TwitchLib.Client; -using TwitchLib.Client.Events; -using TwitchLib.Client.Models; -using TwitchLib.Communication.Clients; -using TwitchLib.Communication.Events; -using TwitchLib.PubSub; -using TwitchLib.PubSub.Events; -using NAudio.Wave.SampleProviders; - -/** -Future handshake/connection procedure: -- GET all tts config data -- Continuous connection to server to receive commands from tom & send logs/errors (med priority, though tough task) - -Ideas: -- Filter messages by badges, username, ..., etc. -- Filter messages by content. -- Speed up TTS based on message queue size? -- Cut TTS off shortly after raid (based on size of raid)? -- Limit duration of TTS -- Voice selection for channel and per user. -**/ - -// dotnet publish -r linux-x64 -p:PublishSingleFile=true --self-contained true -// dotnet publish -r win-x64 -p:PublishSingleFile=true --self-contained true -// SE voices: https://api.streamelements.com/kappa/v2/speech?voice=brian&text=hello - -// Read redeems from file. -var redeems = File.Exists(".redeems") ? await File.ReadAllLinesAsync(".redeems") : new string[0]; - -// Fetch id and username based on api key given. -HermesClient hermes = new HermesClient(); -Console.WriteLine("Fetching Hermes account details..."); -await hermes.UpdateHermesAccount(); - -Console.WriteLine("Username: " + hermes.Username); -Console.WriteLine(); - -Console.WriteLine("Fetching Twitch API details from Hermes..."); -TwitchApiClient twitchapiclient = new TwitchApiClient(await hermes.FetchTwitchBotToken()); -await twitchapiclient.Authorize(); - -Console.WriteLine("Fetching TTS username filters..."); -var usernameFilters = (await hermes.FetchTTSUsernameFilters()) - .ToDictionary(x => x.username, x => x); -Console.WriteLine($"{usernameFilters.Where(f => f.Value.tag == "blacklisted").Count()} username(s) have been blocked."); -Console.WriteLine($"{usernameFilters.Where(f => f.Value.tag == "priority").Count()} user(s) have been prioritized."); - -var enabledVoices = await hermes.FetchTTSEnabledVoices(); -Console.WriteLine($"{enabledVoices.Count()} TTS voices enabled."); - -var wordFilters = await hermes.FetchTTSWordFilters(); -Console.WriteLine($"{wordFilters.Count()} TTS word filters."); - -var defaultVoice = await hermes.FetchTTSDefaultVoice(); -Console.WriteLine("Default Voice: " + defaultVoice); - -TTSPlayer player = new TTSPlayer(); -ISampleProvider playing = null; - -var handler = new ChatMessageHandler(player, defaultVoice, enabledVoices, usernameFilters, wordFilters); - -var channels = File.Exists(".twitchchannels") ? File.ReadAllLines(".twitchchannels") : new string[] { hermes.Username }; -Console.WriteLine("Twitch channels: " + string.Join(", ", channels)); -twitchapiclient.InitializeClient(hermes, channels); -twitchapiclient.InitializePublisher(player, redeems); - - -twitchapiclient.AddOnNewMessageReceived(async Task (object? s, OnMessageReceivedArgs e) => { - var result = handler.Handle(e); - - switch (result) { - case MessageResult.Skip: - AudioPlaybackEngine.Instance.RemoveMixerInput(playing); - playing = null; - break; - default: - break; - } -}); - -AudioPlaybackEngine.Instance.AddOnMixerInputEnded((object? s, SampleProviderEventArgs e) => { - if (e.SampleProvider == playing) { - playing = null; - } -}); - -Task.Run(async () => { - while (true) { - try { - var m = player.ReceiveBuffer(); - if (m == null) { - await Task.Delay(200); - continue; - } - - string url = $"https://api.streamelements.com/kappa/v2/speech?voice={m.Voice}&text={m.Message}"; - var sound = new NetworkWavSound(url); - var provider = new CachedWavProvider(sound); - var data = AudioPlaybackEngine.Instance.ConvertSound(provider); - var resampled = new WdlResamplingSampleProvider(data, AudioPlaybackEngine.Instance.SampleRate); - - m.Audio = resampled; - player.Ready(m); - } catch (COMException e) { - Console.WriteLine(e.GetType().Name + ": " + e.Message + " (HResult: " + e.HResult + ")"); - } catch (Exception e) { - Console.WriteLine(e.GetType().Name + ": " + e.Message); - } - } -}); - -Task.Run(async () => { - while (true) { - try { - while (player.IsEmpty() || playing != null) { - await Task.Delay(200); - } - var m = player.ReceiveReady(); - if (m == null) { - continue; - } - - if (!string.IsNullOrWhiteSpace(m.File) && File.Exists(m.File)) { - Console.WriteLine("Playing sfx: " + m.File); - AudioPlaybackEngine.Instance.PlaySound(m.File); - continue; - } - - Console.WriteLine("Playing message: " + m.Message); - playing = m.Audio; - AudioPlaybackEngine.Instance.AddMixerInput(m.Audio); - } catch (Exception e) { - Console.WriteLine(e.GetType().Name + ": " + e.Message); - } - } -}); - -Console.WriteLine("Twitch API client connecting..."); -twitchapiclient.Connect(); -Console.ReadLine(); -Console.ReadLine(); \ No newline at end of file diff --git a/TwitchChatTTS/Twitch/TwitchApiClient.cs b/TwitchChatTTS/Twitch/TwitchApiClient.cs deleted file mode 100644 index ac28274..0000000 --- a/TwitchChatTTS/Twitch/TwitchApiClient.cs +++ /dev/null @@ -1,122 +0,0 @@ -using TwitchLib.Api; -using TwitchLib.Client; -using TwitchLib.Client.Events; -using TwitchLib.Client.Models; -using TwitchLib.Communication.Clients; -using TwitchLib.Communication.Events; -using TwitchLib.PubSub; -using TwitchLib.PubSub.Events; - -public class TwitchApiClient { - private TwitchBotToken token; - private TwitchClient client; - private TwitchPubSub publisher; - private WebHelper web; - private bool initialized; - - - public TwitchApiClient(TwitchBotToken token) { - client = new TwitchClient(new WebSocketClient()); - publisher = new TwitchPubSub(); - web = new WebHelper(); - initialized = false; - this.token = token; - } - - public async Task Authorize() { - var authorize = await web.Get("https://hermes.goblincaves.com/api/account/reauthorize"); - var status = (int) authorize.StatusCode; - return status == 200 || status == 201; - } - - public async Task Connect() { - client.Connect(); - await publisher.ConnectAsync(); - } - - public void InitializeClient(HermesClient hermes, IEnumerable channels) { - ConnectionCredentials credentials = new ConnectionCredentials(hermes.Username, token.access_token); - client.Initialize(credentials, channels.Distinct().ToList()); - - if (initialized) { - return; - } - - initialized = true; - - client.OnJoinedChannel += async Task (object? s, OnJoinedChannelArgs e) => { - Console.WriteLine("Joined Channel: " + e.Channel); - }; - - client.OnConnected += async Task (object? s, OnConnectedArgs e) => { - Console.WriteLine("-----------------------------------------------------------"); - }; - - client.OnError += async Task (object? s, OnErrorEventArgs e) => { - Console.WriteLine("Log: " + e.Exception.Message + " (" + e.Exception.GetType().Name + ")"); - }; - - client.OnIncorrectLogin += async Task (object? s, OnIncorrectLoginArgs e) => { - Console.WriteLine("Incorrect Login: " + e.Exception.Message + " (" + e.Exception.GetType().Name + ")"); - }; - - client.OnConnectionError += async Task (object? s, OnConnectionErrorArgs e) => { - Console.WriteLine("Connection Error: " + e.Error.Message + " (" + e.Error.GetType().Name + ")"); - }; - - client.OnError += async Task (object? s, OnErrorEventArgs e) => { - Console.WriteLine("Error: " + e.Exception.Message + " (" + e.Exception.GetType().Name + ")"); - }; - } - - public void InitializePublisher(TTSPlayer player, IEnumerable redeems) { - publisher.OnPubSubServiceConnected += async (s, e) => { - publisher.ListenToChannelPoints(token.broadcaster_id); - - await publisher.SendTopicsAsync(token.access_token); - Console.WriteLine("Twitch PubSub has been connected."); - }; - - publisher.OnFollow += (s, e) => { - Console.WriteLine("Follow: " + e.DisplayName); - }; - - publisher.OnChannelPointsRewardRedeemed += (s, e) => { - Console.WriteLine($"Channel Point Reward Redeemed: {e.RewardRedeemed.Redemption.Reward.Title} (id: {e.RewardRedeemed.Redemption.Id})"); - - if (!redeems.Any(r => r.ToLower() == e.RewardRedeemed.Redemption.Reward.Title.ToLower())) - return; - - player.Add(new TTSMessage() { - Voice = "Brian", - Message = e.RewardRedeemed.Redemption.Reward.Title, - File = $"redeems/{e.RewardRedeemed.Redemption.Reward.Title.ToLower()}.mp3", - Priority = -50 - }); - }; - - /*int psConnectionFailures = 0; - publisher.OnPubSubServiceError += async (s, e) => { - Console.WriteLine("PubSub ran into a service error. Attempting to connect again."); - await Task.Delay(Math.Min(3000 + (1 << psConnectionFailures), 120000)); - var connect = await WebHelper.Get("https://hermes.goblincaves.com/api/account/reauthorize"); - if ((int) connect.StatusCode == 200 || (int) connect.StatusCode == 201) { - psConnectionFailures = 0; - } else { - psConnectionFailures++; - } - - var twitchBotData2 = await WebHelper.GetJson("https://hermes.goblincaves.com/api/token/bot"); - if (twitchBotData2 == null) { - Console.WriteLine("The API is down. Contact the owner."); - return; - } - twitchBotData.access_token = twitchBotData2.access_token; - await pubsub.ConnectAsync(); - };*/ - } - - public void AddOnNewMessageReceived(AsyncEventHandler handler) { - client.OnMessageReceived += handler; - } -} \ No newline at end of file diff --git a/TwitchChatTTS/Web.cs b/TwitchChatTTS/Web.cs deleted file mode 100644 index 44b8ec1..0000000 --- a/TwitchChatTTS/Web.cs +++ /dev/null @@ -1,28 +0,0 @@ - - -using System.Net; -using System.Net.Http.Json; - -public class WebHelper { - private static HttpClient _client = new HttpClient(); - - public void AddHeader(string key, string? value) { - _client.DefaultRequestHeaders.Add(key, value); - } - - public async Task GetJson(string uri) { - return (T) await _client.GetFromJsonAsync(uri, typeof(T)); - } - - public async Task Get(string uri) { - return await _client.GetAsync(uri); - } - - public async Task Post(string uri, T data) { - return await _client.PostAsJsonAsync(uri, data); - } - - public async Task Post(string uri) { - return await _client.PostAsJsonAsync(uri, new object()); - } -} \ No newline at end of file