diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 358bb08..11f3ea0 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -60,6 +60,10 @@ jobs: --restart always \ -d \ -p 4343:8080 \ + -e StreamerBotOptions__Host=${{ secrets.STREAMER_BOT_HOST }} \ + -e StreamerBotOptions__Post=${{ secrets.STREAMER_BOT_PORT }} \ + -e StreamerBotOptions__ApiKey=${{ secrets.STREAMER_BOT_API_KEY }} \ + -e StreamerBotOptions__TextToSpeechActionId=${{ secrets.STREAMER_BOT_ACTION_ID }} \ -e StripeOptions__ApiKey=${{ secrets.STRIPE_API_KEY }} \ -e StripeOptions__EventsWebhookSecret=${{ secrets.STRIPE_EVENTS_WEBHOOK_SECRET }} \ -e ContextOptions__DatabaseFilePath=/app/data/groundsforsupport.db \ diff --git a/src/GroundsForSupport.Server/Payments/Endpoints/EventsEndpoint.cs b/src/GroundsForSupport.Server/Payments/Endpoints/EventsEndpoint.cs index 05feb28..3197fda 100644 --- a/src/GroundsForSupport.Server/Payments/Endpoints/EventsEndpoint.cs +++ b/src/GroundsForSupport.Server/Payments/Endpoints/EventsEndpoint.cs @@ -1,5 +1,6 @@ using GroundsForSupport.Server.Data; using GroundsForSupport.Server.Payments.Stripe; +using GroundsForSupport.Server.TextToSpeech; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; @@ -22,7 +23,8 @@ internal static class EventsEndpoint HttpContext httpContext, [FromServices] IOptions options, [FromServices] Context dbContext, - [FromServices] TimeProvider timeProvider + [FromServices] TimeProvider timeProvider, + [FromServices] IStreamerBotService streamerBotService ) { var json = await new StreamReader(httpContext.Request.Body).ReadToEndAsync(); @@ -59,6 +61,12 @@ internal static class EventsEndpoint dbContext.Payments.Add(payment); await dbContext.SaveChangesAsync(); + await streamerBotService.TriggerTextToSpeechAsync( + name, + paymentIntent.AmountReceived, + message + ); + return Results.Ok(); } catch (StripeException e) diff --git a/src/GroundsForSupport.Server/Payments/Stripe/StripeService.cs b/src/GroundsForSupport.Server/Payments/Stripe/StripeService.cs index 59498a9..6f7a8eb 100644 --- a/src/GroundsForSupport.Server/Payments/Stripe/StripeService.cs +++ b/src/GroundsForSupport.Server/Payments/Stripe/StripeService.cs @@ -14,10 +14,10 @@ internal sealed class StripeService( private readonly ILogger _logger = logger; public async Task<(bool IsSuccess, Intent Intent)> CreatePaymentIntentAsync( - string name, - decimal amount, - string? message, - string? email, + string name, + decimal amount, + string? message, + string? email, CancellationToken cancellationToken = default ) { diff --git a/src/GroundsForSupport.Server/Program.cs b/src/GroundsForSupport.Server/Program.cs index 4bd333b..29de23b 100644 --- a/src/GroundsForSupport.Server/Program.cs +++ b/src/GroundsForSupport.Server/Program.cs @@ -5,6 +5,7 @@ using GroundsForSupport.Server.Logging; using GroundsForSupport.Server.Payments.Endpoints; using GroundsForSupport.Server.Payments.Stripe; using GroundsForSupport.Server.RateLimiting; +using GroundsForSupport.Server.TextToSpeech; using Microsoft.AspNetCore.HttpOverrides; @@ -34,6 +35,9 @@ builder.Services.AddHostedService(); builder.Services.ConfigureOptions(); builder.Services.AddSingleton(); +builder.Services.ConfigureOptions(); +builder.Services.AddSingleton(); + builder.Services.ConfigureHttpJsonOptions( static options => options.SerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.CamelCase ); diff --git a/src/GroundsForSupport.Server/TextToSpeech/IStreamerBotService.cs b/src/GroundsForSupport.Server/TextToSpeech/IStreamerBotService.cs new file mode 100644 index 0000000..94aec38 --- /dev/null +++ b/src/GroundsForSupport.Server/TextToSpeech/IStreamerBotService.cs @@ -0,0 +1,11 @@ +namespace GroundsForSupport.Server.TextToSpeech; + +internal interface IStreamerBotService +{ + Task TriggerTextToSpeechAsync( + string name, + long amount, + string message, + CancellationToken cancellationToken = default + ); +} \ No newline at end of file diff --git a/src/GroundsForSupport.Server/TextToSpeech/StreamerBotOptions.cs b/src/GroundsForSupport.Server/TextToSpeech/StreamerBotOptions.cs new file mode 100644 index 0000000..5db9d99 --- /dev/null +++ b/src/GroundsForSupport.Server/TextToSpeech/StreamerBotOptions.cs @@ -0,0 +1,32 @@ +using Microsoft.Extensions.Options; + +namespace GroundsForSupport.Server.TextToSpeech; + +internal sealed record StreamerBotOptions +{ + public string Host { get; init; } = string.Empty; + public int Port { get; init; } + public string ApiKey { get; init; } = string.Empty; + public string TextToSpeechActionId { get; init; } = string.Empty; + + public string GetBaseUrl() + { + return $"http://{Host}:{Port}"; + } +} + +internal sealed record StreamerBotOptionsSetup : IConfigureOptions +{ + private const string SectionName = nameof(StreamerBotOptions); + private readonly IConfiguration _configuration; + + public StreamerBotOptionsSetup(IConfiguration configuration) + { + _configuration = configuration; + } + + public void Configure(StreamerBotOptions options) + { + _configuration.GetSection(SectionName).Bind(options); + } +} \ No newline at end of file diff --git a/src/GroundsForSupport.Server/TextToSpeech/StreamerBotService.cs b/src/GroundsForSupport.Server/TextToSpeech/StreamerBotService.cs new file mode 100644 index 0000000..8b6d66b --- /dev/null +++ b/src/GroundsForSupport.Server/TextToSpeech/StreamerBotService.cs @@ -0,0 +1,46 @@ +using System.Text.Json; + +using Microsoft.Extensions.Options; + +namespace GroundsForSupport.Server.TextToSpeech; + +internal sealed class StreamerBotService( + IHttpClientFactory httpClientFactory, + IOptions options +) : IStreamerBotService +{ + private const string DoActionEndpoint = "/DoAction"; + private readonly IHttpClientFactory _httpClientFactory = httpClientFactory; + private readonly StreamerBotOptions _options = options.Value; + + public Task TriggerTextToSpeechAsync(string name, long amount, string message, CancellationToken cancellationToken = default) + { + var client = _httpClientFactory.CreateClient(); + var url = new UriBuilder(_options.GetBaseUrl()) + { + Path = DoActionEndpoint + }.Uri; + + var request = new HttpRequestMessage(HttpMethod.Post, url); + + var body = new + { + action = new + { + id = _options.TextToSpeechActionId + }, + args = new + { + name, + amount = $"${amount / 100.0:F2}", + message, + apiKey = _options.ApiKey + } + }; + + var json = JsonSerializer.Serialize(body); + request.Content = new StringContent(json, System.Text.Encoding.UTF8, "application/json"); + + return client.SendAsync(request, cancellationToken); + } +} \ No newline at end of file diff --git a/src/GroundsForSupport.Server/appsettings.Example.json b/src/GroundsForSupport.Server/appsettings.Example.json index 07f8b1e..afdb54e 100644 --- a/src/GroundsForSupport.Server/appsettings.Example.json +++ b/src/GroundsForSupport.Server/appsettings.Example.json @@ -5,5 +5,11 @@ }, "ContextOptions": { "DatabaseFilePath": "DatabaseFilePath" + }, + "StreamerBotOptions": { + "Host": "Host", + "Port": 1234, + "ApiKey": "ApiKey", + "TextToSpeechActionId": "TextToSpeechActionId" } } diff --git a/tests/GroundsForSupport.Server.Tests/Integration/Infra/TestApi.cs b/tests/GroundsForSupport.Server.Tests/Integration/Infra/TestApi.cs index 34890c8..58c2bee 100644 --- a/tests/GroundsForSupport.Server.Tests/Integration/Infra/TestApi.cs +++ b/tests/GroundsForSupport.Server.Tests/Integration/Infra/TestApi.cs @@ -1,11 +1,12 @@ -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Mvc.Testing; +using System.Text; +using System.Text.Json; + using GroundsForSupport.Server.Data; using GroundsForSupport.Server.Payments.Stripe; -using System.Text.Json; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Extensions.Configuration; -using System.Text; namespace GroundsForSupport.Server.Tests.Integration.Infra; @@ -25,7 +26,7 @@ public sealed class TestApi : WebApplicationFactory var contextOptionsJson = JsonSerializer.Serialize(contextOptions); var stripeOptionsJson = JsonSerializer.Serialize(stripeOptions); - + var configJson = $@"{{ ""{nameof(ContextOptions)}"": {contextOptionsJson}, ""{nameof(StripeOptions)}"": {stripeOptionsJson} diff --git a/tests/GroundsForSupport.Server.Tests/Integration/PaymentTests.cs b/tests/GroundsForSupport.Server.Tests/Integration/PaymentTests.cs index dbfbe2a..4fa5221 100644 --- a/tests/GroundsForSupport.Server.Tests/Integration/PaymentTests.cs +++ b/tests/GroundsForSupport.Server.Tests/Integration/PaymentTests.cs @@ -69,7 +69,7 @@ public sealed class PaymentTests(TestApi api) : IClassFixture ) ) .ReturnsAsync((true, new Intent("pi_12345_secret_67890"))); - + var api = _api.WithWebHostBuilder( b => b.ConfigureTestServices( s => s.AddSingleton(mockStripeService.Object)