diff --git a/.github/workflows/release-docker-images.yml b/.github/workflows/release-docker-images.yml index c638b980..4fd8dd0e 100644 --- a/.github/workflows/release-docker-images.yml +++ b/.github/workflows/release-docker-images.yml @@ -58,6 +58,19 @@ jobs: docker_hub_username: ${{ secrets.DOCKER_HUB_USERNAME }} docker_hub_token: ${{ secrets.DOCKER_HUB_TOKEN }} + twitch-alerts: + name: Publish Twitch Alert Agent + uses: rGunti/FloppyBot/.github/workflows/template-release-docker-image.yml@main + with: + dockerfile_path: src/FloppyBot.Aux.TwitchAlerts.Agent/Dockerfile + project_path: src/FloppyBot.Aux.TwitchAlerts.Agent/FloppyBot.Aux.TwitchAlerts.Agent.csproj + image_name: floppybot/twitch-alerts + image_tag: ${{ github.ref_name }} + push_latest: true + secrets: + docker_hub_username: ${{ secrets.DOCKER_HUB_USERNAME }} + docker_hub_token: ${{ secrets.DOCKER_HUB_TOKEN }} + webapi: name: Publish Web API Agent uses: rGunti/FloppyBot/.github/workflows/template-release-docker-image.yml@main diff --git a/src/FloppyBot.Aux.MessageCounter.Core/MessageCounter.cs b/src/FloppyBot.Aux.MessageCounter.Core/MessageCounter.cs index bd3e6159..568f8088 100644 --- a/src/FloppyBot.Aux.MessageCounter.Core/MessageCounter.cs +++ b/src/FloppyBot.Aux.MessageCounter.Core/MessageCounter.cs @@ -1,4 +1,5 @@ using FloppyBot.Base.Configuration; +using FloppyBot.Chat; using FloppyBot.Chat.Entities; using FloppyBot.Communication; using Microsoft.Extensions.Configuration; @@ -48,6 +49,10 @@ public void Stop() private void OnMessageReceived(ChatMessage chatMessage) { + if (chatMessage.EventName != SharedEventTypes.CHAT_MESSAGE) + { + return; + } #if DEBUG _logger.LogInformation("Received chat message to count: {@ChatMessage}", chatMessage); #endif diff --git a/src/FloppyBot.Aux.TwitchAlerts.Agent/Dockerfile b/src/FloppyBot.Aux.TwitchAlerts.Agent/Dockerfile new file mode 100644 index 00000000..f3bee01a --- /dev/null +++ b/src/FloppyBot.Aux.TwitchAlerts.Agent/Dockerfile @@ -0,0 +1,5 @@ +FROM mcr.microsoft.com/dotnet/runtime:6.0 AS runtime +COPY ./out /app +COPY ./app-version /app/version +WORKDIR /app +ENTRYPOINT [ "dotnet", "FloppyBot.Aux.TwitchAlerts.Agent.dll" ] diff --git a/src/FloppyBot.Aux.TwitchAlerts.Agent/FloppyBot.Aux.TwitchAlerts.Agent.csproj b/src/FloppyBot.Aux.TwitchAlerts.Agent/FloppyBot.Aux.TwitchAlerts.Agent.csproj new file mode 100644 index 00000000..5cc9381d --- /dev/null +++ b/src/FloppyBot.Aux.TwitchAlerts.Agent/FloppyBot.Aux.TwitchAlerts.Agent.csproj @@ -0,0 +1,23 @@ + + + + net6.0 + enable + enable + dotnet-FloppyBot.Aux.TwitchAlerts.Agent-FE25745F-A00C-4D5C-B500-BD5A61D5841B + + + + + + + + + + + + + + + + diff --git a/src/FloppyBot.Aux.TwitchAlerts.Agent/Program.cs b/src/FloppyBot.Aux.TwitchAlerts.Agent/Program.cs new file mode 100644 index 00000000..ac3a6684 --- /dev/null +++ b/src/FloppyBot.Aux.TwitchAlerts.Agent/Program.cs @@ -0,0 +1,27 @@ +using FloppyBot.Aux.TwitchAlerts.Agent; +using FloppyBot.Aux.TwitchAlerts.Core; +using FloppyBot.Base.Configuration; +using FloppyBot.Base.Logging; +using FloppyBot.Base.Storage.MongoDb; +using FloppyBot.Communication.Redis.Config; +using FloppyBot.HealthCheck.Core; +using FloppyBot.HealthCheck.KillSwitch; + +IHostBuilder hostBuilder = Host.CreateDefaultBuilder(args).SetupConfiguration().SetupSerilog(); + +IHost host = hostBuilder + .ConfigureServices(services => + { + services + .AddRedisCommunication() + .AddHealthCheck() + .AddKillSwitchTrigger() + .AddKillSwitch() + .AddMongoDbStorage() + .AddTwitchAlertService() + .AddTwitchAlertCore() + .AddHostedService(); + }) + .Build(); + +await host.ArmKillSwitch().LogAndRun(); diff --git a/src/FloppyBot.Aux.TwitchAlerts.Agent/Properties/launchSettings.json b/src/FloppyBot.Aux.TwitchAlerts.Agent/Properties/launchSettings.json new file mode 100644 index 00000000..fe960d07 --- /dev/null +++ b/src/FloppyBot.Aux.TwitchAlerts.Agent/Properties/launchSettings.json @@ -0,0 +1,11 @@ +{ + "profiles": { + "FloppyBot.Aux.TwitchAlerts.Agent": { + "commandName": "Project", + "dotnetRunMessages": true, + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/FloppyBot.Aux.TwitchAlerts.Agent/TwitchAlertHost.cs b/src/FloppyBot.Aux.TwitchAlerts.Agent/TwitchAlertHost.cs new file mode 100644 index 00000000..b04bb3b3 --- /dev/null +++ b/src/FloppyBot.Aux.TwitchAlerts.Agent/TwitchAlertHost.cs @@ -0,0 +1,36 @@ +using FloppyBot.Aux.TwitchAlerts.Core; + +namespace FloppyBot.Aux.TwitchAlerts.Agent; + +public class TwitchAlertHost : BackgroundService +{ + private readonly ILogger _logger; + private readonly TwitchAlertListener _listener; + + public TwitchAlertHost(ILogger logger, TwitchAlertListener listener) + { + _logger = logger; + _listener = listener; + } + + public override Task StartAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Starting up Twitch Alert Agent ..."); + _listener.Start(); + + _logger.LogInformation("Awaiting new messages to count"); + return Task.CompletedTask; + } + + public override Task StopAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Shutting down Twitch Alert Agent ..."); + _listener.Stop(); + return base.StopAsync(cancellationToken); + } + + protected override Task ExecuteAsync(CancellationToken stoppingToken) + { + return Task.CompletedTask; + } +} diff --git a/src/FloppyBot.Aux.TwitchAlerts.Agent/floppybot.Development.json b/src/FloppyBot.Aux.TwitchAlerts.Agent/floppybot.Development.json new file mode 100644 index 00000000..efcc989a --- /dev/null +++ b/src/FloppyBot.Aux.TwitchAlerts.Agent/floppybot.Development.json @@ -0,0 +1,11 @@ +{ + "Serilog": { + "MinimumLevel": { + "Default": "Verbose", + "Override": { + "FloppyBot.HealthCheck.Core.HealthCheckProducerCronJob": "Debug" + } + } + }, + "InstanceName": "DEV" +} diff --git a/src/FloppyBot.Aux.TwitchAlerts.Agent/floppybot.json b/src/FloppyBot.Aux.TwitchAlerts.Agent/floppybot.json new file mode 100644 index 00000000..7a9503f4 --- /dev/null +++ b/src/FloppyBot.Aux.TwitchAlerts.Agent/floppybot.json @@ -0,0 +1,17 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "ConnectionStrings": { + "MongoDb": "mongodb://localhost:27017/FloppyBot", + "Redis": "localhost", + "HealthCheck": "{Redis}|HealthCheck", + "KillSwitch": "{Redis}|KillSwitch", + "MessageInput": "{Redis}|Message.Received", + "MessageOutput": "{Redis}|Message.Responded.Twitch" + }, + "InstanceName": "1" +} diff --git a/src/FloppyBot.Aux.TwitchAlerts.Core/DiSetup.cs b/src/FloppyBot.Aux.TwitchAlerts.Core/DiSetup.cs new file mode 100644 index 00000000..43fff6c8 --- /dev/null +++ b/src/FloppyBot.Aux.TwitchAlerts.Core/DiSetup.cs @@ -0,0 +1,19 @@ +using FloppyBot.Aux.TwitchAlerts.Core.Entities; +using Microsoft.Extensions.DependencyInjection; + +namespace FloppyBot.Aux.TwitchAlerts.Core; + +public static class DiSetup +{ + public static IServiceCollection AddTwitchAlertCore(this IServiceCollection services) + { + return services.AddSingleton(); + } + + public static IServiceCollection AddTwitchAlertService(this IServiceCollection services) + { + return services + .AddTransient() + .AddAutoMapper(typeof(TwitchAlertListener)); + } +} diff --git a/src/FloppyBot.Aux.TwitchAlerts.Core/Entities/ITwitchAlertService.cs b/src/FloppyBot.Aux.TwitchAlerts.Core/Entities/ITwitchAlertService.cs new file mode 100644 index 00000000..87acbcaa --- /dev/null +++ b/src/FloppyBot.Aux.TwitchAlerts.Core/Entities/ITwitchAlertService.cs @@ -0,0 +1,33 @@ +using AutoMapper; +using FloppyBot.Aux.TwitchAlerts.Core.Entities.Storage; +using FloppyBot.Base.Storage; + +namespace FloppyBot.Aux.TwitchAlerts.Core.Entities; + +public interface ITwitchAlertService +{ + TwitchAlertSettings? GetAlertSettings(string channelId); + void StoreAlertSettings(TwitchAlertSettings settings); +} + +public class TwitchAlertService : ITwitchAlertService +{ + private readonly IRepository _repository; + private readonly IMapper _mapper; + + public TwitchAlertService(IRepositoryFactory repositoryFactory, IMapper mapper) + { + _mapper = mapper; + _repository = repositoryFactory.GetRepository(); + } + + public TwitchAlertSettings? GetAlertSettings(string channelId) + { + return _mapper.Map(_repository.GetById(channelId)); + } + + public void StoreAlertSettings(TwitchAlertSettings settings) + { + _repository.Upsert(_mapper.Map(settings)); + } +} diff --git a/src/FloppyBot.Aux.TwitchAlerts.Core/Entities/Storage/TwitchAlertSettingsEo.cs b/src/FloppyBot.Aux.TwitchAlerts.Core/Entities/Storage/TwitchAlertSettingsEo.cs new file mode 100644 index 00000000..13c3b03a --- /dev/null +++ b/src/FloppyBot.Aux.TwitchAlerts.Core/Entities/Storage/TwitchAlertSettingsEo.cs @@ -0,0 +1,26 @@ +using FloppyBot.Base.Storage; + +namespace FloppyBot.Aux.TwitchAlerts.Core.Entities.Storage; + +public record TwitchAlertSettingsEo( + string Id, + bool SubAlertsEnabled, + TwitchAlertMessageEo[] SubMessages, + TwitchAlertMessageEo[] ReSubMessages, + TwitchAlertMessageEo[] GiftSubMessages, + TwitchAlertMessageEo[] GiftSubCommunityMessages +) : IEntity +{ + public TwitchAlertSettingsEo WithId(string newId) + { + return this with { Id = newId }; + } +} + +public record TwitchAlertMessageEo( + string DefaultMessage, + string? Tier1Message, + string? Tier2Message, + string? Tier3Message, + string? PrimeMessage +); diff --git a/src/FloppyBot.Aux.TwitchAlerts.Core/Entities/Storage/TwitchAlertStorageProfile.cs b/src/FloppyBot.Aux.TwitchAlerts.Core/Entities/Storage/TwitchAlertStorageProfile.cs new file mode 100644 index 00000000..867f8d69 --- /dev/null +++ b/src/FloppyBot.Aux.TwitchAlerts.Core/Entities/Storage/TwitchAlertStorageProfile.cs @@ -0,0 +1,57 @@ +using System.Collections.Immutable; +using AutoMapper; + +namespace FloppyBot.Aux.TwitchAlerts.Core.Entities.Storage; + +public class TwitchAlertStorageProfile : Profile +{ + public TwitchAlertStorageProfile() + { + // dto -> eo + CreateMap() + .ConvertUsing( + (dto, _, ctx) => + new TwitchAlertSettingsEo( + dto.Id, + dto.SubAlertsEnabled, + dto.SubMessage + .Select(msg => ctx.Mapper.Map(msg)) + .ToArray(), + dto.ReSubMessage + .Select(msg => ctx.Mapper.Map(msg)) + .ToArray(), + dto.GiftSubMessage + .Select(msg => ctx.Mapper.Map(msg)) + .ToArray(), + dto.GiftSubCommunityMessage + .Select(msg => ctx.Mapper.Map(msg)) + .ToArray() + ) + ); + CreateMap(); + + // eo -> dto + CreateMap() + .ConvertUsing( + (eo, _, ctx) => + new TwitchAlertSettings + { + Id = eo.Id, + SubAlertsEnabled = eo.SubAlertsEnabled, + SubMessage = eo.SubMessages + .Select(msg => ctx.Mapper.Map(msg)) + .ToImmutableList(), + ReSubMessage = eo.ReSubMessages + .Select(msg => ctx.Mapper.Map(msg)) + .ToImmutableList(), + GiftSubMessage = eo.GiftSubMessages + .Select(msg => ctx.Mapper.Map(msg)) + .ToImmutableList(), + GiftSubCommunityMessage = eo.GiftSubCommunityMessages + .Select(msg => ctx.Mapper.Map(msg)) + .ToImmutableList(), + } + ); + CreateMap(); + } +} diff --git a/src/FloppyBot.Aux.TwitchAlerts.Core/Entities/TwitchAlertSettings.cs b/src/FloppyBot.Aux.TwitchAlerts.Core/Entities/TwitchAlertSettings.cs new file mode 100644 index 00000000..ae78d391 --- /dev/null +++ b/src/FloppyBot.Aux.TwitchAlerts.Core/Entities/TwitchAlertSettings.cs @@ -0,0 +1,59 @@ +#pragma warning disable CS8618 +using System.Collections.Immutable; +using FloppyBot.Base.EquatableCollections; +using FloppyBot.Base.Storage; + +namespace FloppyBot.Aux.TwitchAlerts.Core.Entities; + +public record TwitchAlertSettings : IEntity +{ + private readonly IImmutableList _subMessages = + ImmutableList.Empty; + private readonly IImmutableList _reSubMessages = + ImmutableList.Empty; + private readonly IImmutableList _giftSubMessages = + ImmutableList.Empty; + private readonly IImmutableList _giftSubCommunityMessages = + ImmutableList.Empty; + + public string Id { get; init; } + + public bool SubAlertsEnabled { get; init; } + + public IImmutableList SubMessage + { + get => _subMessages; + init => _subMessages = value.WithValueSemantics(); + } + + public IImmutableList ReSubMessage + { + get => _reSubMessages; + init => _reSubMessages = value.WithValueSemantics(); + } + + public IImmutableList GiftSubMessage + { + get => _giftSubMessages; + init => _giftSubMessages = value.WithValueSemantics(); + } + + public IImmutableList GiftSubCommunityMessage + { + get => _giftSubCommunityMessages; + init => _giftSubCommunityMessages = value.WithValueSemantics(); + } + + public TwitchAlertSettings WithId(string newId) + { + return this with { Id = newId }; + } +} + +public record TwitchAlertMessage( + string DefaultMessage, + string? Tier1Message = null, + string? Tier2Message = null, + string? Tier3Message = null, + string? PrimeMessage = null +); diff --git a/src/FloppyBot.Aux.TwitchAlerts.Core/FloppyBot.Aux.TwitchAlerts.Core.csproj b/src/FloppyBot.Aux.TwitchAlerts.Core/FloppyBot.Aux.TwitchAlerts.Core.csproj new file mode 100644 index 00000000..0c81ce19 --- /dev/null +++ b/src/FloppyBot.Aux.TwitchAlerts.Core/FloppyBot.Aux.TwitchAlerts.Core.csproj @@ -0,0 +1,25 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + + + + + + + + + diff --git a/src/FloppyBot.Aux.TwitchAlerts.Core/TwitchAlertListener.cs b/src/FloppyBot.Aux.TwitchAlerts.Core/TwitchAlertListener.cs new file mode 100644 index 00000000..54b90b9c --- /dev/null +++ b/src/FloppyBot.Aux.TwitchAlerts.Core/TwitchAlertListener.cs @@ -0,0 +1,217 @@ +using System.Collections.Immutable; +using System.Text.Json; +using FloppyBot.Aux.TwitchAlerts.Core.Entities; +using FloppyBot.Base.Configuration; +using FloppyBot.Base.Extensions; +using FloppyBot.Base.TextFormatting; +using FloppyBot.Chat.Entities; +using FloppyBot.Chat.Entities.Identifiers; +using FloppyBot.Chat.Twitch.Events; +using FloppyBot.Communication; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace FloppyBot.Aux.TwitchAlerts.Core; + +public class TwitchAlertListener : IDisposable +{ + private static readonly ISet AllowedEvents = new[] + { + TwitchEventTypes.SUBSCRIPTION, + TwitchEventTypes.RE_SUBSCRIPTION, + TwitchEventTypes.SUBSCRIPTION_GIFT, + TwitchEventTypes.SUBSCRIPTION_GIFT_COMMUNITY, + }.ToHashSet(); + + private static readonly IImmutableDictionary< + TwitchSubscriptionPlanTier, + Func + > MessageTemplateSelectors = new Dictionary< + TwitchSubscriptionPlanTier, + Func + > + { + { TwitchSubscriptionPlanTier.Tier1, m => m.Tier1Message ?? m.DefaultMessage }, + { TwitchSubscriptionPlanTier.Tier2, m => m.Tier2Message ?? m.DefaultMessage }, + { TwitchSubscriptionPlanTier.Tier3, m => m.Tier3Message ?? m.DefaultMessage }, + { TwitchSubscriptionPlanTier.Prime, m => m.PrimeMessage ?? m.DefaultMessage }, + }.ToImmutableDictionary(); + + private static readonly Func DefaultTemplateSelector = m => + m.DefaultMessage; + + private static readonly IImmutableDictionary< + string, + Func> + > MessageTemplateListSelector = new Dictionary< + string, + Func> + > + { + { TwitchEventTypes.SUBSCRIPTION, s => s.SubMessage }, + { TwitchEventTypes.RE_SUBSCRIPTION, s => s.ReSubMessage }, + { TwitchEventTypes.SUBSCRIPTION_GIFT, s => s.GiftSubMessage }, + { TwitchEventTypes.SUBSCRIPTION_GIFT_COMMUNITY, s => s.GiftSubCommunityMessage }, + }.ToImmutableDictionary(); + + private static string? DetermineTemplate( + TwitchEvent twitchEvent, + TwitchAlertSettings alertSettings, + TwitchSubscriptionPlanTier subTier + ) + { + var templateListSelector = MessageTemplateListSelector.GetValueOrDefault( + twitchEvent.EventName, + _ => Enumerable.Empty() + ); + var templateSelector = MessageTemplateSelectors.GetValueOrDefault( + subTier, + DefaultTemplateSelector + ); + + var templates = templateListSelector(alertSettings).Select(templateSelector).ToList(); + // TODO: Get a random one + return templates.FirstOrDefault(); + } + + private static TwitchSubscriptionPlanTier GetTier(TwitchEvent twitchEvent) + { + return twitchEvent switch + { + TwitchSubscriptionReceivedEvent subReceived => subReceived.SubscriptionPlanTier.Tier, + TwitchReSubscriptionReceivedEvent reSubscriptionReceivedEvent + => reSubscriptionReceivedEvent.SubscriptionPlanTier.Tier, + TwitchSubscriptionGiftEvent subGiftEvent => subGiftEvent.SubscriptionPlanTier.Tier, + TwitchSubscriptionCommunityGiftEvent subCommunityGiftEvent + => subCommunityGiftEvent.SubscriptionPlanTier.Tier, + _ => throw new ArgumentOutOfRangeException(nameof(twitchEvent)), + }; + } + + private static TwitchEvent? ParseTwitchEvent(string type, string content) + { + return type switch + { + TwitchEventTypes.SUBSCRIPTION + => JsonSerializer.Deserialize(content), + TwitchEventTypes.RE_SUBSCRIPTION + => JsonSerializer.Deserialize(content), + TwitchEventTypes.SUBSCRIPTION_GIFT + => JsonSerializer.Deserialize(content), + TwitchEventTypes.SUBSCRIPTION_GIFT_COMMUNITY + => JsonSerializer.Deserialize(content), + _ => throw new ArgumentOutOfRangeException(nameof(type)), + }; + } + + private readonly ILogger _logger; + private readonly INotificationReceiver _chatMessageReceiver; + private readonly INotificationSender _responder; + private readonly ITwitchAlertService _alertService; + + public TwitchAlertListener( + ILogger logger, + INotificationReceiverFactory receiverFactor, + INotificationSenderFactory senderFactory, + IConfiguration configuration, + ITwitchAlertService alertService + ) + { + _logger = logger; + _alertService = alertService; + _chatMessageReceiver = receiverFactor.GetNewReceiver( + configuration.GetParsedConnectionString("MessageInput") + ); + _responder = senderFactory.GetNewSender( + configuration.GetParsedConnectionString("MessageOutput") + ); + + _chatMessageReceiver.NotificationReceived += OnMessageReceived; + } + + public void Start() + { + _logger.LogInformation( + "Connecting to message input to start listening for incoming messages" + ); + _chatMessageReceiver.StartListening(); + } + + public void Stop() + { + _logger.LogInformation("Shutting down Twitch Alert Listener"); + _chatMessageReceiver.StopListening(); + } + + public void Dispose() + { + Stop(); + GC.SuppressFinalize(this); + } + + private void OnMessageReceived(ChatMessage chatMessage) + { + if (!AllowedEvents.Contains(chatMessage.EventName)) + { +#if DEBUG + _logger.LogDebug( + "Received chat message with event name that is not allowed: {ChatMessageEventName}", + chatMessage.EventName + ); +#endif + return; + } +#if DEBUG + _logger.LogInformation("Received chat message to count: {@ChatMessage}", chatMessage); +#endif + + var twitchEvent = ParseTwitchEvent(chatMessage.EventName, chatMessage.Content); + if (twitchEvent == null) + { + _logger.LogWarning("Failed to parse chat message content"); + return; + } + + var alertMessage = GetFormattedMessage(chatMessage, twitchEvent); + + if (alertMessage == null) + { + return; + } + + var response = chatMessage with + { + Content = alertMessage.Format( + twitchEvent.AsDictionary().Add("User", chatMessage.Author.DisplayName) + ), + }; + + _responder.Send(response); + } + + private string? GetFormattedMessage(ChatMessage chatMessage, TwitchEvent twitchEvent) + { + var channelId = chatMessage.Identifier.GetChannel(); + var alertSettings = _alertService.GetAlertSettings(channelId); + if (alertSettings == null) + { + _logger.LogDebug("No alert settings found for channel {Channel}, skipping", channelId); + return null; + } + + var subTier = GetTier(twitchEvent); + + var template = DetermineTemplate(twitchEvent, alertSettings, subTier); + if (template == null) + { + _logger.LogDebug( + "No template found for event {EventName} and tier {Tier}, skipping", + twitchEvent.EventName, + subTier + ); + return null; + } + + return template.Format(twitchEvent); + } +} diff --git a/src/FloppyBot.Base.Extensions/ObjectExtensions.cs b/src/FloppyBot.Base.Extensions/ObjectExtensions.cs new file mode 100644 index 00000000..222f327e --- /dev/null +++ b/src/FloppyBot.Base.Extensions/ObjectExtensions.cs @@ -0,0 +1,27 @@ +using System.Collections.Immutable; +using System.Reflection; + +namespace FloppyBot.Base.Extensions; + +public static class ObjectExtensions +{ + public static ImmutableDictionary AsDictionary( + this object? source, + BindingFlags bindingAttr = + BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance + ) + { + if (source == null) + { + return ImmutableDictionary.Empty; + } + + return source + .GetType() + .GetProperties(bindingAttr) + .ToImmutableDictionary( + propInfo => propInfo.Name, + propInfo => propInfo.GetValue(source, null) + ); + } +} diff --git a/src/FloppyBot.Base.Storage.LiteDb/LiteDbRepository.cs b/src/FloppyBot.Base.Storage.LiteDb/LiteDbRepository.cs index fce8a457..aa3f41f8 100644 --- a/src/FloppyBot.Base.Storage.LiteDb/LiteDbRepository.cs +++ b/src/FloppyBot.Base.Storage.LiteDb/LiteDbRepository.cs @@ -75,4 +75,10 @@ public int Delete(IEnumerable entities) { return Delete(entities.Select(i => i.Id)); } + + public TEntity Upsert(TEntity entity) + { + _collection.Upsert(entity); + return GetById(entity.Id)!; + } } diff --git a/src/FloppyBot.Base.Storage.MongoDb/MongoDbRepository.cs b/src/FloppyBot.Base.Storage.MongoDb/MongoDbRepository.cs index 66fc98dd..b64c8dfb 100644 --- a/src/FloppyBot.Base.Storage.MongoDb/MongoDbRepository.cs +++ b/src/FloppyBot.Base.Storage.MongoDb/MongoDbRepository.cs @@ -67,5 +67,15 @@ public int Delete(IEnumerable entities) return Delete(entities.Select(i => i.Id)); } + public TEntity Upsert(TEntity entity) + { + var result = _collection.ReplaceOne( + GetIdFilter(entity.Id), + entity, + new ReplaceOptions { IsUpsert = true } + ); + return GetById(result.UpsertedId.AsString)!; + } + private FilterDefinition GetIdFilter(string id) => Filter.Eq(i => i.Id, id); } diff --git a/src/FloppyBot.Base.Storage/IRepository.cs b/src/FloppyBot.Base.Storage/IRepository.cs index a45308cc..c6e5caa7 100644 --- a/src/FloppyBot.Base.Storage/IRepository.cs +++ b/src/FloppyBot.Base.Storage/IRepository.cs @@ -13,4 +13,5 @@ public interface IRepository bool Delete(TEntity entity); int Delete(IEnumerable ids); int Delete(IEnumerable entities); + TEntity Upsert(TEntity entity); } diff --git a/src/FloppyBot.Chat.Twitch.Events/FloppyBot.Chat.Twitch.Events.csproj b/src/FloppyBot.Chat.Twitch.Events/FloppyBot.Chat.Twitch.Events.csproj new file mode 100644 index 00000000..64474110 --- /dev/null +++ b/src/FloppyBot.Chat.Twitch.Events/FloppyBot.Chat.Twitch.Events.csproj @@ -0,0 +1,13 @@ + + + + net6.0 + enable + enable + + + + + + + diff --git a/src/FloppyBot.Chat.Twitch.Events/TwitchEvent.cs b/src/FloppyBot.Chat.Twitch.Events/TwitchEvent.cs new file mode 100644 index 00000000..9ce770ff --- /dev/null +++ b/src/FloppyBot.Chat.Twitch.Events/TwitchEvent.cs @@ -0,0 +1,3 @@ +namespace FloppyBot.Chat.Twitch.Events; + +public abstract record TwitchEvent(string EventName); diff --git a/src/FloppyBot.Chat.Twitch.Events/TwitchEventTypes.cs b/src/FloppyBot.Chat.Twitch.Events/TwitchEventTypes.cs new file mode 100644 index 00000000..0dde3b1b --- /dev/null +++ b/src/FloppyBot.Chat.Twitch.Events/TwitchEventTypes.cs @@ -0,0 +1,9 @@ +namespace FloppyBot.Chat.Twitch.Events; + +public static class TwitchEventTypes +{ + public const string SUBSCRIPTION = "Twitch.Subscription"; + public const string RE_SUBSCRIPTION = "Twitch.ReSubscription"; + public const string SUBSCRIPTION_GIFT = "Twitch.SubscriptionGift"; + public const string SUBSCRIPTION_GIFT_COMMUNITY = "Twitch.SubscriptionGiftCommunity"; +} diff --git a/src/FloppyBot.Chat.Twitch.Events/TwitchReSubscriptionReceivedEvent.cs b/src/FloppyBot.Chat.Twitch.Events/TwitchReSubscriptionReceivedEvent.cs new file mode 100644 index 00000000..5b08a39e --- /dev/null +++ b/src/FloppyBot.Chat.Twitch.Events/TwitchReSubscriptionReceivedEvent.cs @@ -0,0 +1,6 @@ +namespace FloppyBot.Chat.Twitch.Events; + +public record TwitchReSubscriptionReceivedEvent( + TwitchSubscriptionPlan SubscriptionPlanTier, + int Months +) : TwitchEvent(TwitchEventTypes.RE_SUBSCRIPTION); diff --git a/src/FloppyBot.Chat.Twitch.Events/TwitchSubscriptionCommunityGiftEvent.cs b/src/FloppyBot.Chat.Twitch.Events/TwitchSubscriptionCommunityGiftEvent.cs new file mode 100644 index 00000000..7b50f866 --- /dev/null +++ b/src/FloppyBot.Chat.Twitch.Events/TwitchSubscriptionCommunityGiftEvent.cs @@ -0,0 +1,7 @@ +namespace FloppyBot.Chat.Twitch.Events; + +public record TwitchSubscriptionCommunityGiftEvent( + TwitchSubscriptionPlan SubscriptionPlanTier, + int MassGiftCount, + string MultiMonthDuration +) : TwitchEvent(TwitchEventTypes.SUBSCRIPTION_GIFT_COMMUNITY); diff --git a/src/FloppyBot.Chat.Twitch.Events/TwitchSubscriptionGiftEvent.cs b/src/FloppyBot.Chat.Twitch.Events/TwitchSubscriptionGiftEvent.cs new file mode 100644 index 00000000..13807584 --- /dev/null +++ b/src/FloppyBot.Chat.Twitch.Events/TwitchSubscriptionGiftEvent.cs @@ -0,0 +1,8 @@ +using FloppyBot.Chat.Entities; + +namespace FloppyBot.Chat.Twitch.Events; + +public record TwitchSubscriptionGiftEvent( + TwitchSubscriptionPlan SubscriptionPlanTier, + ChatUser Recipient +) : TwitchEvent(TwitchEventTypes.SUBSCRIPTION_GIFT); diff --git a/src/FloppyBot.Chat.Twitch.Events/TwitchSubscriptionPlan.cs b/src/FloppyBot.Chat.Twitch.Events/TwitchSubscriptionPlan.cs new file mode 100644 index 00000000..a37a5f13 --- /dev/null +++ b/src/FloppyBot.Chat.Twitch.Events/TwitchSubscriptionPlan.cs @@ -0,0 +1,3 @@ +namespace FloppyBot.Chat.Twitch.Events; + +public record TwitchSubscriptionPlan(TwitchSubscriptionPlanTier Tier, string Name); diff --git a/src/FloppyBot.Chat.Twitch.Events/TwitchSubscriptionPlanTier.cs b/src/FloppyBot.Chat.Twitch.Events/TwitchSubscriptionPlanTier.cs new file mode 100644 index 00000000..ae66316b --- /dev/null +++ b/src/FloppyBot.Chat.Twitch.Events/TwitchSubscriptionPlanTier.cs @@ -0,0 +1,10 @@ +namespace FloppyBot.Chat.Twitch.Events; + +public enum TwitchSubscriptionPlanTier +{ + Unknown = 0, + Prime = 1, + Tier1 = 10, + Tier2 = 20, + Tier3 = 30, +} diff --git a/src/FloppyBot.Chat.Twitch.Events/TwitchSubscriptionReceivedEvent.cs b/src/FloppyBot.Chat.Twitch.Events/TwitchSubscriptionReceivedEvent.cs new file mode 100644 index 00000000..3a932798 --- /dev/null +++ b/src/FloppyBot.Chat.Twitch.Events/TwitchSubscriptionReceivedEvent.cs @@ -0,0 +1,4 @@ +namespace FloppyBot.Chat.Twitch.Events; + +public record TwitchSubscriptionReceivedEvent(TwitchSubscriptionPlan SubscriptionPlanTier) + : TwitchEvent(TwitchEventTypes.SUBSCRIPTION); diff --git a/src/FloppyBot.Chat.Twitch.Tests/TwitchChatInterfaceTests.cs b/src/FloppyBot.Chat.Twitch.Tests/TwitchChatInterfaceTests.cs index fd64edd6..25df6fff 100644 --- a/src/FloppyBot.Chat.Twitch.Tests/TwitchChatInterfaceTests.cs +++ b/src/FloppyBot.Chat.Twitch.Tests/TwitchChatInterfaceTests.cs @@ -1,11 +1,13 @@ using System.Collections.Generic; using System.Drawing; using System.Linq; +using System.Text.Json; using FakeItEasy; using FloppyBot.Base.Testing; using FloppyBot.Chat.Entities; using FloppyBot.Chat.Entities.Identifiers; using FloppyBot.Chat.Twitch.Config; +using FloppyBot.Chat.Twitch.Events; using FloppyBot.Chat.Twitch.Monitor; using Microsoft.VisualStudio.TestTools.UnitTesting; using TwitchLib.Client; @@ -13,7 +15,7 @@ using TwitchLib.Client.Events; using TwitchLib.Client.Interfaces; using TwitchLib.Client.Models; -using ChatMessage = TwitchLib.Client.Models.ChatMessage; +using ChatMessage = FloppyBot.Chat.Entities.ChatMessage; namespace FloppyBot.Chat.Twitch.Tests; @@ -147,6 +149,78 @@ _configuration with Assert.AreEqual(1, receivedMessages); } + [TestMethod] + public void SubscriptionMessageIsEmitted() + { + TwitchChatInterface chatInterface = CreateInterface(); + var receivedMessages = new List(); + chatInterface.MessageReceived += (_, message) => receivedMessages.Add(message); + _client.OnNewSubscriber += Raise.With( + _client, + new OnNewSubscriberArgs + { + Channel = "atwitchchannel", + Subscriber = new Subscriber( + new List>(), + new List>(), + "000000", + Color.Black, + "ATwitchUser", + "n/a", + "MSG-ID", + "atwitchuser", + string.Empty, + "sub", // see Twitch Chat API docs + "5", + "5", + true, + "SYS-MSG", + "Here is a resub message", + SubscriptionPlan.Tier2, + "Tier 2", + "ROOM-ID", + "USER-ID", + false, + false, + true, + false, + "111111111", + UserType.Viewer, + "RAW-IRC", + "atwitchchannel" + ), + } + ); + + Assert.AreEqual(1, receivedMessages.Count); + Assert.IsTrue(receivedMessages.All(m => m.EventName == TwitchEventTypes.SUBSCRIPTION)); + + var message = receivedMessages.First(); + Assert.AreEqual( + new ChatMessage( + "Twitch/atwitchchannel/MSG-ID", + new ChatUser("Twitch/USER-ID", "ATwitchUser", PrivilegeLevel.Viewer), + TwitchEventTypes.SUBSCRIPTION, + "{\"SubscriptionPlanTier\":{\"Tier\":20,\"Name\":\"Tier 2\"},\"EventName\":\"Twitch.Subscription\"}", + null, + chatInterface.SupportedFeatures + ), + message + ); + + var parsedObject = JsonSerializer.Deserialize( + message.Content + ); + + Assert.IsNotNull(parsedObject); + Assert.AreEqual( + new TwitchSubscriptionReceivedEvent( + new TwitchSubscriptionPlan(TwitchSubscriptionPlanTier.Tier2, "Tier 2") + ), + parsedObject + ); + } + private TwitchChatInterface CreateInterface(TwitchConfiguration? configuration = null) { if (configuration != null) @@ -178,7 +252,7 @@ private OnMessageReceivedArgs CreateChatMessage( { return new OnMessageReceivedArgs { - ChatMessage = new ChatMessage( + ChatMessage = new TwitchLib.Client.Models.ChatMessage( _configuration.Username, "userid", authorUsername, diff --git a/src/FloppyBot.Chat.Twitch/Extensions/TwitchEntityExtensions.cs b/src/FloppyBot.Chat.Twitch/Extensions/TwitchEntityExtensions.cs new file mode 100644 index 00000000..bfd3fda3 --- /dev/null +++ b/src/FloppyBot.Chat.Twitch/Extensions/TwitchEntityExtensions.cs @@ -0,0 +1,62 @@ +using FloppyBot.Chat.Entities; +using FloppyBot.Chat.Entities.Identifiers; +using FloppyBot.Chat.Twitch.Events; +using TwitchLib.Client.Enums; + +namespace FloppyBot.Chat.Twitch.Extensions; + +internal static class TwitchEntityExtensions +{ + internal static ChatUser ConvertToChatUser( + string username, + string displayName, + PrivilegeLevel privilegeLevel = PrivilegeLevel.Unknown + ) + { + return new ChatUser( + new ChannelIdentifier(TwitchChatInterface.IF_NAME, username), + displayName, + privilegeLevel + ); + } + + internal static TwitchSubscriptionPlan ConvertToPlan( + this SubscriptionPlan plan, + string? planName = null + ) + { + var tier = plan.ConvertToInternalEnum(); + return new TwitchSubscriptionPlan(tier, planName ?? tier.GetDefaultName()); + } + + private static TwitchSubscriptionPlanTier ConvertToInternalEnum(this SubscriptionPlan plan) + { + return plan switch + { + SubscriptionPlan.NotSet => TwitchSubscriptionPlanTier.Unknown, + SubscriptionPlan.Prime => TwitchSubscriptionPlanTier.Prime, + SubscriptionPlan.Tier1 => TwitchSubscriptionPlanTier.Tier1, + SubscriptionPlan.Tier2 => TwitchSubscriptionPlanTier.Tier2, + SubscriptionPlan.Tier3 => TwitchSubscriptionPlanTier.Tier3, + _ + => throw new ArgumentOutOfRangeException( + nameof(plan), + plan, + "Subscription plan is not convertible" + ), + }; + } + + private static string GetDefaultName(this TwitchSubscriptionPlanTier planTier) + { + return planTier switch + { + TwitchSubscriptionPlanTier.Unknown => "Unknown", + TwitchSubscriptionPlanTier.Prime => "Prime", + TwitchSubscriptionPlanTier.Tier1 => "Tier 1", + TwitchSubscriptionPlanTier.Tier2 => "Tier 2", + TwitchSubscriptionPlanTier.Tier3 => "Tier 3", + _ => planTier.ToString(), + }; + } +} diff --git a/src/FloppyBot.Chat.Twitch/FloppyBot.Chat.Twitch.csproj b/src/FloppyBot.Chat.Twitch/FloppyBot.Chat.Twitch.csproj index fbde2b83..04d005c3 100644 --- a/src/FloppyBot.Chat.Twitch/FloppyBot.Chat.Twitch.csproj +++ b/src/FloppyBot.Chat.Twitch/FloppyBot.Chat.Twitch.csproj @@ -14,7 +14,9 @@ + + diff --git a/src/FloppyBot.Chat.Twitch/TwitchChatInterface.cs b/src/FloppyBot.Chat.Twitch/TwitchChatInterface.cs index 09380d1d..bc4b9acd 100644 --- a/src/FloppyBot.Chat.Twitch/TwitchChatInterface.cs +++ b/src/FloppyBot.Chat.Twitch/TwitchChatInterface.cs @@ -1,6 +1,9 @@ -using FloppyBot.Chat.Entities; +using System.Text.Json; +using FloppyBot.Chat.Entities; using FloppyBot.Chat.Entities.Identifiers; using FloppyBot.Chat.Twitch.Config; +using FloppyBot.Chat.Twitch.Events; +using FloppyBot.Chat.Twitch.Extensions; using FloppyBot.Chat.Twitch.Monitor; using Microsoft.Extensions.Logging; using TwitchLib.Client; @@ -44,6 +47,11 @@ ITwitchChannelOnlineMonitor onlineMonitor _client.OnJoinedChannel += Client_OnJoinedChannel; _client.OnMessageReceived += Client_OnMessageReceived; _client.OnReconnected += Client_OnReconnected; + + _client.OnNewSubscriber += Client_OnNewSubscriber; + _client.OnReSubscriber += Client_OnReSubscriber; + _client.OnGiftedSubscription += Client_OnGiftedSubscription; + _client.OnCommunitySubscription += Client_OnCommunitySubscription; } public string Name => _channelIdentifier; @@ -98,17 +106,30 @@ private static PrivilegeLevel DeterminePrivilegeLevel( TwitchLib.Client.Models.ChatMessage chatMessage ) { - if (chatMessage.IsBroadcaster) + return DeterminePrivilegeLevel( + chatMessage.IsBroadcaster, + chatMessage.IsModerator, + chatMessage.IsMe + ); + } + + private static PrivilegeLevel DeterminePrivilegeLevel( + bool isBroadcaster, + bool isModerator, + bool isMe + ) + { + if (isBroadcaster) { return PrivilegeLevel.Administrator; } - if (chatMessage.IsModerator) + if (isModerator) { return PrivilegeLevel.Moderator; } - if (chatMessage.IsMe) + if (isMe) { return PrivilegeLevel.Unknown; } @@ -168,8 +189,8 @@ private void Client_OnMessageReceived(object? sender, OnMessageReceivedArgs e) var message = new ChatMessage( NewChatMessageIdentifier(e.ChatMessage.Id), - new ChatUser( - new ChannelIdentifier(IF_NAME, chatMessage.Username), + TwitchEntityExtensions.ConvertToChatUser( + chatMessage.Username, chatMessage.DisplayName, DeterminePrivilegeLevel(chatMessage) ), @@ -182,6 +203,135 @@ private void Client_OnMessageReceived(object? sender, OnMessageReceivedArgs e) MessageReceived?.Invoke(this, message); } + private void Client_OnNewSubscriber(object? sender, OnNewSubscriberArgs e) + { + _logger.LogTrace( + "Received new subscriber {TwitchUser}@{TwitchChannel}: {TwitchSubscriptionPlan}", + e.Subscriber.DisplayName, + e.Subscriber.Channel, + e.Subscriber.SubscriptionPlan + ); + + var eventArgs = new TwitchSubscriptionReceivedEvent( + e.Subscriber.SubscriptionPlan.ConvertToPlan(e.Subscriber.SubscriptionPlanName) + ); + MessageReceived?.Invoke( + this, + new ChatMessage( + NewChatMessageIdentifier(e.Subscriber.Id), + TwitchEntityExtensions.ConvertToChatUser( + e.Subscriber.UserId, + e.Subscriber.DisplayName, + DeterminePrivilegeLevel(false, e.Subscriber.IsModerator, false) + ), + TwitchEventTypes.SUBSCRIPTION, + JsonSerializer.Serialize(eventArgs), + null, + SupportedFeatures + ) + ); + } + + private void Client_OnReSubscriber(object? sender, OnReSubscriberArgs e) + { + _logger.LogTrace( + "Received re-subscriber {TwitchUser}@{TwitchChannel}: {TwitchSubscriptionPlan} for {TwitchSubscriptionMonths} months", + e.ReSubscriber.DisplayName, + e.Channel, + e.ReSubscriber.SubscriptionPlan, + e.ReSubscriber.Months + ); + + var eventArgs = new TwitchReSubscriptionReceivedEvent( + e.ReSubscriber.SubscriptionPlan.ConvertToPlan(e.ReSubscriber.SubscriptionPlanName), + e.ReSubscriber.Months + ); + MessageReceived?.Invoke( + this, + new ChatMessage( + NewChatMessageIdentifier(e.ReSubscriber.Id), + TwitchEntityExtensions.ConvertToChatUser( + e.ReSubscriber.UserId, + e.ReSubscriber.DisplayName, + DeterminePrivilegeLevel(false, e.ReSubscriber.IsModerator, false) + ), + TwitchEventTypes.RE_SUBSCRIPTION, + JsonSerializer.Serialize(eventArgs), + null, + SupportedFeatures + ) + ); + } + + private void Client_OnGiftedSubscription(object? sender, OnGiftedSubscriptionArgs e) + { + _logger.LogTrace( + "Received gifted subscription {TwitchUser}@{TwitchChannel}: {TwitchSubscriptionPlan} to {TwitchGiftRecipient} for {TwitchSubscriptionMonths} months", + e.GiftedSubscription.DisplayName, + e.Channel, + e.GiftedSubscription.MsgParamSubPlan, + e.GiftedSubscription.MsgParamRecipientUserName, + e.GiftedSubscription.MsgParamMonths + ); + + var eventArgs = new TwitchSubscriptionGiftEvent( + e.GiftedSubscription.MsgParamSubPlan.ConvertToPlan( + e.GiftedSubscription.MsgParamSubPlanName + ), + TwitchEntityExtensions.ConvertToChatUser( + e.GiftedSubscription.MsgParamRecipientUserName, + e.GiftedSubscription.MsgParamRecipientDisplayName + ) + ); + MessageReceived?.Invoke( + this, + new ChatMessage( + NewChatMessageIdentifier(e.GiftedSubscription.Id), + TwitchEntityExtensions.ConvertToChatUser( + e.GiftedSubscription.UserId, + e.GiftedSubscription.DisplayName, + DeterminePrivilegeLevel(false, e.GiftedSubscription.IsModerator, false) + ), + TwitchEventTypes.RE_SUBSCRIPTION, + JsonSerializer.Serialize(eventArgs), + null, + SupportedFeatures + ) + ); + } + + private void Client_OnCommunitySubscription(object? sender, OnCommunitySubscriptionArgs e) + { + _logger.LogTrace( + "Received community subscription {TwitchUser}@{TwitchChannel}: User gifted {TwitchMassGiftCount} {TwitchSubscriptionPlan} subscriptions", + e.GiftedSubscription.DisplayName, + e.Channel, + e.GiftedSubscription.MsgParamMassGiftCount, + e.GiftedSubscription.MsgParamSubPlan + ); + + var eventArgs = new TwitchSubscriptionCommunityGiftEvent( + e.GiftedSubscription.MsgParamSubPlan.ConvertToPlan(), + e.GiftedSubscription.MsgParamMassGiftCount, + e.GiftedSubscription.MsgParamMultiMonthGiftDuration + ); + MessageReceived?.Invoke( + this, + new ChatMessage( + NewChatMessageIdentifier(e.GiftedSubscription.Id), + TwitchEntityExtensions.ConvertToChatUser( + e.GiftedSubscription.UserId, + e.GiftedSubscription.DisplayName, + DeterminePrivilegeLevel(false, e.GiftedSubscription.IsModerator, false) + ), + TwitchEventTypes.RE_SUBSCRIPTION, + JsonSerializer.Serialize(eventArgs), + null, + SupportedFeatures + ) + ); + } + private void Client_OnReconnected(object? sender, OnReconnectedEventArgs e) { _logger.LogInformation("Reconnected"); diff --git a/src/FloppyBot.Commands.Aux.Twitch/FloppyBot.Commands.Aux.Twitch.csproj b/src/FloppyBot.Commands.Aux.Twitch/FloppyBot.Commands.Aux.Twitch.csproj index 66f55637..7015fdfc 100644 --- a/src/FloppyBot.Commands.Aux.Twitch/FloppyBot.Commands.Aux.Twitch.csproj +++ b/src/FloppyBot.Commands.Aux.Twitch/FloppyBot.Commands.Aux.Twitch.csproj @@ -8,6 +8,7 @@ + diff --git a/src/FloppyBot.Commands.Aux.Twitch/SubAlertCommands.cs b/src/FloppyBot.Commands.Aux.Twitch/SubAlertCommands.cs new file mode 100644 index 00000000..e53937fa --- /dev/null +++ b/src/FloppyBot.Commands.Aux.Twitch/SubAlertCommands.cs @@ -0,0 +1,102 @@ +using System.Collections.Immutable; +using FloppyBot.Aux.TwitchAlerts.Core; +using FloppyBot.Aux.TwitchAlerts.Core.Entities; +using FloppyBot.Chat.Entities; +using FloppyBot.Commands.Core.Attributes; +using FloppyBot.Commands.Core.Attributes.Args; +using FloppyBot.Commands.Core.Attributes.Dependencies; +using FloppyBot.Commands.Core.Attributes.Guards; +using FloppyBot.Commands.Core.Attributes.Metadata; +using Microsoft.Extensions.DependencyInjection; + +namespace FloppyBot.Commands.Aux.Twitch; + +[CommandHost] +[SourceInterfaceGuard("Twitch")] +[PrivilegeGuard(PrivilegeLevel.Moderator)] +[CommandCategory("Community")] +public class SubAlertCommands +{ + private const string REPLY_ALERT_SET = "✅ Sub Alert Message has been set"; + private const string REPLY_ALERT_CLEAR = "✅ Sub Alert Message has been cleared"; + + private const string CHAT_MESSAGE_FORMAT = + "{User} just subscribed with {SubscriptionTier}! Thank you so much for the support! 🎉"; + + private readonly ITwitchAlertService _alertService; + + public SubAlertCommands(ITwitchAlertService alertService) + { + _alertService = alertService; + } + + [DependencyRegistration] + // ReSharper disable once UnusedMember.Global + public static void RegisterDependencies(IServiceCollection services) + { + services.AddTwitchAlertCore(); + } + + [Command("subalert")] + [CommandDescription("Sets the message to be sent when someone subscribes to the channel.")] + [CommandSyntax("")] + // ReSharper disable once UnusedMember.Global + public string SetAlertMessage( + [SourceChannel] string sourceChannel, + [AllArguments] string message + ) + { + var settings = + _alertService.GetAlertSettings(sourceChannel) + ?? new TwitchAlertSettings { Id = sourceChannel }; + + settings = settings with + { + SubMessage = ImmutableList + .Create() + .Add(new TwitchAlertMessage(message)), + SubAlertsEnabled = true, + }; + + _alertService.StoreAlertSettings(settings); + return REPLY_ALERT_SET; + } + + [Command("clearalert")] + [CommandDescription("Clears the message to be sent when someone subscribes to the channel.")] + [CommandSyntax("")] + // ReSharper disable once UnusedMember.Global + public string ClearAlertMessage([SourceChannel] string sourceChannel) + { + var settings = _alertService.GetAlertSettings(sourceChannel); + if (settings is not null) + { + settings = settings with { SubAlertsEnabled = false, }; + _alertService.StoreAlertSettings(settings); + } + + return REPLY_ALERT_CLEAR; + } + + [Command("setdefaultalert")] + [CommandDescription("Resets the sub alert message to the default message")] + [CommandSyntax("")] + // ReSharper disable once UnusedMember.Global + public string SetDefaultAlertMessage([SourceChannel] string sourceChannel) + { + var settings = + _alertService.GetAlertSettings(sourceChannel) + ?? new TwitchAlertSettings { Id = sourceChannel }; + + settings = settings with + { + SubMessage = ImmutableList + .Create() + .Add(new TwitchAlertMessage(CHAT_MESSAGE_FORMAT)), + SubAlertsEnabled = true, + }; + + _alertService.StoreAlertSettings(settings); + return REPLY_ALERT_SET; + } +} diff --git a/src/FloppyBot.Commands.Executor.Agent/ExecutorAgent.cs b/src/FloppyBot.Commands.Executor.Agent/ExecutorAgent.cs index 2dce3c62..7cecc894 100644 --- a/src/FloppyBot.Commands.Executor.Agent/ExecutorAgent.cs +++ b/src/FloppyBot.Commands.Executor.Agent/ExecutorAgent.cs @@ -3,7 +3,6 @@ using FloppyBot.Chat.Entities; using FloppyBot.Commands.Core.Executor; using FloppyBot.Commands.Core.Replier; -using FloppyBot.Commands.Executor.Agent.DistRegistry; using FloppyBot.Commands.Parser.Entities; using FloppyBot.Communication; using Microsoft.Extensions.Configuration; @@ -16,8 +15,6 @@ public class ExecutorAgent : BackgroundService { private readonly ICommandExecutor _commandExecutor; - private readonly DistributedCommandRegistryAdapter _distributedCommandRegistryAdapter; - private readonly IndexInitializer _indexInitializer; private readonly INotificationReceiver _instructionReceiver; private readonly ILogger _logger; @@ -30,7 +27,6 @@ public ExecutorAgent( INotificationReceiverFactory receiverFactory, ICommandExecutor commandExecutor, IndexInitializer indexInitializer, - DistributedCommandRegistryAdapter distributedCommandRegistryAdapter, IMessageReplier replier ) { @@ -44,7 +40,6 @@ IMessageReplier replier _commandExecutor = commandExecutor; _indexInitializer = indexInitializer; - _distributedCommandRegistryAdapter = distributedCommandRegistryAdapter; _replier = replier; } diff --git a/src/FloppyBot.Commands.Parser.Agent/CommandParsingAgent.cs b/src/FloppyBot.Commands.Parser.Agent/CommandParsingAgent.cs index 0d13f114..0d68c531 100644 --- a/src/FloppyBot.Commands.Parser.Agent/CommandParsingAgent.cs +++ b/src/FloppyBot.Commands.Parser.Agent/CommandParsingAgent.cs @@ -1,4 +1,5 @@ using FloppyBot.Base.Configuration; +using FloppyBot.Chat; using FloppyBot.Chat.Entities; using FloppyBot.Commands.Parser.Entities; using FloppyBot.Communication; @@ -55,6 +56,16 @@ protected override Task ExecuteAsync(CancellationToken stoppingToken) private void OnNotificationReceived(ChatMessage notification) { + if (notification.EventName != SharedEventTypes.CHAT_MESSAGE) + { + // Events that aren't chat messages are ignored. + _logger.LogDebug( + "Received notification with event name {EventName}, ignoring", + notification.EventName + ); + return; + } + #if DEBUG _logger.LogInformation("Received chat message to parse: {@ChatMessage}", notification); #endif diff --git a/src/FloppyBot.sln b/src/FloppyBot.sln index adb54873..e69925d0 100644 --- a/src/FloppyBot.sln +++ b/src/FloppyBot.sln @@ -180,6 +180,16 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FloppyBot.Aux.MessageCounte EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FloppyBot.Aux.MessageCounter.Agent", "FloppyBot.Aux.MessageCounter.Agent\FloppyBot.Aux.MessageCounter.Agent.csproj", "{90175374-9AF5-4413-B679-5B2601B0F0E6}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FloppyBot.Chat.Twitch.Events", "FloppyBot.Chat.Twitch.Events\FloppyBot.Chat.Twitch.Events.csproj", "{B8C87D58-A7C4-47F1-AD97-014718507493}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "MessageCounter", "MessageCounter", "{FCF25164-838D-4EDF-95DB-D304E4A5CD66}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "TwitchAlerts", "TwitchAlerts", "{F77A1C8D-1D24-4C7E-B3B6-94054E861B2F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FloppyBot.Aux.TwitchAlerts.Core", "FloppyBot.Aux.TwitchAlerts.Core\FloppyBot.Aux.TwitchAlerts.Core.csproj", "{3DB9575C-A45C-43C3-A393-689F095D5C40}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FloppyBot.Aux.TwitchAlerts.Agent", "FloppyBot.Aux.TwitchAlerts.Agent\FloppyBot.Aux.TwitchAlerts.Agent.csproj", "{74116ADF-DE02-4091-BED7-430E45E2DE91}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -265,8 +275,13 @@ Global {DE6DCAA6-4E22-484E-BAF6-ED7DE2FD523E} = {EBBD7204-782A-4642-AFBF-F52D97C0B985} {0DC72F20-5325-44E0-AA31-F00BBAF2F110} = {DE6DCAA6-4E22-484E-BAF6-ED7DE2FD523E} {0170115D-E0FE-491A-8479-B78D8A32877E} = {DE6DCAA6-4E22-484E-BAF6-ED7DE2FD523E} - {33B0D4C4-4D5D-4989-9A9C-4DC83D2300F1} = {035EAE62-BFE4-4664-B314-DBC7099701C1} - {90175374-9AF5-4413-B679-5B2601B0F0E6} = {035EAE62-BFE4-4664-B314-DBC7099701C1} + {B8C87D58-A7C4-47F1-AD97-014718507493} = {374B5765-6FDD-4876-AEB0-9CAB04C58472} + {FCF25164-838D-4EDF-95DB-D304E4A5CD66} = {035EAE62-BFE4-4664-B314-DBC7099701C1} + {90175374-9AF5-4413-B679-5B2601B0F0E6} = {FCF25164-838D-4EDF-95DB-D304E4A5CD66} + {33B0D4C4-4D5D-4989-9A9C-4DC83D2300F1} = {FCF25164-838D-4EDF-95DB-D304E4A5CD66} + {F77A1C8D-1D24-4C7E-B3B6-94054E861B2F} = {035EAE62-BFE4-4664-B314-DBC7099701C1} + {3DB9575C-A45C-43C3-A393-689F095D5C40} = {F77A1C8D-1D24-4C7E-B3B6-94054E861B2F} + {74116ADF-DE02-4091-BED7-430E45E2DE91} = {F77A1C8D-1D24-4C7E-B3B6-94054E861B2F} EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {67258FB8-8B58-4F1B-B099-3F177074B50F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU @@ -521,5 +536,17 @@ Global {90175374-9AF5-4413-B679-5B2601B0F0E6}.Debug|Any CPU.Build.0 = Debug|Any CPU {90175374-9AF5-4413-B679-5B2601B0F0E6}.Release|Any CPU.ActiveCfg = Release|Any CPU {90175374-9AF5-4413-B679-5B2601B0F0E6}.Release|Any CPU.Build.0 = Release|Any CPU + {B8C87D58-A7C4-47F1-AD97-014718507493}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B8C87D58-A7C4-47F1-AD97-014718507493}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B8C87D58-A7C4-47F1-AD97-014718507493}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B8C87D58-A7C4-47F1-AD97-014718507493}.Release|Any CPU.Build.0 = Release|Any CPU + {3DB9575C-A45C-43C3-A393-689F095D5C40}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3DB9575C-A45C-43C3-A393-689F095D5C40}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3DB9575C-A45C-43C3-A393-689F095D5C40}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3DB9575C-A45C-43C3-A393-689F095D5C40}.Release|Any CPU.Build.0 = Release|Any CPU + {74116ADF-DE02-4091-BED7-430E45E2DE91}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {74116ADF-DE02-4091-BED7-430E45E2DE91}.Debug|Any CPU.Build.0 = Debug|Any CPU + {74116ADF-DE02-4091-BED7-430E45E2DE91}.Release|Any CPU.ActiveCfg = Release|Any CPU + {74116ADF-DE02-4091-BED7-430E45E2DE91}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal