feat: implement triggering text to speech on successful payment

This commit is contained in:
Stevan Freeborn
2026-01-05 21:56:14 -06:00
parent 7022894ccb
commit 961142ac07
10 changed files with 123 additions and 11 deletions
+4
View File
@@ -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 \
@@ -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<StripeOptions> 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)
+4
View File
@@ -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<MigrationService>();
builder.Services.ConfigureOptions<StripeOptionsSetup>();
builder.Services.AddSingleton<IStripeService, StripeService>();
builder.Services.ConfigureOptions<StreamerBotOptionsSetup>();
builder.Services.AddSingleton<IStreamerBotService, StreamerBotService>();
builder.Services.ConfigureHttpJsonOptions(
static options => options.SerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.CamelCase
);
@@ -0,0 +1,11 @@
namespace GroundsForSupport.Server.TextToSpeech;
internal interface IStreamerBotService
{
Task TriggerTextToSpeechAsync(
string name,
long amount,
string message,
CancellationToken cancellationToken = default
);
}
@@ -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<StreamerBotOptions>
{
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);
}
}
@@ -0,0 +1,46 @@
using System.Text.Json;
using Microsoft.Extensions.Options;
namespace GroundsForSupport.Server.TextToSpeech;
internal sealed class StreamerBotService(
IHttpClientFactory httpClientFactory,
IOptions<StreamerBotOptions> 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);
}
}
@@ -5,5 +5,11 @@
},
"ContextOptions": {
"DatabaseFilePath": "DatabaseFilePath"
},
"StreamerBotOptions": {
"Host": "Host",
"Port": 1234,
"ApiKey": "ApiKey",
"TextToSpeechActionId": "TextToSpeechActionId"
}
}
@@ -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;