diff --git a/src/DevChatter.Bot.Core/ChatUserCollection.cs b/src/DevChatter.Bot.Core/ChatUserCollection.cs index fea7a2a5..18ecd21c 100644 --- a/src/DevChatter.Bot.Core/ChatUserCollection.cs +++ b/src/DevChatter.Bot.Core/ChatUserCollection.cs @@ -5,24 +5,42 @@ using DevChatter.Bot.Core.Data.Model; using DevChatter.Bot.Core.Data.Specifications; using DevChatter.Bot.Core.Extensions; +using DevChatter.Bot.Core.Systems.Chat; namespace DevChatter.Bot.Core { public class ChatUserCollection : IChatUserCollection { private readonly IRepository _repository; + private readonly IKnownBotService _knownBotService; private readonly object _userCreationLock = new object(); private readonly object _activeChatUsersLock = new object(); private readonly object _currencyLock = new object(); private readonly List _activeChatUsers = new List(); - public ChatUserCollection(IRepository repository) + public ChatUserCollection(IRepository repository, IKnownBotService knownBotService) { _repository = repository; + _knownBotService = knownBotService; } public bool NeedToWatchUser(string displayName) { + ChatUser chatUserFromDb = GetOrCreateChatUser(displayName); + + if (chatUserFromDb.IsKnownBot == null) + { + lock (_activeChatUsersLock) + { + bool isKnownBot = _knownBotService.IsKnownBot(chatUserFromDb.DisplayName).GetAwaiter().GetResult(); + chatUserFromDb.IsKnownBot = isKnownBot; + _repository.Update(chatUserFromDb); + } + } + + if (chatUserFromDb.IsKnownBot.Value) + return false; + // Don't lock in here // ReSharper disable once InconsistentlySynchronizedField return _activeChatUsers.All(name => name != displayName); diff --git a/src/DevChatter.Bot.Core/Data/Model/ChatUser.cs b/src/DevChatter.Bot.Core/Data/Model/ChatUser.cs index b054400d..a448c2a3 100644 --- a/src/DevChatter.Bot.Core/Data/Model/ChatUser.cs +++ b/src/DevChatter.Bot.Core/Data/Model/ChatUser.cs @@ -8,6 +8,7 @@ public class ChatUser : DataEntity public string DisplayName { get; set; } public UserRole? Role { get; set; } public int Tokens { get; set; } + public bool? IsKnownBot { get; set; } public bool CanRunCommand(IBotCommand botCommand) { diff --git a/src/DevChatter.Bot.Core/Events/CurrencyGenerator.cs b/src/DevChatter.Bot.Core/Events/CurrencyGenerator.cs index af1aaf11..09514c7c 100644 --- a/src/DevChatter.Bot.Core/Events/CurrencyGenerator.cs +++ b/src/DevChatter.Bot.Core/Events/CurrencyGenerator.cs @@ -37,14 +37,15 @@ private void ChatClientOnOnUserNoticed(object sender, UserStatusEventArgs eventA private void WatchUserIfNeeded(string displayName, ChatUser chatUser) { + // Calling this here to make sure the user is in the database before calling NeedToWatchUser + ChatUser userFromDb = _chatUserCollection.GetOrCreateChatUser(displayName, chatUser); + if (_chatUserCollection.NeedToWatchUser(displayName)) { - ChatUser userFromDb = _chatUserCollection.GetOrCreateChatUser(displayName, chatUser); _chatUserCollection.WatchUser(userFromDb.DisplayName); } } - private void ChatClientOnUserLeft(object sender, UserStatusEventArgs eventArgs) { _chatUserCollection.GetOrCreateChatUser(eventArgs.DisplayName, eventArgs.ToChatUser()); diff --git a/src/DevChatter.Bot.Core/Systems/Chat/IKnownBotService.cs b/src/DevChatter.Bot.Core/Systems/Chat/IKnownBotService.cs new file mode 100644 index 00000000..c1a1fbc7 --- /dev/null +++ b/src/DevChatter.Bot.Core/Systems/Chat/IKnownBotService.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace DevChatter.Bot.Core.Systems.Chat +{ + public interface IKnownBotService + { + Task IsKnownBot(string username); + } +} diff --git a/src/DevChatter.Bot.Infra.Ef/Migrations/20180513054353_Add-IsKnownBotColumn.Designer.cs b/src/DevChatter.Bot.Infra.Ef/Migrations/20180513054353_Add-IsKnownBotColumn.Designer.cs new file mode 100644 index 00000000..adbbfb24 --- /dev/null +++ b/src/DevChatter.Bot.Infra.Ef/Migrations/20180513054353_Add-IsKnownBotColumn.Designer.cs @@ -0,0 +1,181 @@ +// +using DevChatter.Bot.Core.Data.Model; +using DevChatter.Bot.Infra.Ef; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Storage.Internal; +using System; + +namespace DevChatter.Bot.Infra.Ef.Migrations +{ + [DbContext(typeof(AppDataContext))] + [Migration("20180513054353_Add-IsKnownBotColumn")] + partial class AddIsKnownBotColumn + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.0.2-rtm-10011") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("DevChatter.Bot.Core.Commands.SimpleCommand", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("CommandText"); + + b.Property("HelpText"); + + b.Property("RoleRequired"); + + b.Property("StaticResponse"); + + b.HasKey("Id"); + + b.ToTable("SimpleCommands"); + }); + + modelBuilder.Entity("DevChatter.Bot.Core.Data.Model.ChatUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DisplayName"); + + b.Property("IsKnownBot"); + + b.Property("Role"); + + b.Property("Tokens"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.ToTable("ChatUsers"); + }); + + modelBuilder.Entity("DevChatter.Bot.Core.Data.Model.CommandUsageEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChatClientUsed"); + + b.Property("CommandWord"); + + b.Property("DateTimeUsed"); + + b.Property("FullTypeName"); + + b.Property("UserDisplayName"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.ToTable("CommandUsages"); + }); + + modelBuilder.Entity("DevChatter.Bot.Core.Data.Model.CommandWordEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("CommandWord"); + + b.Property("FullTypeName"); + + b.Property("IsPrimary"); + + b.HasKey("Id"); + + b.HasIndex("CommandWord"); + + b.ToTable("CommandWords"); + }); + + modelBuilder.Entity("DevChatter.Bot.Core.Data.Model.HangmanWord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Word"); + + b.HasKey("Id"); + + b.ToTable("HangmanWords"); + }); + + modelBuilder.Entity("DevChatter.Bot.Core.Data.Model.IntervalMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DelayInMinutes"); + + b.Property("MessageText"); + + b.HasKey("Id"); + + b.ToTable("IntervalMessages"); + }); + + modelBuilder.Entity("DevChatter.Bot.Core.Data.Model.QuoteEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AddedBy"); + + b.Property("Author"); + + b.Property("DateAdded"); + + b.Property("QuoteId"); + + b.Property("Text"); + + b.HasKey("Id"); + + b.HasIndex("QuoteId"); + + b.ToTable("QuoteEntities"); + }); + + modelBuilder.Entity("DevChatter.Bot.Core.Data.Model.ScheduleEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ExampleDateTime"); + + b.HasKey("Id"); + + b.ToTable("ScheduleEntities"); + }); + + modelBuilder.Entity("DevChatter.Bot.Core.Data.Model.StreamerEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelName"); + + b.Property("DateAdded"); + + b.Property("TimesShoutedOut"); + + b.HasKey("Id"); + + b.ToTable("Streamers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/DevChatter.Bot.Infra.Ef/Migrations/20180513054353_Add-IsKnownBotColumn.cs b/src/DevChatter.Bot.Infra.Ef/Migrations/20180513054353_Add-IsKnownBotColumn.cs new file mode 100644 index 00000000..9f198843 --- /dev/null +++ b/src/DevChatter.Bot.Infra.Ef/Migrations/20180513054353_Add-IsKnownBotColumn.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace DevChatter.Bot.Infra.Ef.Migrations +{ + public partial class AddIsKnownBotColumn : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "IsKnownBot", + table: "ChatUsers", + nullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "IsKnownBot", + table: "ChatUsers"); + } + } +} diff --git a/src/DevChatter.Bot.Infra.Ef/Migrations/AppDataContextModelSnapshot.cs b/src/DevChatter.Bot.Infra.Ef/Migrations/AppDataContextModelSnapshot.cs index 0135e628..33e3fcd0 100644 --- a/src/DevChatter.Bot.Infra.Ef/Migrations/AppDataContextModelSnapshot.cs +++ b/src/DevChatter.Bot.Infra.Ef/Migrations/AppDataContextModelSnapshot.cs @@ -46,6 +46,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("DisplayName"); + b.Property("IsKnownBot"); + b.Property("Role"); b.Property("Tokens"); diff --git a/src/DevChatter.Bot.Infra.Twitch/DevChatterBotTwitchModule.cs b/src/DevChatter.Bot.Infra.Twitch/DevChatterBotTwitchModule.cs index 8442af24..0c6b7fca 100644 --- a/src/DevChatter.Bot.Infra.Twitch/DevChatterBotTwitchModule.cs +++ b/src/DevChatter.Bot.Infra.Twitch/DevChatterBotTwitchModule.cs @@ -23,6 +23,8 @@ protected override void Load(ContainerBuilder builder) builder.RegisterType().AsImplementedInterfaces().SingleInstance(); builder.RegisterType().AsImplementedInterfaces().SingleInstance(); + + builder.RegisterType().AsImplementedInterfaces().SingleInstance(); } } } diff --git a/src/DevChatter.Bot.Infra.Twitch/TwitchKnownBotService.cs b/src/DevChatter.Bot.Infra.Twitch/TwitchKnownBotService.cs new file mode 100644 index 00000000..92ba9e36 --- /dev/null +++ b/src/DevChatter.Bot.Infra.Twitch/TwitchKnownBotService.cs @@ -0,0 +1,37 @@ +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using DevChatter.Bot.Core.Systems.Chat; + +namespace DevChatter.Bot.Infra.Twitch +{ + public class TwitchKnownBotService : IKnownBotService + { + private const string API_ENDPOINT = "https://api.twitchbots.info/v1/"; + + private readonly HttpClient _client; + + public TwitchKnownBotService() + { + _client = new HttpClient(); + } + + public async Task IsKnownBot(string username) + { + HttpResponseMessage response = await _client.GetAsync($"{API_ENDPOINT}bot/{username}").ConfigureAwait(false); + + switch (response.StatusCode) + { + case HttpStatusCode.NotFound: + return false; + + case HttpStatusCode.OK: + return true; + + default: + throw new HttpRequestException( + $"Unable to fetch known bot. Service returned {(int) response.StatusCode} {response.StatusCode}"); + } + } + } +} diff --git a/src/UnitTests/Core/Automation/CurrencyUpdateTests/IsTimeToRunShould.cs b/src/UnitTests/Core/Automation/CurrencyUpdateTests/IsTimeToRunShould.cs index 5e582d93..78dd10a1 100644 --- a/src/UnitTests/Core/Automation/CurrencyUpdateTests/IsTimeToRunShould.cs +++ b/src/UnitTests/Core/Automation/CurrencyUpdateTests/IsTimeToRunShould.cs @@ -42,7 +42,9 @@ public void ReturnFalse_AfterInvokingAction() const int intervalInMinutes = 1; var repository = new Mock(); repository.Setup(x => x.List(It.IsAny>())).Returns(new List()); - var currencyGenerator = new CurrencyGenerator(new List(), new ChatUserCollection(repository.Object)); + var knownBotService = new Mock(); + var currencyGenerator = new CurrencyGenerator(new List(), + new ChatUserCollection(repository.Object, knownBotService.Object)); var fakeClock = new FakeClock(); var currencyUpdate = new CurrencyUpdate(intervalInMinutes, currencyGenerator, fakeClock); diff --git a/src/UnitTests/Core/Events/CurrencyGeneratorTests/AddCurrencyToShould.cs b/src/UnitTests/Core/Events/CurrencyGeneratorTests/AddCurrencyToShould.cs index 402830dc..78c63de9 100644 --- a/src/UnitTests/Core/Events/CurrencyGeneratorTests/AddCurrencyToShould.cs +++ b/src/UnitTests/Core/Events/CurrencyGeneratorTests/AddCurrencyToShould.cs @@ -18,7 +18,9 @@ public class AddCurrencyToShould public void AllowGoingToMaxInt_GivenZeroStartAndMaxIntAdded() { var mockRepo = new Mock(); - var currencyGenerator = new CurrencyGenerator(new List(), new ChatUserCollection(mockRepo.Object)); + var mockKnownBot = new Mock(); + var currencyGenerator = new CurrencyGenerator(new List(), + new ChatUserCollection(mockRepo.Object, mockKnownBot.Object)); var chatUser = new ChatUser {Tokens = 0}; mockRepo.Setup(x => x.List(It.IsAny())).Returns(new List { chatUser }); @@ -32,7 +34,9 @@ public void AllowGoingToMaxInt_GivenZeroStartAndMaxIntAdded() public void CapAtMaxInt_GivenOverflowPossibility() { var mockRepo = new Mock(); - var currencyGenerator = new CurrencyGenerator(new List(), new ChatUserCollection(mockRepo.Object)); + var mockKnownBot = new Mock(); + var currencyGenerator = new CurrencyGenerator(new List(), + new ChatUserCollection(mockRepo.Object, mockKnownBot.Object)); var chatUser = new ChatUser {Tokens = 100}; mockRepo.Setup(x => x.List(It.IsAny())).Returns(new List { chatUser });