From a1ef35d0a7815eaee9384b7de0005cecf54c89fe Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Mon, 26 May 2025 11:56:43 -0500 Subject: [PATCH] feat: begin adding subscription to YT notifications --- src/src/StevesBot.Webhook/Program.cs | 13 ++++- .../StevesBot.Webhook.csproj | 2 + .../StevesBot.Webhook/SubscriptionWorker.cs | 49 +++++++++++++++++++ src/src/StevesBot.Webhook/Usings.cs | 6 +++ .../YouTube/IPubSubClient.cs | 16 ++++++ .../StevesBot.Webhook/YouTube/PubSubClient.cs | 32 ++++++++++++ .../YouTube/PubSubClientException.cs | 16 ++++++ .../YouTube/SubscriptionOptions.cs | 10 ++++ .../appsettings.Example.json | 6 +++ 9 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 src/src/StevesBot.Webhook/SubscriptionWorker.cs create mode 100644 src/src/StevesBot.Webhook/Usings.cs create mode 100644 src/src/StevesBot.Webhook/YouTube/IPubSubClient.cs create mode 100644 src/src/StevesBot.Webhook/YouTube/PubSubClient.cs create mode 100644 src/src/StevesBot.Webhook/YouTube/PubSubClientException.cs create mode 100644 src/src/StevesBot.Webhook/YouTube/SubscriptionOptions.cs create mode 100644 src/src/StevesBot.Webhook/appsettings.Example.json diff --git a/src/src/StevesBot.Webhook/Program.cs b/src/src/StevesBot.Webhook/Program.cs index b5041fa..16fa3cd 100644 --- a/src/src/StevesBot.Webhook/Program.cs +++ b/src/src/StevesBot.Webhook/Program.cs @@ -1,7 +1,18 @@ var builder = WebApplication.CreateBuilder(args); +builder.Services + .AddOptionsWithValidateOnStart() + .BindConfiguration(nameof(SubscriptionOptions)) + .ValidateDataAnnotations(); + +builder.Services + .AddHttpClient() + .AddStandardResilienceHandler(); + builder.Services.AddOpenApi(); +builder.Services.AddHostedService(); + var app = builder.Build(); if (app.Environment.IsDevelopment()) @@ -11,4 +22,4 @@ if (app.Environment.IsDevelopment()) app.UseHttpsRedirection(); -app.Run(); +app.Run(); \ No newline at end of file diff --git a/src/src/StevesBot.Webhook/StevesBot.Webhook.csproj b/src/src/StevesBot.Webhook/StevesBot.Webhook.csproj index 250aa1e..b26a484 100644 --- a/src/src/StevesBot.Webhook/StevesBot.Webhook.csproj +++ b/src/src/StevesBot.Webhook/StevesBot.Webhook.csproj @@ -8,6 +8,8 @@ + + diff --git a/src/src/StevesBot.Webhook/SubscriptionWorker.cs b/src/src/StevesBot.Webhook/SubscriptionWorker.cs new file mode 100644 index 0000000..436ab1e --- /dev/null +++ b/src/src/StevesBot.Webhook/SubscriptionWorker.cs @@ -0,0 +1,49 @@ +namespace StevesBot.Webhook; + +internal sealed class SubscriptionWorker( + ILogger logger, + IOptions options, + IPubSubClient pubSubClient +) : IHostedLifecycleService +{ + private readonly ILogger _logger = logger; + private readonly SubscriptionOptions _options = options.Value; + private readonly IPubSubClient _pubSubClient = pubSubClient; + + public Task StartAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + public Task StartingAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + public async Task StartedAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Subscribing to notifications"); + + await _pubSubClient.SubscribeAsync( + _options.CallbackBaseUrl, + _options.TopicUrl, + cancellationToken + ); + } + + + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + public Task StoppingAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + public Task StoppedAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/src/src/StevesBot.Webhook/Usings.cs b/src/src/StevesBot.Webhook/Usings.cs new file mode 100644 index 0000000..6deeb72 --- /dev/null +++ b/src/src/StevesBot.Webhook/Usings.cs @@ -0,0 +1,6 @@ +global using System.ComponentModel.DataAnnotations; + +global using Microsoft.Extensions.Options; + +global using StevesBot.Webhook; +global using StevesBot.Webhook.YouTube; \ No newline at end of file diff --git a/src/src/StevesBot.Webhook/YouTube/IPubSubClient.cs b/src/src/StevesBot.Webhook/YouTube/IPubSubClient.cs new file mode 100644 index 0000000..4eb39f5 --- /dev/null +++ b/src/src/StevesBot.Webhook/YouTube/IPubSubClient.cs @@ -0,0 +1,16 @@ +namespace StevesBot.Webhook.YouTube; + +internal interface IPubSubClient +{ + Task SubscribeAsync( + string callbackUrl, + string topicUrl, + CancellationToken cancellationToken = default + ); + + Task UnsubscribeAsync( + string callbackUrl, + string topicUrl, + CancellationToken cancellationToken = default + ); +} \ No newline at end of file diff --git a/src/src/StevesBot.Webhook/YouTube/PubSubClient.cs b/src/src/StevesBot.Webhook/YouTube/PubSubClient.cs new file mode 100644 index 0000000..0c79a26 --- /dev/null +++ b/src/src/StevesBot.Webhook/YouTube/PubSubClient.cs @@ -0,0 +1,32 @@ + +namespace StevesBot.Webhook.YouTube; + +internal sealed class PubSubClient(HttpClient httpClient) : IPubSubClient +{ + private const string SubscribeEndpoint = "subscribe"; + private readonly HttpClient _httpClient = httpClient; + + public async Task SubscribeAsync(string callbackUrl, string topicUrl, CancellationToken cancellationToken = default) + { + var uri = new Uri(SubscribeEndpoint, UriKind.Relative); + var formFields = new Dictionary() + { + { "hub.callback", callbackUrl }, + { "hub.topic", topicUrl }, + { "hub.verify", "async" }, + { "hub.mode", "subscribe" } + }; + using var form = new FormUrlEncodedContent(formFields); + var response = await _httpClient.PostAsync(uri, form, cancellationToken); + + if (response.IsSuccessStatusCode is false) + { + throw new PubSubClientException("Failed to subscribe"); + } + } + + public Task UnsubscribeAsync(string callbackUrl, string topicUrl, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } +} diff --git a/src/src/StevesBot.Webhook/YouTube/PubSubClientException.cs b/src/src/StevesBot.Webhook/YouTube/PubSubClientException.cs new file mode 100644 index 0000000..27aa0ec --- /dev/null +++ b/src/src/StevesBot.Webhook/YouTube/PubSubClientException.cs @@ -0,0 +1,16 @@ +namespace StevesBot.Webhook.YouTube; + +internal sealed class PubSubClientException : Exception +{ + public PubSubClientException() + { + } + + public PubSubClientException(string message) : base(message) + { + } + + public PubSubClientException(string message, Exception innerException) : base(message, innerException) + { + } +} \ No newline at end of file diff --git a/src/src/StevesBot.Webhook/YouTube/SubscriptionOptions.cs b/src/src/StevesBot.Webhook/YouTube/SubscriptionOptions.cs new file mode 100644 index 0000000..da8e9f1 --- /dev/null +++ b/src/src/StevesBot.Webhook/YouTube/SubscriptionOptions.cs @@ -0,0 +1,10 @@ +namespace StevesBot.Webhook.YouTube; + +internal sealed record SubscriptionOptions +{ + [Required] + public string CallbackBaseUrl { get; init; } = string.Empty; + + [Required] + public string TopicUrl { get; init; } = string.Empty; +} \ No newline at end of file diff --git a/src/src/StevesBot.Webhook/appsettings.Example.json b/src/src/StevesBot.Webhook/appsettings.Example.json new file mode 100644 index 0000000..87686fb --- /dev/null +++ b/src/src/StevesBot.Webhook/appsettings.Example.json @@ -0,0 +1,6 @@ +{ + "SubscriptionOptions": { + "CallbackUrl": "CallbackUrl", + "TopicUrl": "TopicUrl" + } +}