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