diff --git a/Fritz.Chatbot/Commands/PingCommand.cs b/Fritz.Chatbot/Commands/PingCommand.cs index 5ab4aa2d..343f4605 100644 --- a/Fritz.Chatbot/Commands/PingCommand.cs +++ b/Fritz.Chatbot/Commands/PingCommand.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System; +using System.Threading.Tasks; using Fritz.StreamLib.Core; namespace Fritz.Chatbot.Commands @@ -13,7 +14,15 @@ public class PingCommand : ICommand public async Task Execute(string userName, string fullCommandText) { - await ChatService.SendWhisperAsync(userName, "pong"); + var thisTask = ChatService.SendWhisperAsync(userName, "pong"); + + try { + await thisTask; + } catch (Exception ex) { // the first of the InnerExcpetion + var howManyExceptions = thisTask.Exception.InnerExceptions[0]; + } + + } } diff --git a/Fritz.StreamTools/.gitignore b/Fritz.StreamTools/.gitignore index 62fcdf1a..8ac19554 100644 --- a/Fritz.StreamTools/.gitignore +++ b/Fritz.StreamTools/.gitignore @@ -1,2 +1,2 @@ -wwwroot/js/signalr-client.js -wwwroot/js/GoalConfiguration.min.js +wwwroot/js/signalr-client.js +wwwroot/js/GoalConfiguration.min.js diff --git a/Fritz.StreamTools/GlobalSuppressions.cs b/Fritz.StreamTools/GlobalSuppressions.cs index 54dca3da..3908af77 100644 --- a/Fritz.StreamTools/GlobalSuppressions.cs +++ b/Fritz.StreamTools/GlobalSuppressions.cs @@ -1,12 +1,12 @@ - -// This file is used by Code Analysis to maintain SuppressMessage -// attributes that are applied to this project. -// Project-level suppressions either have no target or are given -// a specific target and scoped to a namespace, type, member, etc. - -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Readability", "RCS1018:Add default access modifier.", Justification = "")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Maintainability", "RCS1141:Add parameter to documentation comment.", Justification = "")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Redundancy", "RCS1163:Unused parameter.", Justification = "")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "RCS1090:Call 'ConfigureAwait(false)'.", Justification = "No SynchronizationContext in AspNet core")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0007:Use implicit type", Justification = "Dont want to use var for bool, int ...")] + +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. + +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Readability", "RCS1018:Add default access modifier.", Justification = "")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Maintainability", "RCS1141:Add parameter to documentation comment.", Justification = "")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Redundancy", "RCS1163:Unused parameter.", Justification = "")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "RCS1090:Call 'ConfigureAwait(false)'.", Justification = "No SynchronizationContext in AspNet core")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0007:Use implicit type", Justification = "Dont want to use var for bool, int ...")] \ No newline at end of file diff --git a/Fritz.StreamTools/Helpers/JsonExtensions.cs b/Fritz.StreamTools/Helpers/JsonExtensions.cs index a456c623..d11b5d53 100644 --- a/Fritz.StreamTools/Helpers/JsonExtensions.cs +++ b/Fritz.StreamTools/Helpers/JsonExtensions.cs @@ -1,17 +1,17 @@ -using System; -using Newtonsoft.Json.Linq; - -namespace Fritz.StreamTools.Helpers -{ - public static class JsonExtensions - { - public static bool IsNullOrEmpty(this JToken token) - { - return (token == null) || - (token.Type == JTokenType.Array && !token.HasValues) || - (token.Type == JTokenType.Object && !token.HasValues) || - (token.Type == JTokenType.String && token.ToString() == String.Empty) || - (token.Type == JTokenType.Null); - } - } -} +using System; +using Newtonsoft.Json.Linq; + +namespace Fritz.StreamTools.Helpers +{ + public static class JsonExtensions + { + public static bool IsNullOrEmpty(this JToken token) + { + return (token == null) || + (token.Type == JTokenType.Array && !token.HasValues) || + (token.Type == JTokenType.Object && !token.HasValues) || + (token.Type == JTokenType.String && token.ToString() == String.Empty) || + (token.Type == JTokenType.Null); + } + } +} diff --git a/Fritz.StreamTools/Helpers/ReflectionHelper.cs b/Fritz.StreamTools/Helpers/ReflectionHelper.cs index 0afe9d9f..5af04172 100644 --- a/Fritz.StreamTools/Helpers/ReflectionHelper.cs +++ b/Fritz.StreamTools/Helpers/ReflectionHelper.cs @@ -1,28 +1,28 @@ -using System; -using System.Reflection; -using System.Runtime.InteropServices; - -namespace Fritz.StreamTools.Helpers -{ - public static class ReflectionHelper - { - // - // Uses reflection to find the named event and calls DynamicInvoke on it - // - public static void RaiseEvent(object instance, string name, EventArgs args) - { - if (instance == null) - throw new ArgumentNullException(nameof(instance)); - if (string.IsNullOrEmpty(name)) - throw new ArgumentException("message", nameof(name)); - - var fieldInfo = instance.GetType().GetField(name, BindingFlags.NonPublic | BindingFlags.Instance); - if (fieldInfo == null) - return; - var multicastDelegate = fieldInfo.GetValue(instance) as MulticastDelegate; - - // NOTE: Using DynamicInvoke so tests work! - multicastDelegate?.DynamicInvoke(new object[] { instance, args }); - } - } -} +using System; +using System.Reflection; +using System.Runtime.InteropServices; + +namespace Fritz.StreamTools.Helpers +{ + public static class ReflectionHelper + { + // + // Uses reflection to find the named event and calls DynamicInvoke on it + // + public static void RaiseEvent(object instance, string name, EventArgs args) + { + if (instance == null) + throw new ArgumentNullException(nameof(instance)); + if (string.IsNullOrEmpty(name)) + throw new ArgumentException("message", nameof(name)); + + var fieldInfo = instance.GetType().GetField(name, BindingFlags.NonPublic | BindingFlags.Instance); + if (fieldInfo == null) + return; + var multicastDelegate = fieldInfo.GetValue(instance) as MulticastDelegate; + + // NOTE: Using DynamicInvoke so tests work! + multicastDelegate?.DynamicInvoke(new object[] { instance, args }); + } + } +} diff --git a/Fritz.StreamTools/Helpers/TaskExtensions.cs b/Fritz.StreamTools/Helpers/TaskExtensions.cs index cb886e92..2ab2b16b 100644 --- a/Fritz.StreamTools/Helpers/TaskExtensions.cs +++ b/Fritz.StreamTools/Helpers/TaskExtensions.cs @@ -1,49 +1,49 @@ -using System; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; - -namespace System.Threading.Tasks -{ - public static class TaskHelpers - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Forget(this Task task) - { - // Empty on purpose! - } - - private const int s_defaultTimeout = 5000; - - public static Task OrTimeout(this Task task, int milliseconds = s_defaultTimeout) - { - return OrTimeout(task, new TimeSpan(0, 0, 0, 0, milliseconds)); - } - - public static async Task OrTimeout(this Task task, TimeSpan timeout) - { - var completed = await Task.WhenAny(task, Task.Delay(timeout)); - if (completed != task) - { - throw new TimeoutException(); - } - - await task; - } - - public static Task OrTimeout(this Task task, int milliseconds = s_defaultTimeout) - { - return OrTimeout(task, new TimeSpan(0, 0, 0, 0, milliseconds)); - } - - public static async Task OrTimeout(this Task task, TimeSpan timeout) - { - var completed = await Task.WhenAny(task, Task.Delay(timeout)); - if (completed != task) - { - throw new TimeoutException(); - } - - return await task; - } - } -} +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +namespace System.Threading.Tasks +{ + public static class TaskHelpers + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Forget(this Task task) + { + // Empty on purpose! + } + + private const int s_defaultTimeout = 5000; + + public static Task OrTimeout(this Task task, int milliseconds = s_defaultTimeout) + { + return OrTimeout(task, new TimeSpan(0, 0, 0, 0, milliseconds)); + } + + public static async Task OrTimeout(this Task task, TimeSpan timeout) + { + var completed = await Task.WhenAny(task, Task.Delay(timeout)); + if (completed != task) + { + throw new TimeoutException(); + } + + await task; + } + + public static Task OrTimeout(this Task task, int milliseconds = s_defaultTimeout) + { + return OrTimeout(task, new TimeSpan(0, 0, 0, 0, milliseconds)); + } + + public static async Task OrTimeout(this Task task, TimeSpan timeout) + { + var completed = await Task.WhenAny(task, Task.Delay(timeout)); + if (completed != task) + { + throw new TimeoutException(); + } + + return await task; + } + } +} diff --git a/Fritz.StreamTools/Pages/CurrentViewers.cshtml b/Fritz.StreamTools/Pages/CurrentViewers.cshtml index a7363878..273f663c 100644 --- a/Fritz.StreamTools/Pages/CurrentViewers.cshtml +++ b/Fritz.StreamTools/Pages/CurrentViewers.cshtml @@ -1,59 +1,59 @@ -@page -@model Fritz.StreamTools.Pages.CurrentViewersModel -@using System.Collections.Generic -@{ - Layout = null; - - var fontIcon = new Dictionary - { - {"Mixer", "microsoft"}, - {"Fake", "tumblr" } - }; - -} - - - - - - - CurrentViewers - - - - @foreach (var service in Model.StreamService.ViewerCountByService.OrderByDescending(s => s.service)) - { - - @service.count - - } - - Total: @(Model.StreamService.ViewerCountByService.Sum(s => s.count)) - - - - - - - - +@page +@model Fritz.StreamTools.Pages.CurrentViewersModel +@using System.Collections.Generic +@{ + Layout = null; + + var fontIcon = new Dictionary + { + {"Mixer", "microsoft"}, + {"Fake", "tumblr" } + }; + +} + + + + + + + CurrentViewers + + + + @foreach (var service in Model.StreamService.ViewerCountByService.OrderByDescending(s => s.service)) + { + + @service.count + + } + + Total: @(Model.StreamService.ViewerCountByService.Sum(s => s.count)) + + + + + + + + diff --git a/Fritz.StreamTools/SampleQuotes.txt b/Fritz.StreamTools/SampleQuotes.txt index eeed99c0..6a2f9079 100644 --- a/Fritz.StreamTools/SampleQuotes.txt +++ b/Fritz.StreamTools/SampleQuotes.txt @@ -1,20 +1,20 @@ -It’s not a bug. It’s an undocumented feature! -“Software Developer” – An organism that turns caffeine into software -If debugging is the process of removing software bugs, then programming must be the process of putting them in – Edsger Dijkstra -A user interface is like a joke. If you have to explain it, it’s not that good. -I don’t care if it works on your machine! We are not shipping your machine!” – Vidiu Platon -Measuring programming progress by lines of code is like measuring aircraft building progress by weight. – Bill Gates (co-founder of Microsoft) -I’m very font of you because you are just my type. -My code DOESN’T work, I have no idea why. My code WORKS, I have no idea why. -Things aren’t always #000000 and #FFFFFF -One man’s crappy software is another man’s full time job. – Jessica Gaston -Software undergoes beta testing shortly before it’s released. Beta is Latin for “still doesn’t work”. -Always code as if the guy who ends up maintaining your code will be a violent psychopath who knows where you live. – Martin Golding -Talk is cheap. Show me the code. – Linus Torvalds -Writing the first 90 percent of a computer program takes 90 percent of the time. The remaining ten percent also takes 90 percent of the time and the final touches also take 90 percent of the time. – N.J. Rubenking -Old programmers never die. They simply give up their resources. -There are only two industries that refer to their customers as “users”. – (Edward Tufte) -Any code of your own that you haven’t looked at for six or more months might as well have been written by someone else. – (Eagleson’s Law) -If at first you don’t succeed; call it version 1.0 -If Internet Explorer is brave enough to ask to be your default browser, You are brave enough to ask that girl out. -"To teach is to learn twice." - Anonymous +It’s not a bug. It’s an undocumented feature! +“Software Developer” – An organism that turns caffeine into software +If debugging is the process of removing software bugs, then programming must be the process of putting them in – Edsger Dijkstra +A user interface is like a joke. If you have to explain it, it’s not that good. +I don’t care if it works on your machine! We are not shipping your machine!” – Vidiu Platon +Measuring programming progress by lines of code is like measuring aircraft building progress by weight. – Bill Gates (co-founder of Microsoft) +I’m very font of you because you are just my type. +My code DOESN’T work, I have no idea why. My code WORKS, I have no idea why. +Things aren’t always #000000 and #FFFFFF +One man’s crappy software is another man’s full time job. – Jessica Gaston +Software undergoes beta testing shortly before it’s released. Beta is Latin for “still doesn’t work”. +Always code as if the guy who ends up maintaining your code will be a violent psychopath who knows where you live. – Martin Golding +Talk is cheap. Show me the code. – Linus Torvalds +Writing the first 90 percent of a computer program takes 90 percent of the time. The remaining ten percent also takes 90 percent of the time and the final touches also take 90 percent of the time. – N.J. Rubenking +Old programmers never die. They simply give up their resources. +There are only two industries that refer to their customers as “users”. – (Edward Tufte) +Any code of your own that you haven’t looked at for six or more months might as well have been written by someone else. – (Eagleson’s Law) +If at first you don’t succeed; call it version 1.0 +If Internet Explorer is brave enough to ask to be your default browser, You are brave enough to ask that girl out. +"To teach is to learn twice." - Anonymous diff --git a/Fritz.StreamTools/Services/FakeService.cs b/Fritz.StreamTools/Services/FakeService.cs index e4eca930..1b13fc32 100644 --- a/Fritz.StreamTools/Services/FakeService.cs +++ b/Fritz.StreamTools/Services/FakeService.cs @@ -1,149 +1,149 @@ -using Fritz.StreamLib.Core; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -namespace Fritz.StreamTools.Services -{ - public class FakeService : IHostedService, IStreamService - { - - IConfiguration _config; - - int _numberOfFollowers; - int _numberOfViewers; - int _updateViewersInterval; - int _updateFollowersInterval; - Timer _updateViewers; - Timer _updateFollowers; - - public string Name => "Fake"; - - public ILogger Logger { get; } - - public FakeService(IConfiguration config, ILoggerFactory loggerFactory) - { - - this._config = config; - this.Logger = loggerFactory.CreateLogger("StreamServices"); - - } - - public int CurrentFollowerCount => _numberOfFollowers; - - public int CurrentViewerCount => _numberOfViewers; - - public ValueTask Uptime() => new ValueTask((TimeSpan?)null); - - public event EventHandler Updated; - - public Task StartAsync(CancellationToken cancellationToken) - { - - _numberOfFollowers = int.Parse("0" + _config["StreamServices:Fake:CurrentFollowerCount"]); - _numberOfViewers = int.Parse("0" + _config["StreamServices:Fake:CurrentViewerCount"]); - _updateViewersInterval = int.Parse("0" + _config["StreamServices:Fake:UpdateViewersInterval"]); - _updateFollowersInterval = int.Parse("0" + _config["StreamServices:Fake:UpdateFollowersInterval"]); - - Logger.LogInformation($"Now monitoring Fake with {CurrentFollowerCount} followers and {CurrentViewerCount} Viewers"); - - SetupViewerUpdateTimer(); - SetupFollowerUpdateTimer(); - - return Task.CompletedTask; - - } - - private void SetupFollowerUpdateTimer() - { - if (_updateFollowersInterval == default(int)) - { - return; - } - - _updateFollowers = new Timer((o) => - { - - if (_numberOfFollowers >= 100) - { - _numberOfFollowers = 1; - } - - _numberOfFollowers++; - - Logger.LogInformation($"New Followers on Fake, new total: {_numberOfFollowers}"); - - Updated?.Invoke( - null, - new ServiceUpdatedEventArgs() - { - NewFollowers = _numberOfFollowers, - ServiceName = Name - }); - - }, - null, - 4000, - _updateFollowersInterval); - - } - - private void SetupViewerUpdateTimer() - { - if (_updateViewersInterval == default(int)) - { - return; - } - - _updateViewers = new Timer((o) => - { - - if (_numberOfViewers >= 100) - { - _numberOfViewers = 1; - } - - _numberOfViewers++; - - Logger.LogInformation($"New Viewers on Fake, new total: {_numberOfViewers}"); - - Updated?.Invoke( - null, - new ServiceUpdatedEventArgs() - { - NewViewers = _numberOfViewers, - ServiceName = Name - }); - - }, - null, - 5000, - _updateViewersInterval); - - } - - public Task StopAsync(CancellationToken cancellationToken) - { - - Logger.LogInformation($"Stopping monitoring Fake with {CurrentFollowerCount} followers and {CurrentViewerCount} Viewers"); - - if (_updateViewers != null) - { - _updateViewers.Dispose(); - } - - if (_updateFollowers != null) - { - _updateFollowers.Dispose(); - } - - return Task.CompletedTask; - - } - } -} +using Fritz.StreamLib.Core; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Fritz.StreamTools.Services +{ + public class FakeService : IHostedService, IStreamService + { + + IConfiguration _config; + + int _numberOfFollowers; + int _numberOfViewers; + int _updateViewersInterval; + int _updateFollowersInterval; + Timer _updateViewers; + Timer _updateFollowers; + + public string Name => "Fake"; + + public ILogger Logger { get; } + + public FakeService(IConfiguration config, ILoggerFactory loggerFactory) + { + + this._config = config; + this.Logger = loggerFactory.CreateLogger("StreamServices"); + + } + + public int CurrentFollowerCount => _numberOfFollowers; + + public int CurrentViewerCount => _numberOfViewers; + + public ValueTask Uptime() => new ValueTask((TimeSpan?)null); + + public event EventHandler Updated; + + public Task StartAsync(CancellationToken cancellationToken) + { + + _numberOfFollowers = int.Parse("0" + _config["StreamServices:Fake:CurrentFollowerCount"]); + _numberOfViewers = int.Parse("0" + _config["StreamServices:Fake:CurrentViewerCount"]); + _updateViewersInterval = int.Parse("0" + _config["StreamServices:Fake:UpdateViewersInterval"]); + _updateFollowersInterval = int.Parse("0" + _config["StreamServices:Fake:UpdateFollowersInterval"]); + + Logger.LogInformation($"Now monitoring Fake with {CurrentFollowerCount} followers and {CurrentViewerCount} Viewers"); + + SetupViewerUpdateTimer(); + SetupFollowerUpdateTimer(); + + return Task.CompletedTask; + + } + + private void SetupFollowerUpdateTimer() + { + if (_updateFollowersInterval == default(int)) + { + return; + } + + _updateFollowers = new Timer((o) => + { + + if (_numberOfFollowers >= 100) + { + _numberOfFollowers = 1; + } + + _numberOfFollowers++; + + Logger.LogInformation($"New Followers on Fake, new total: {_numberOfFollowers}"); + + Updated?.Invoke( + null, + new ServiceUpdatedEventArgs() + { + NewFollowers = _numberOfFollowers, + ServiceName = Name + }); + + }, + null, + 4000, + _updateFollowersInterval); + + } + + private void SetupViewerUpdateTimer() + { + if (_updateViewersInterval == default(int)) + { + return; + } + + _updateViewers = new Timer((o) => + { + + if (_numberOfViewers >= 100) + { + _numberOfViewers = 1; + } + + _numberOfViewers++; + + Logger.LogInformation($"New Viewers on Fake, new total: {_numberOfViewers}"); + + Updated?.Invoke( + null, + new ServiceUpdatedEventArgs() + { + NewViewers = _numberOfViewers, + ServiceName = Name + }); + + }, + null, + 5000, + _updateViewersInterval); + + } + + public Task StopAsync(CancellationToken cancellationToken) + { + + Logger.LogInformation($"Stopping monitoring Fake with {CurrentFollowerCount} followers and {CurrentViewerCount} Viewers"); + + if (_updateViewers != null) + { + _updateViewers.Dispose(); + } + + if (_updateFollowers != null) + { + _updateFollowers.Dispose(); + } + + return Task.CompletedTask; + + } + } +} diff --git a/Fritz.StreamTools/Services/StreamService.cs b/Fritz.StreamTools/Services/StreamService.cs index ee769f91..48e5f665 100644 --- a/Fritz.StreamTools/Services/StreamService.cs +++ b/Fritz.StreamTools/Services/StreamService.cs @@ -1,72 +1,72 @@ -using Fritz.StreamLib.Core; -using Microsoft.Extensions.DependencyInjection; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Fritz.StreamTools.Services -{ - - public class StreamService : IStreamService - { - - private IEnumerable _services; - - public StreamService( - IEnumerable services - ) - { - - _services = services; - - } - - public int CurrentFollowerCount { get { return _services.Sum(s => s.CurrentFollowerCount); } } - - public int CurrentViewerCount { get { return _services.Sum(s => s.CurrentViewerCount); } } - - public string Name { get { return "Aggregate"; } } - - public IEnumerable<(string service, int count)> ViewerCountByService - { - get - { - - return _services.Select(s => (s.Name, s.CurrentViewerCount)); - - } - } - - public IEnumerable<(string service, int count)> FollowerCountByService - { - get - { - - return _services.Select(s => (s.Name, s.CurrentFollowerCount)); - - } - } - - public ValueTask Uptime() => new ValueTask((TimeSpan?)null); - - public event EventHandler Updated - { - add - { - foreach (var s in _services) - { - s.Updated += value; - } - } - remove - { - foreach (var s in _services) - { - s.Updated -= value; - } - } - } - - } -} +using Fritz.StreamLib.Core; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Fritz.StreamTools.Services +{ + + public class StreamService : IStreamService + { + + private IEnumerable _services; + + public StreamService( + IEnumerable services + ) + { + + _services = services; + + } + + public int CurrentFollowerCount { get { return _services.Sum(s => s.CurrentFollowerCount); } } + + public int CurrentViewerCount { get { return _services.Sum(s => s.CurrentViewerCount); } } + + public string Name { get { return "Aggregate"; } } + + public IEnumerable<(string service, int count)> ViewerCountByService + { + get + { + + return _services.Select(s => (s.Name, s.CurrentViewerCount)); + + } + } + + public IEnumerable<(string service, int count)> FollowerCountByService + { + get + { + + return _services.Select(s => (s.Name, s.CurrentFollowerCount)); + + } + } + + public ValueTask Uptime() => new ValueTask((TimeSpan?)null); + + public event EventHandler Updated + { + add + { + foreach (var s in _services) + { + s.Updated += value; + } + } + remove + { + foreach (var s in _services) + { + s.Updated -= value; + } + } + } + + } +} diff --git a/Fritz.StreamTools/Services/TwitchService.cs b/Fritz.StreamTools/Services/TwitchService.cs index 8ce1527c..4f3c43a7 100644 --- a/Fritz.StreamTools/Services/TwitchService.cs +++ b/Fritz.StreamTools/Services/TwitchService.cs @@ -1,275 +1,277 @@ -using Fritz.StreamLib.Core; -using Fritz.Twitch; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using TwitchLib; -using TwitchLib.Events.Client; -using TwitchLib.Extensions.Client; -using TwitchLib.Models.API.v5.Streams; -using TwitchLib.Models.Client; -using TwitchLib.Services; - -namespace Fritz.StreamTools.Services -{ - - - public class TwitchService : IHostedService, IStreamService, IChatService - { - - private IConfiguration Configuration { get; } - public ILogger Logger { get; } - - private readonly Proxy Proxy; - - private ChatClient _ChatClient; - - public event EventHandler Updated; - public event EventHandler ChatMessage; - public event EventHandler UserJoined; - public event EventHandler UserLeft; - - public TwitchService(IConfiguration config, ILoggerFactory loggerFactory, Fritz.Twitch.Proxy proxy, Fritz.Twitch.ChatClient chatClient) - { - this.Configuration = config; - this.Logger = loggerFactory.CreateLogger("StreamServices"); - this.Proxy = proxy; - this._ChatClient = chatClient; - } - - public async Task StartAsync(CancellationToken cancellationToken) - { - await StartTwitchMonitoring(); - } - - public Task StopAsync(CancellationToken cancellationToken) - { - return StopTwitchMonitoring(); - } - - public static int _CurrentFollowerCount; - public int CurrentFollowerCount - { - get { return _CurrentFollowerCount; } - internal set { _CurrentFollowerCount = value; } - } - - public static int _CurrentViewerCount; - - public int CurrentViewerCount { get { return _CurrentViewerCount; } } - - public string Name { get { return "Twitch"; } } - - public ValueTask Uptime() => Proxy.Uptime(); - - public bool IsAuthenticated => true; - - private async Task StartTwitchMonitoring() - { - - _ChatClient.Connected += (c, args) => Logger.LogInformation("Now connected to Twitch Chat"); - _ChatClient.NewMessage += _ChatClient_NewMessage; - _ChatClient.UserJoined += _ChatClient_UserJoined; - _ChatClient.Init(); - - _CurrentFollowerCount = await Proxy.GetFollowerCountAsync(); - Proxy.NewFollowers += Proxy_NewFollowers; - Proxy.WatchFollowers(10000); - - _CurrentViewerCount = await Proxy.GetViewerCountAsync(); - Proxy.NewViewers += Proxy_NewViewers; - Proxy.WatchViewers(); - - Logger.LogInformation($"Now monitoring Twitch with {_CurrentFollowerCount} followers and {_CurrentViewerCount} Viewers"); - - } - - private void _ChatClient_UserJoined(object sender, ChatUserJoinedEventArgs e) - { - UserJoined?.Invoke(this, new ChatUserInfoEventArgs - { - ServiceName = "Twitch", - UserName = e.UserName - }); - } - - private void _ChatClient_NewMessage(object sender, NewMessageEventArgs e) - { - - ChatMessage?.Invoke(this, new ChatMessageEventArgs - { - IsModerator = false, - IsOwner = (_ChatClient.ChannelName == e.UserName), - IsWhisper = e.IsWhisper, - Message = e.Message, - ServiceName = "Twitch", - UserName = e.UserName - }); - } - - private void Proxy_NewViewers(object sender, NewViewersEventArgs e) - { - Interlocked.Exchange(ref _CurrentViewerCount, e.ViewerCount); - Logger.LogInformation($"New Viewers on Twitch, new total: {_CurrentViewerCount}"); - - Updated?.Invoke(this, new ServiceUpdatedEventArgs - { - ServiceName = Name, - NewViewers = _CurrentViewerCount - }); - } - - private void Proxy_NewFollowers(object sender, NewFollowersEventArgs e) - { - Interlocked.Exchange(ref _CurrentFollowerCount, e.FollowerCount); - Logger.LogInformation($"New Followers on Twitch, new total: {_CurrentFollowerCount}"); - - Updated?.Invoke(this, new ServiceUpdatedEventArgs - { - ServiceName = Name, - NewFollowers = _CurrentFollowerCount - }); - } - - private void _TwitchClient_OnConnected(object sender, OnConnectedArgs e) - { - Logger.LogInformation("Now connected to Twitch Chat Room"); - } - - private void _TwitchClient_OnWhisperReceived(object sender, OnWhisperReceivedArgs e) - { - ChatMessage?.Invoke(this, new ChatMessageEventArgs - { - IsModerator = false, - IsOwner = false, - IsWhisper = true, - Message = e.WhisperMessage.Message, - ServiceName = "Twitch", - UserName = e.WhisperMessage.Username - }); - } - - private void _TwitchClient_OnUserLeft(object sender, OnUserLeftArgs e) - { - - UserLeft?.Invoke(this, new ChatUserInfoEventArgs - { - ChannelId = 0, - ServiceName = "Twitch", - UserId = 0, - UserName = e.Username - }); - - } - - private void _TwitchClient_OnUserJoined(object sender, OnUserJoinedArgs e) - { - - UserJoined?.Invoke(this, new ChatUserInfoEventArgs - { - ServiceName = "Twitch", - UserName = e.Username - }); - - } - - private void _TwitchClient_OnMessageReceived(object sender, OnMessageReceivedArgs e) - { - - ChatMessage?.Invoke(this, new ChatMessageEventArgs - { - IsModerator = e.ChatMessage.IsModerator, - IsOwner = e.ChatMessage.IsBroadcaster, - IsWhisper = false, - Message = e.ChatMessage.Message, - ServiceName = "Twitch", - UserName = e.ChatMessage.Username - }); - - } - - internal void Service_OnNewFollowersDetected(object sender, - TwitchLib.Events.Services.FollowerService.OnNewFollowersDetectedArgs e) - { - Interlocked.Exchange(ref _CurrentFollowerCount, _CurrentFollowerCount + e.NewFollowers.Count); - Logger.LogInformation($"New Followers on Twitch, new total: {_CurrentFollowerCount}"); - - Updated?.Invoke(this, new ServiceUpdatedEventArgs - { - ServiceName = Name, - NewFollowers = _CurrentFollowerCount - }); - } - - private Task StopTwitchMonitoring() - { - - Proxy.Dispose(); - _ChatClient.Dispose(); - - return Task.CompletedTask; - } - - public Task SendMessageAsync(string message) - { - _ChatClient.PostMessage(message); - return Task.FromResult(true); - } - - public Task SendWhisperAsync(string userName, string message) - { - - _ChatClient.WhisperMessage(message, userName); - return Task.FromResult(true); - - } - - public Task TimeoutUserAsync(string userName, TimeSpan time) - { - - //_TwitchClient.TimeoutUser(userName, time); - //return Task.FromResult(true); - return Task.FromResult(false); - - - } - - public Task BanUserAsync(string userName) - { - //_TwitchClient.BanUser(userName); - //return Task.FromResult(true); - return Task.FromResult(false); - - } - - public Task UnbanUserAsync(string userName) - { - //_TwitchClient.UnbanUser(userName); - //return Task.FromResult(true); - return Task.FromResult(false); - } - - internal void MessageReceived(bool isModerator, bool isBroadcaster, string message, string userName) - { - ChatMessage?.Invoke(this, new ChatMessageEventArgs - { - IsModerator = isModerator, - IsOwner = isBroadcaster, - IsWhisper = false, - Message = message, - ServiceName = "Twitch", - UserName = userName - }); - - } - - } - -} +using Fritz.StreamLib.Core; +using Fritz.Twitch; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using TwitchLib; +using TwitchLib.Events.Client; +using TwitchLib.Extensions.Client; +using TwitchLib.Models.API.v5.Streams; +using TwitchLib.Models.Client; +using TwitchLib.Services; + +namespace Fritz.StreamTools.Services +{ + + + public class TwitchService : IHostedService, IStreamService, IChatService + { + + private IConfiguration Configuration { get; } + public ILogger Logger { get; } + + private readonly Proxy Proxy; + + private ChatClient _ChatClient; + + public event EventHandler Updated; + public event EventHandler ChatMessage; + public event EventHandler UserJoined; + public event EventHandler UserLeft; + + public TwitchService(IConfiguration config, ILoggerFactory loggerFactory, Fritz.Twitch.Proxy proxy, Fritz.Twitch.ChatClient chatClient) + { + this.Configuration = config; + this.Logger = loggerFactory.CreateLogger("StreamServices"); + this.Proxy = proxy; + this._ChatClient = chatClient; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + await StartTwitchMonitoring(); + } + + public Task StopAsync(CancellationToken cancellationToken) + { + return StopTwitchMonitoring(); + } + + public static int _CurrentFollowerCount; + public int CurrentFollowerCount + { + get { return _CurrentFollowerCount; } + internal set { _CurrentFollowerCount = value; } + } + + public static int _CurrentViewerCount; + + public int CurrentViewerCount { get { return _CurrentViewerCount; } } + + public string Name { get { return "Twitch"; } } + + public ValueTask Uptime() => Proxy.Uptime(); + + public bool IsAuthenticated => true; + + private async Task StartTwitchMonitoring() + { + + _ChatClient.Connected += (c, args) => Logger.LogInformation("Now connected to Twitch Chat"); + _ChatClient.NewMessage += _ChatClient_NewMessage; + _ChatClient.UserJoined += _ChatClient_UserJoined; + _ChatClient.Init(); + + _CurrentFollowerCount = await Proxy.GetFollowerCountAsync(); + Proxy.NewFollowers += Proxy_NewFollowers; + Proxy.WatchFollowers(10000); + + _CurrentViewerCount = await Proxy.GetViewerCountAsync(); + Proxy.NewViewers += Proxy_NewViewers; + Proxy.WatchViewers(); + + Logger.LogInformation($"Now monitoring Twitch with {_CurrentFollowerCount} followers and {_CurrentViewerCount} Viewers"); + + } + + private void _ChatClient_UserJoined(object sender, ChatUserJoinedEventArgs e) + { + UserJoined?.Invoke(this, new ChatUserInfoEventArgs + { + ServiceName = "Twitch", + UserName = e.UserName + }); + } + + private void _ChatClient_NewMessage(object sender, NewMessageEventArgs e) + { + + ChatMessage?.Invoke(this, new ChatMessageEventArgs + { + IsModerator = false, + IsOwner = (_ChatClient.ChannelName == e.UserName), + IsWhisper = e.IsWhisper, + Message = e.Message, + ServiceName = "Twitch", + UserName = e.UserName + }); + } + + private void Proxy_NewViewers(object sender, NewViewersEventArgs e) + { + Interlocked.Exchange(ref _CurrentViewerCount, e.ViewerCount); + Logger.LogInformation($"New Viewers on Twitch, new total: {_CurrentViewerCount}"); + + Updated?.Invoke(this, new ServiceUpdatedEventArgs + { + ServiceName = Name, + NewViewers = _CurrentViewerCount + }); + } + + private void Proxy_NewFollowers(object sender, NewFollowersEventArgs e) + { + Interlocked.Exchange(ref _CurrentFollowerCount, e.FollowerCount); + Logger.LogInformation($"New Followers on Twitch, new total: {_CurrentFollowerCount}"); + + Updated?.Invoke(this, new ServiceUpdatedEventArgs + { + ServiceName = Name, + NewFollowers = _CurrentFollowerCount + }); + } + + private void _TwitchClient_OnConnected(object sender, OnConnectedArgs e) + { + Logger.LogInformation("Now connected to Twitch Chat Room"); + } + + private void _TwitchClient_OnWhisperReceived(object sender, OnWhisperReceivedArgs e) + { + ChatMessage?.Invoke(this, new ChatMessageEventArgs + { + IsModerator = false, + IsOwner = false, + IsWhisper = true, + Message = e.WhisperMessage.Message, + ServiceName = "Twitch", + UserName = e.WhisperMessage.Username + }); + } + + private void _TwitchClient_OnUserLeft(object sender, OnUserLeftArgs e) + { + + UserLeft?.Invoke(this, new ChatUserInfoEventArgs + { + ChannelId = 0, + ServiceName = "Twitch", + UserId = 0, + UserName = e.Username + }); + + } + + private void _TwitchClient_OnUserJoined(object sender, OnUserJoinedArgs e) + { + + UserJoined?.Invoke(this, new ChatUserInfoEventArgs + { + ServiceName = "Twitch", + UserName = e.Username + }); + + } + + private void _TwitchClient_OnMessageReceived(object sender, OnMessageReceivedArgs e) + { + + ChatMessage?.Invoke(this, new ChatMessageEventArgs + { + IsModerator = e.ChatMessage.IsModerator, + IsOwner = e.ChatMessage.IsBroadcaster, + IsWhisper = false, + Message = e.ChatMessage.Message, + ServiceName = "Twitch", + UserName = e.ChatMessage.Username + }); + + } + + internal void Service_OnNewFollowersDetected(object sender, + TwitchLib.Events.Services.FollowerService.OnNewFollowersDetectedArgs e) + { + Interlocked.Exchange(ref _CurrentFollowerCount, _CurrentFollowerCount + e.NewFollowers.Count); + Logger.LogInformation($"New Followers on Twitch, new total: {_CurrentFollowerCount}"); + + Updated?.Invoke(this, new ServiceUpdatedEventArgs + { + ServiceName = Name, + NewFollowers = _CurrentFollowerCount + }); + } + + private Task StopTwitchMonitoring() + { + + Proxy.Dispose(); + _ChatClient.Dispose(); + + return Task.CompletedTask; + } + + public async Task SendMessageAsync(string message) + { + + await _ChatClient.PostMessageAsync(message); + return true; + + } + + public async Task SendWhisperAsync(string userName, string message) + { + + await _ChatClient.WhisperMessageAsync(message, userName); + return true; + + } + + public Task TimeoutUserAsync(string userName, TimeSpan time) + { + + //_TwitchClient.TimeoutUser(userName, time); + //return Task.FromResult(true); + return Task.FromResult(false); + + + } + + public Task BanUserAsync(string userName) + { + //_TwitchClient.BanUser(userName); + //return Task.FromResult(true); + return Task.FromResult(false); + + } + + public Task UnbanUserAsync(string userName) + { + //_TwitchClient.UnbanUser(userName); + //return Task.FromResult(true); + return Task.FromResult(false); + } + + internal void MessageReceived(bool isModerator, bool isBroadcaster, string message, string userName) + { + ChatMessage?.Invoke(this, new ChatMessageEventArgs + { + IsModerator = isModerator, + IsOwner = isBroadcaster, + IsWhisper = false, + Message = message, + ServiceName = "Twitch", + UserName = userName + }); + + } + + } + +} diff --git a/Fritz.StreamTools/StartupServices/ConfigureServices.cs b/Fritz.StreamTools/StartupServices/ConfigureServices.cs index 35fc9378..7c8dc1d9 100644 --- a/Fritz.StreamTools/StartupServices/ConfigureServices.cs +++ b/Fritz.StreamTools/StartupServices/ConfigureServices.cs @@ -1,109 +1,109 @@ -using System; -using System.IO; -using System.Linq; -using Fritz.StreamLib.Core; -using Fritz.StreamTools.Hubs; -using Fritz.StreamTools.Models; -using Fritz.StreamTools.Services; -using Fritz.StreamTools.TagHelpers; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using MixerLib; - -namespace Fritz.StreamTools.StartupServices -{ - public static class ConfigureServices - { - public static void Execute( - IServiceCollection services, - IConfiguration configuration) - { - services.AddSingleton(); - services.Configure(configuration.GetSection("FollowerGoal")); - services.Configure(configuration.GetSection("FollowerCount")); - services.AddStreamingServices(configuration); - services.AddSingleton(); - services.AddAspNetFeatures(); - - services.AddSingleton, ConfigureSignalrTagHelperOptions>(); - services.AddSingleton(cfg => cfg.GetService>().Value); - - services.AddSingleton(); - } - - private static void AddStreamingServices(this IServiceCollection services, - IConfiguration configuration) - { - - services.Configure(configuration.GetSection("StreamServices:Twitch")); - - var provider = services.BuildServiceProvider(); - - services.AddStreamService(configuration, - (c, l) => new TwitchService(c, l, provider.GetService(), provider.GetService()), - c => string.IsNullOrEmpty(c["StreamServices:Twitch:ClientId"])); // Test to disable - services.AddStreamService(configuration, - (c, l) => new MixerService(c, l), // Factory - c => string.IsNullOrEmpty(c["StreamServices:Mixer:Channel"])); // Test to disable - services.AddStreamService(configuration, - (c, l) => new FakeService(c, l), // Factory - c => !bool.TryParse(c["StreamServices:Fake:Enabled"], out var enabled) || !enabled); // Test to disable - - services.AddSingleton(); - } - - /// - /// Generically configure stream service providers and register them with the DI container - /// - /// Type of StreamService to create - /// The DI services configuration object - /// Application Configuration to use to populate our service - /// Callback method that defines how to instantiate the service - /// Callback test to determine whether to disable the service - private static void AddStreamService(this IServiceCollection services, - IConfiguration configuration, - Func factory, - Func isDisabled) - where TStreamService : class, IStreamService - { - - // Don't configure this service if it is disabled - if (isDisabled(configuration)) - { - return; - } - - // Configure and grab a logger so that we can log information - // about the creation of the services - var provider = services.BuildServiceProvider(); // Build a 'temporary' instance of the DI container - var loggerFactory = provider.GetService(); - - var service = factory(configuration, loggerFactory); - - services.AddSingleton(service as IHostedService); - services.AddSingleton(service as IStreamService); - services.AddSingleton(service); - - if (service is IChatService chatService) - { - services.AddSingleton(chatService); - } - } - - /// - /// Configure the standard ASP.NET Core services - /// - /// - private static void AddAspNetFeatures(this IServiceCollection services) - { - services.AddSignalR(); - services.AddSingleton(); - services.AddMvc(); - } - - } -} +using System; +using System.IO; +using System.Linq; +using Fritz.StreamLib.Core; +using Fritz.StreamTools.Hubs; +using Fritz.StreamTools.Models; +using Fritz.StreamTools.Services; +using Fritz.StreamTools.TagHelpers; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using MixerLib; + +namespace Fritz.StreamTools.StartupServices +{ + public static class ConfigureServices + { + public static void Execute( + IServiceCollection services, + IConfiguration configuration) + { + services.AddSingleton(); + services.Configure(configuration.GetSection("FollowerGoal")); + services.Configure(configuration.GetSection("FollowerCount")); + services.AddStreamingServices(configuration); + services.AddSingleton(); + services.AddAspNetFeatures(); + + services.AddSingleton, ConfigureSignalrTagHelperOptions>(); + services.AddSingleton(cfg => cfg.GetService>().Value); + + services.AddSingleton(); + } + + private static void AddStreamingServices(this IServiceCollection services, + IConfiguration configuration) + { + + services.Configure(configuration.GetSection("StreamServices:Twitch")); + + var provider = services.BuildServiceProvider(); + + services.AddStreamService(configuration, + (c, l) => new TwitchService(c, l, provider.GetService(), provider.GetService()), + c => string.IsNullOrEmpty(c["StreamServices:Twitch:ClientId"])); // Test to disable + services.AddStreamService(configuration, + (c, l) => new MixerService(c, l), // Factory + c => string.IsNullOrEmpty(c["StreamServices:Mixer:Channel"])); // Test to disable + services.AddStreamService(configuration, + (c, l) => new FakeService(c, l), // Factory + c => !bool.TryParse(c["StreamServices:Fake:Enabled"], out var enabled) || !enabled); // Test to disable + + services.AddSingleton(); + } + + /// + /// Generically configure stream service providers and register them with the DI container + /// + /// Type of StreamService to create + /// The DI services configuration object + /// Application Configuration to use to populate our service + /// Callback method that defines how to instantiate the service + /// Callback test to determine whether to disable the service + private static void AddStreamService(this IServiceCollection services, + IConfiguration configuration, + Func factory, + Func isDisabled) + where TStreamService : class, IStreamService + { + + // Don't configure this service if it is disabled + if (isDisabled(configuration)) + { + return; + } + + // Configure and grab a logger so that we can log information + // about the creation of the services + var provider = services.BuildServiceProvider(); // Build a 'temporary' instance of the DI container + var loggerFactory = provider.GetService(); + + var service = factory(configuration, loggerFactory); + + services.AddSingleton(service as IHostedService); + services.AddSingleton(service as IStreamService); + services.AddSingleton(service); + + if (service is IChatService chatService) + { + services.AddSingleton(chatService); + } + } + + /// + /// Configure the standard ASP.NET Core services + /// + /// + private static void AddAspNetFeatures(this IServiceCollection services) + { + services.AddSignalR(); + services.AddSingleton(); + services.AddMvc(); + } + + } +} diff --git a/Fritz.StreamTools/Views/Followers/Count.cshtml b/Fritz.StreamTools/Views/Followers/Count.cshtml index b7674916..dd3f08b1 100644 --- a/Fritz.StreamTools/Views/Followers/Count.cshtml +++ b/Fritz.StreamTools/Views/Followers/Count.cshtml @@ -1,63 +1,63 @@ -@model FollowerCountConfiguration -@{ - Layout = null; -} - - - - - - Follower Count - - - - - Followers: @Model.CurrentValue - - - - - - - +@model FollowerCountConfiguration +@{ + Layout = null; +} + + + + + + Follower Count + + + + + Followers: @Model.CurrentValue + + + + + + + diff --git a/Fritz.StreamTools/Views/Followers/Goal.cshtml b/Fritz.StreamTools/Views/Followers/Goal.cshtml index db49f9fd..546a487c 100644 --- a/Fritz.StreamTools/Views/Followers/Goal.cshtml +++ b/Fritz.StreamTools/Views/Followers/Goal.cshtml @@ -1,107 +1,107 @@ -@model FollowerGoalConfiguration -@{ - Layout = null; -} - - - - - - Followers Goal - @Model.FontName - - - - - -
-
- @Model.Caption - @Model.CurrentValue - @Model.Goal -
-
-
- @Model.Caption - @Model.CurrentValue - @Model.Goal -
- - - - - - - +@model FollowerGoalConfiguration +@{ + Layout = null; +} + + + + + + Followers Goal + @Model.FontName + + + + + +
+
+ @Model.Caption + @Model.CurrentValue + @Model.Goal +
+
+
+ @Model.Caption + @Model.CurrentValue + @Model.Goal +
+ + + + + + + diff --git a/Fritz.StreamTools/bundleconfig.json b/Fritz.StreamTools/bundleconfig.json index cfd92db9..ecdad276 100644 --- a/Fritz.StreamTools/bundleconfig.json +++ b/Fritz.StreamTools/bundleconfig.json @@ -1,39 +1,39 @@ -[ - { - "outputFileName": "wwwroot/css/site.min.css", - "inputFiles": [ - "wwwroot/css/site.css" - ] - }, - { - "outputFileName": "wwwroot/js/site.min.js", - "inputFiles": [ - "wwwroot/js/site.js" - ], - "minify": { - "enabled": false - }, - "sourceMap": false - }, - { - "outputFileName": "wwwroot/js/GoalConfiguration.min.js", - "inputFiles": [ - "wwwroot/js/GoalConfiguration/GoogleFonts.js", - "wwwroot/js/GoalConfiguration/Preview.js" - ,"wwwroot/js/GoalConfiguration/GoalConfiguration.js" - ], - "minify": { - "enabled": true - }, - "sourceMap": false - }, - { - "outputFileName": "wwwroot/lib/signalr/signalr-client.js", - "inputFiles": [ - "node_modules/@aspnet/signalr/dist/browser/signalr.min.js" - ], - "minify": { - "enabled": false - } - } -] +[ + { + "outputFileName": "wwwroot/css/site.min.css", + "inputFiles": [ + "wwwroot/css/site.css" + ] + }, + { + "outputFileName": "wwwroot/js/site.min.js", + "inputFiles": [ + "wwwroot/js/site.js" + ], + "minify": { + "enabled": false + }, + "sourceMap": false + }, + { + "outputFileName": "wwwroot/js/GoalConfiguration.min.js", + "inputFiles": [ + "wwwroot/js/GoalConfiguration/GoogleFonts.js", + "wwwroot/js/GoalConfiguration/Preview.js" + ,"wwwroot/js/GoalConfiguration/GoalConfiguration.js" + ], + "minify": { + "enabled": true + }, + "sourceMap": false + }, + { + "outputFileName": "wwwroot/lib/signalr/signalr-client.js", + "inputFiles": [ + "node_modules/@aspnet/signalr/dist/browser/signalr.min.js" + ], + "minify": { + "enabled": false + } + } +] diff --git a/Fritz.StreamTools/wwwroot/js/streamhub.js b/Fritz.StreamTools/wwwroot/js/streamhub.js index 26632343..069a36be 100644 --- a/Fritz.StreamTools/wwwroot/js/streamhub.js +++ b/Fritz.StreamTools/wwwroot/js/streamhub.js @@ -1,40 +1,40 @@ - -class StreamHub { - constructor() { - this.onFollowers = null; - this.onViewers = null; - this.debug = true; - this._hub = null; - } - - start(groups) { - let url = (groups) ? "/followerstream?groups=" + groups : "/followerstream"; - this._hub = new signalR.HubConnection(url); - - this._hub.onclose(() => { - if (this.debug) console.debug("hub connection closed"); - - // Hub connection was closed for some reason - let interval = setInterval(() => { - // Try to reconnect hub every 5 secs - this.start(groups).then(() => { - // Reconnect succeeded - clearInterval(interval); - if (this.debug) console.debug("hub reconnected"); - }); - }, 5000); - }); - - this._hub.on('OnFollowersCountUpdated', followerCount => { - if (this.debug) console.debug("OnFollowersCountUpdated", { followerCount }); - if (this.onFollowers) this.onFollowers(followerCount); - }); - this._hub.on('OnViewersCountUpdated', (serviceName, viewerCount) => { - if (this.debug) console.debug("OnViewersCountUpdated", { serviceName, viewerCount }); - if (this.onViewers) this.onViewers(serviceName, viewerCount); - }); - - return this._hub.start(); - - } -} + +class StreamHub { + constructor() { + this.onFollowers = null; + this.onViewers = null; + this.debug = true; + this._hub = null; + } + + start(groups) { + let url = (groups) ? "/followerstream?groups=" + groups : "/followerstream"; + this._hub = new signalR.HubConnection(url); + + this._hub.onclose(() => { + if (this.debug) console.debug("hub connection closed"); + + // Hub connection was closed for some reason + let interval = setInterval(() => { + // Try to reconnect hub every 5 secs + this.start(groups).then(() => { + // Reconnect succeeded + clearInterval(interval); + if (this.debug) console.debug("hub reconnected"); + }); + }, 5000); + }); + + this._hub.on('OnFollowersCountUpdated', followerCount => { + if (this.debug) console.debug("OnFollowersCountUpdated", { followerCount }); + if (this.onFollowers) this.onFollowers(followerCount); + }); + this._hub.on('OnViewersCountUpdated', (serviceName, viewerCount) => { + if (this.debug) console.debug("OnViewersCountUpdated", { serviceName, viewerCount }); + if (this.onViewers) this.onViewers(serviceName, viewerCount); + }); + + return this._hub.start(); + + } +} diff --git a/Fritz.StreamTools/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.ttf b/Fritz.StreamTools/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.ttf index 1413fc60..1f853124 100644 Binary files a/Fritz.StreamTools/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.ttf and b/Fritz.StreamTools/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.ttf differ diff --git a/Fritz.Twitch/ChatClient.cs b/Fritz.Twitch/ChatClient.cs index 721b95f8..2709dedb 100644 --- a/Fritz.Twitch/ChatClient.cs +++ b/Fritz.Twitch/ChatClient.cs @@ -15,311 +15,317 @@ namespace Fritz.Twitch { - public class ChatClient : IDisposable - { + public class ChatClient : IDisposable + { - public const string LOGGER_CATEGORY = "Fritz.TwitchChat"; - private TcpClient _TcpClient; - private StreamReader inputStream; - private StreamWriter outputStream; - private int _Retries; - private Task _ReceiveMassagesTask; - private MemoryStream _ReceiveStream = new MemoryStream(); + public const string LOGGER_CATEGORY = "Fritz.TwitchChat"; + private TcpClient _TcpClient; + private StreamReader inputStream; + private StreamWriter outputStream; + private int _Retries; + private Task _ReceiveMassagesTask; + private MemoryStream _ReceiveStream = new MemoryStream(); - internal static readonly Regex reUserName = new Regex(@"!([^@]+)@"); - internal static Regex reChatMessage; - internal static Regex reWhisperMessage; + internal static readonly Regex reUserName = new Regex(@"!([^@]+)@"); + internal static Regex reChatMessage; + internal static Regex reWhisperMessage; - public event EventHandler Connected; - public event EventHandler NewMessage; - public event EventHandler UserJoined; + public event EventHandler Connected; + public event EventHandler NewMessage; + public event EventHandler UserJoined; - private DateTime _NextReset; - private int _RemainingThrottledCommands; - // private static readonly ReaderWriterLockSlim _ + private DateTime _NextReset; + private int _RemainingThrottledCommands; + // private static readonly ReaderWriterLockSlim _ - public ChatClient(IOptions settings, ILoggerFactory loggerFactory) : this(settings.Value, loggerFactory.CreateLogger(LOGGER_CATEGORY)) - { + public ChatClient(IOptions settings, ILoggerFactory loggerFactory) : this(settings.Value, loggerFactory.CreateLogger(LOGGER_CATEGORY)) + { - } + } - internal ChatClient(ConfigurationSettings settings, ILogger logger) - { + internal ChatClient(ConfigurationSettings settings, ILogger logger) + { - this.Settings = settings; - this.Logger = logger; + this.Settings = settings; + this.Logger = logger; - reChatMessage = new Regex($@"PRIVMSG #{Settings.ChannelName} :(.*)$"); - reWhisperMessage = new Regex($@"WHISPER {Settings.ChatBotName} :(.*)$"); + reChatMessage = new Regex($@"PRIVMSG #{Settings.ChannelName} :(.*)$"); + reWhisperMessage = new Regex($@"WHISPER {Settings.ChatBotName} :(.*)$"); - _Shutdown = new CancellationTokenSource(); + _Shutdown = new CancellationTokenSource(); - } + } - ~ChatClient() - { + ~ChatClient() + { - Logger.LogError("GC the ChatClient"); + Logger.LogError("GC the ChatClient"); - // Do not change this code. Put cleanup code in Dispose(bool disposing) above. - Dispose(false); - } + // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + Dispose(false); + } - public void Init() - { + public void Init() + { - Connect(); + Connect(); + var receiveTask = Task.Run(async () => await ReceiveMessagesOnThread()); + } - _ReceiveMessagesThread = new Thread(ReceiveMessagesOnThread); - _ReceiveMessagesThread.Start(); + public ConfigurationSettings Settings { get; } + public ILogger Logger { get; } - } + public string ChannelName => Settings.ChannelName; - public ConfigurationSettings Settings { get; } - public ILogger Logger { get; } + private readonly CancellationTokenSource _Shutdown; - public string ChannelName => Settings.ChannelName; + private void Connect() + { - private readonly CancellationTokenSource _Shutdown; + _TcpClient = new TcpClient("irc.chat.twitch.tv", 80); - private void Connect() - { + inputStream = new StreamReader(_TcpClient.GetStream()); + outputStream = new StreamWriter(_TcpClient.GetStream()); - _TcpClient = new TcpClient("irc.chat.twitch.tv", 80); + Logger.LogTrace("Beginning IRC authentication to Twitch"); + outputStream.WriteLine("CAP REQ :twitch.tv/tags twitch.tv/commands twitch.tv/membership"); + outputStream.WriteLine($"PASS oauth:{Settings.OAuthToken}"); + outputStream.WriteLine($"NICK {Settings.ChatBotName}"); + outputStream.WriteLine($"USER {Settings.ChatBotName} 8 * :{Settings.ChatBotName}"); + outputStream.Flush(); - inputStream = new StreamReader(_TcpClient.GetStream()); - outputStream = new StreamWriter(_TcpClient.GetStream()); + outputStream.WriteLine($"JOIN #{Settings.ChannelName}"); + outputStream.Flush(); - Logger.LogTrace("Beginning IRC authentication to Twitch"); - outputStream.WriteLine("CAP REQ :twitch.tv/tags twitch.tv/commands twitch.tv/membership"); - outputStream.WriteLine($"PASS oauth:{Settings.OAuthToken}"); - outputStream.WriteLine($"NICK {Settings.ChatBotName}"); - outputStream.WriteLine($"USER {Settings.ChatBotName} 8 * :{Settings.ChatBotName}"); - outputStream.Flush(); + Connected?.Invoke(this, new ChatConnectedEventArgs()); - outputStream.WriteLine($"JOIN #{Settings.ChannelName}"); - outputStream.Flush(); + } - Connected?.Invoke(this, new ChatConnectedEventArgs()); + private async Task SendMessageAsync(string message, bool flush = true) + { - } + var throttled = CheckThrottleStatus(); - private void SendMessage(string message, bool flush = true) - { + await Task.Delay(throttled.GetValueOrDefault(TimeSpan.FromSeconds(0))); - var throttled = CheckThrottleStatus(); + await outputStream.WriteLineAsync(message); + if (flush) + { + await outputStream.FlushAsync(); + } - Thread.Sleep(throttled.GetValueOrDefault(TimeSpan.FromSeconds(0))); + } - outputStream.WriteLine(message); - if (flush) - { - outputStream.Flush(); - } + private TimeSpan? CheckThrottleStatus() + { - } + var throttleDuration = TimeSpan.FromSeconds(30); + var maximumCommands = 100; - private TimeSpan? CheckThrottleStatus() - { + if (_NextReset == null) + { + _NextReset = DateTime.UtcNow.Add(throttleDuration); + } + else if (_NextReset < DateTime.UtcNow) + { + _NextReset = DateTime.UtcNow.Add(throttleDuration); + } - var throttleDuration = TimeSpan.FromSeconds(30); - var maximumCommands = 100; + // TODO: FInish checking and enforcing the chat throttling - if (_NextReset == null) - { - _NextReset = DateTime.UtcNow.Add(throttleDuration); - } else if (_NextReset < DateTime.UtcNow) - { - _NextReset = DateTime.UtcNow.Add(throttleDuration); - } + return null; - // TODO: FInish checking and enforcing the chat throttling - return null; + } + /// + /// Public async interface to post messages to channel + /// + /// + public async Task PostMessageAsync(string message) + { - } + var fullMessage = $":{Settings.ChatBotName}!{Settings.ChatBotName}@{Settings.ChatBotName}.tmi.twitch.tv PRIVMSG #{Settings.ChannelName} :{message}"; - /// - /// Public async interface to post messages to channel - /// - /// - public void PostMessage(string message) - { + await SendMessageAsync(fullMessage); - var fullMessage = $":{Settings.ChatBotName}!{Settings.ChatBotName}@{Settings.ChatBotName}.tmi.twitch.tv PRIVMSG #{Settings.ChannelName} :{message}"; + } - SendMessage(fullMessage); + public Task WhisperMessageAsync(string message, string userName) + { + if (string.IsNullOrWhiteSpace(userName)) + throw new ArgumentException(nameof(userName), "Missing username"); + return localMethod(); - } + async Task localMethod() + { + var fullMessage = $":{Settings.ChatBotName}!{Settings.ChatBotName}@{Settings.ChatBotName}.tmi.twitch.tv PRIVMSG #jtv :/w {userName} {message}"; + await SendMessageAsync(fullMessage); + } - public void WhisperMessage(string message, string userName) - { + } - var fullMessage = $":{Settings.ChatBotName}!{Settings.ChatBotName}@{Settings.ChatBotName}.tmi.twitch.tv PRIVMSG #jtv :/w {userName} {message}"; - SendMessage(fullMessage); + private async Task ReceiveMessagesOnThread() + { - } + var lastMessageReceivedTimestamp = DateTime.Now; + var errorPeriod = TimeSpan.FromSeconds(60); + await Task.Delay(1).ConfigureAwait(continueOnCapturedContext: false); - private void ReceiveMessagesOnThread() - { + while (true) + { + if (DateTime.Now.Subtract(lastMessageReceivedTimestamp) > errorPeriod) + { + Logger.LogError($"Haven't received a message in {errorPeriod.TotalSeconds} seconds"); + lastMessageReceivedTimestamp = DateTime.Now; + } - var lastMessageReceivedTimestamp = DateTime.Now; - var errorPeriod = TimeSpan.FromSeconds(60); + if (_Shutdown.IsCancellationRequested) + { + break; + } - while (true) - { + if (_TcpClient.Connected && _TcpClient.Available > 0) + { - Thread.Sleep(50); + var msg = await ReadMessageAsync(); + if (string.IsNullOrEmpty(msg)) + { + continue; + } - if (DateTime.Now.Subtract(lastMessageReceivedTimestamp) > errorPeriod) - { - Logger.LogError($"Haven't received a message in {errorPeriod.TotalSeconds} seconds"); - lastMessageReceivedTimestamp = DateTime.Now; - } + lastMessageReceivedTimestamp = DateTime.Now; + Logger.LogTrace($"> {msg}"); - if (_Shutdown.IsCancellationRequested) - { - break; - } + // Handle the Twitch keep-alive + if (msg.StartsWith("PING")) + { + Logger.LogWarning("Received PING from Twitch... sending PONG"); + await SendMessageAsync($"PONG :{msg.Split(':')[1]}"); + continue; + } - if (_TcpClient.Connected && _TcpClient.Available > 0) - { + ProcessMessage(msg); - var msg = ReadMessage(); - if (string.IsNullOrEmpty(msg)) - { - continue; - } + } + else if (!_TcpClient.Connected) + { + // Reconnect + Logger.LogWarning("Disconnected from Twitch.. Reconnecting in 2 seconds"); + await Task.Delay(2000); + this.Init(); + return; + } - lastMessageReceivedTimestamp = DateTime.Now; - Logger.LogTrace($"> {msg}"); + } - // Handle the Twitch keep-alive - if (msg.StartsWith("PING")) - { - Logger.LogWarning("Received PING from Twitch... sending PONG"); - SendMessage($"PONG :{msg.Split(':')[1]}"); - continue; - } + Logger.LogWarning("Exiting ReceiveMessages Loop"); - ProcessMessage(msg); + } - } else if (!_TcpClient.Connected) - { - // Reconnect - Logger.LogWarning("Disconnected from Twitch.. Reconnecting in 2 seconds"); - Thread.Sleep(2000); - this.Init(); - return; - } + private void ProcessMessage(string msg) + { - } + // Logger.LogTrace("Processing message: " + msg); - Logger.LogWarning("Exiting ReceiveMessages Loop"); + var userName = ""; + var message = ""; - } + userName = ChatClient.reUserName.Match(msg).Groups[1].Value; - private void ProcessMessage(string msg) - { + if (!string.IsNullOrEmpty(userName) && msg.Contains($" JOIN #{ChannelName}")) + { + UserJoined?.Invoke(this, new ChatUserJoinedEventArgs { UserName = userName }); + } - // Logger.LogTrace("Processing message: " + msg); + // Review messages sent to the channel + if (reChatMessage.IsMatch(msg)) + { - var userName = ""; - var message = ""; + message = ChatClient.reChatMessage.Match(msg).Groups[1].Value; + Logger.LogTrace($"Message received from '{userName}': {message}"); + NewMessage?.Invoke(this, new NewMessageEventArgs + { + UserName = userName, + Message = message + }); - userName = ChatClient.reUserName.Match(msg).Groups[1].Value; + } + else if (reWhisperMessage.IsMatch(msg)) + { - if (!string.IsNullOrEmpty(userName) && msg.Contains($" JOIN #{ChannelName}")) - { - UserJoined?.Invoke(this, new ChatUserJoinedEventArgs { UserName = userName }); - } + message = ChatClient.reWhisperMessage.Match(msg).Groups[1].Value; + Logger.LogTrace($"Whisper received from '{userName}': {message}"); - // Review messages sent to the channel - if (reChatMessage.IsMatch(msg)) - { + NewMessage?.Invoke(this, new NewMessageEventArgs + { + UserName = userName, + Message = message, + IsWhisper = true + }); - message = ChatClient.reChatMessage.Match(msg).Groups[1].Value; - Logger.LogTrace($"Message received from '{userName}': {message}"); - NewMessage?.Invoke(this, new NewMessageEventArgs - { - UserName = userName, - Message = message - }); + } - } else if (reWhisperMessage.IsMatch(msg)) - { + } - message = ChatClient.reWhisperMessage.Match(msg).Groups[1].Value; - Logger.LogTrace($"Whisper received from '{userName}': {message}"); + private async Task ReadMessageAsync() + { - NewMessage?.Invoke(this, new NewMessageEventArgs - { - UserName = userName, - Message = message, - IsWhisper = true - }); + string message = null; - } + try + { + message = await inputStream.ReadLineAsync(); + } + catch (Exception ex) + { + Logger.LogError("Error reading messages: " + ex); + } - } + return message ?? ""; - private string ReadMessage() - { + } - string message = null; + #region IDisposable Support + private bool disposedValue = false; // To detect redundant calls + private Thread _ReceiveMessagesThread; - try - { - message = inputStream.ReadLine(); - } catch (Exception ex) - { - Logger.LogError("Error reading messages: " + ex); - } + protected virtual void Dispose(bool disposing) + { - return message ?? ""; + Logger.LogWarning("Disposing of ChatClient"); - } + if (!disposedValue) + { + if (disposing) + { + _Shutdown.Cancel(); + } - #region IDisposable Support - private bool disposedValue = false; // To detect redundant calls - private Thread _ReceiveMessagesThread; + _TcpClient.Dispose(); + disposedValue = true; + } + } - protected virtual void Dispose(bool disposing) - { + // This code added to correctly implement the disposable pattern. + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + #endregion + } - Logger.LogWarning("Disposing of ChatClient"); + public static class BufferHelpers + { - if (!disposedValue) - { - if (disposing) - { - _Shutdown.Cancel(); - } + public static ArraySegment ToBuffer(this string text) + { - _TcpClient.Dispose(); - disposedValue = true; - } - } + return Encoding.UTF8.GetBytes(text); - // This code added to correctly implement the disposable pattern. - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - #endregion - } + } - public static class BufferHelpers { - - public static ArraySegment ToBuffer(this string text) - { - - return Encoding.UTF8.GetBytes(text); - - } - - } + } } diff --git a/Test/GlobalSuppressions.cs b/Test/GlobalSuppressions.cs index 43aade19..d8b40e03 100644 --- a/Test/GlobalSuppressions.cs +++ b/Test/GlobalSuppressions.cs @@ -1,11 +1,11 @@ - -// This file is used by Code Analysis to maintain SuppressMessage -// attributes that are applied to this project. -// Project-level suppressions either have no target or are given -// a specific target and scoped to a namespace, type, member, etc. - -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Readability", "RCS1018:Add default access modifier.", Justification = "")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Maintainability", "RCS1141:Add parameter to documentation comment.", Justification = "")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Redundancy", "RCS1163:Unused parameter.", Justification = "")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "RCS1090:Call 'ConfigureAwait(false)'.", Justification = "No SynchronizationContext in AspNet core")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0007:Use implicit type", Justification = "Dont want to use var for bool, int ...")] + +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. + +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Readability", "RCS1018:Add default access modifier.", Justification = "")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Maintainability", "RCS1141:Add parameter to documentation comment.", Justification = "")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Redundancy", "RCS1163:Unused parameter.", Justification = "")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "RCS1090:Call 'ConfigureAwait(false)'.", Justification = "No SynchronizationContext in AspNet core")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0007:Use implicit type", Justification = "Dont want to use var for bool, int ...")] diff --git a/Test/Startup/ConfigureServicesTests.cs b/Test/Startup/ConfigureServicesTests.cs index 4f407914..c0735dae 100644 --- a/Test/Startup/ConfigureServicesTests.cs +++ b/Test/Startup/ConfigureServicesTests.cs @@ -1,64 +1,64 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Fritz.StreamLib.Core; -using Fritz.StreamTools.Services; -using Fritz.StreamTools.StartupServices; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using MixerLib; -using Xunit; - -namespace Test.Startup -{ - - public class ConfigureServicesTests - { - [Theory, MemberData(nameof(Configurations))] - public void Execute_RegisterStreamServicesWithVariousConfigurations_ReturnExpected(Dictionary configurations, Type[] expected) - { - // arrange - var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(configurations) - .Build(); - - var serviceCollection = new ServiceCollection(); - serviceCollection.AddSingleton(new LoggerFactory()); - serviceCollection.AddSingleton(configuration); - - // act - ConfigureServices.Execute(serviceCollection, configuration); - - // assert - var provider = serviceCollection.BuildServiceProvider(); - - Assert.Equal(expected, provider.GetServices().Select(x => x.GetType()).Intersect(expected)); - Assert.Equal(expected, provider.GetServices().Select(x => x.GetType()).Intersect(expected)); - } - - public static IEnumerable Configurations - { - get - { - yield return new object[]{ MakeFakeConfiguration("123456", "654321", true), new [] { typeof(TwitchService), typeof(MixerService), typeof(FakeService) } }; - yield return new object[]{ MakeFakeConfiguration("", "654321", true), new [] { typeof(MixerService), typeof(FakeService) } }; - yield return new object[]{ MakeFakeConfiguration("", "", true), new [] { typeof(FakeService) } }; - yield return new object[]{ MakeFakeConfiguration("123456", "654321", false), new [] { typeof(TwitchService), typeof(MixerService) } }; - } - } - - private static Dictionary MakeFakeConfiguration(string twitchClientId, - string mixerClientId, - bool enableFake) - { - return new Dictionary - { - {"StreamServices:Twitch:ClientId", twitchClientId}, - {"StreamServices:Mixer:Channel", mixerClientId}, - {"StreamServices:Fake:Enabled", enableFake.ToString()} - }; - } - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using Fritz.StreamLib.Core; +using Fritz.StreamTools.Services; +using Fritz.StreamTools.StartupServices; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using MixerLib; +using Xunit; + +namespace Test.Startup +{ + + public class ConfigureServicesTests + { + [Theory, MemberData(nameof(Configurations))] + public void Execute_RegisterStreamServicesWithVariousConfigurations_ReturnExpected(Dictionary configurations, Type[] expected) + { + // arrange + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(configurations) + .Build(); + + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton(new LoggerFactory()); + serviceCollection.AddSingleton(configuration); + + // act + ConfigureServices.Execute(serviceCollection, configuration); + + // assert + var provider = serviceCollection.BuildServiceProvider(); + + Assert.Equal(expected, provider.GetServices().Select(x => x.GetType()).Intersect(expected)); + Assert.Equal(expected, provider.GetServices().Select(x => x.GetType()).Intersect(expected)); + } + + public static IEnumerable Configurations + { + get + { + yield return new object[]{ MakeFakeConfiguration("123456", "654321", true), new [] { typeof(TwitchService), typeof(MixerService), typeof(FakeService) } }; + yield return new object[]{ MakeFakeConfiguration("", "654321", true), new [] { typeof(MixerService), typeof(FakeService) } }; + yield return new object[]{ MakeFakeConfiguration("", "", true), new [] { typeof(FakeService) } }; + yield return new object[]{ MakeFakeConfiguration("123456", "654321", false), new [] { typeof(TwitchService), typeof(MixerService) } }; + } + } + + private static Dictionary MakeFakeConfiguration(string twitchClientId, + string mixerClientId, + bool enableFake) + { + return new Dictionary + { + {"StreamServices:Twitch:ClientId", twitchClientId}, + {"StreamServices:Mixer:Channel", mixerClientId}, + {"StreamServices:Fake:Enabled", enableFake.ToString()} + }; + } + } +} diff --git a/docs/images/FollowerGoalSample.PNG b/docs/images/FollowerGoalSample.PNG index 6acbd226..61e5c05b 100644 Binary files a/docs/images/FollowerGoalSample.PNG and b/docs/images/FollowerGoalSample.PNG differ