diff --git a/src/src/StevesBot.Webhook/Program.cs b/src/src/StevesBot.Webhook/Program.cs index 115485d..03ca99f 100644 --- a/src/src/StevesBot.Webhook/Program.cs +++ b/src/src/StevesBot.Webhook/Program.cs @@ -1,5 +1,3 @@ -using Microsoft.AspNetCore.Mvc; - var builder = WebApplication.CreateBuilder(args); builder.Services @@ -28,25 +26,7 @@ app.UseHttpsRedirection(); const string ytCallback = "yt-callback"; -app.MapGet( - ytCallback, - static ( - [FromQuery(Name = "hub.mode")] string mode, - [FromQuery(Name = "hub.topic")] string topic, - [FromQuery(Name = "hub.challenge")] string challenge, - [FromServices] IOptions subOptions, - [FromServices] ILogger logger - ) => - { - if (topic != subOptions.Value.TopicUrl) - { - logger.LogInformation("Received verification request for wrong topic: {Topic}", topic); - return Results.NotFound(); - } - - return Results.Text(challenge); - } -); +app.MapGet(ytCallback, VerifySubscriptionHandler.HandleAsync); // TODO: Implement logic to do the following: // - extract video id from notification diff --git a/src/src/StevesBot.Webhook/Usings.cs b/src/src/StevesBot.Webhook/Usings.cs index 6deeb72..b901b81 100644 --- a/src/src/StevesBot.Webhook/Usings.cs +++ b/src/src/StevesBot.Webhook/Usings.cs @@ -1,6 +1,7 @@ global using System.ComponentModel.DataAnnotations; +global using Microsoft.AspNetCore.Mvc; global using Microsoft.Extensions.Options; global using StevesBot.Webhook; -global using StevesBot.Webhook.YouTube; \ No newline at end of file +global using StevesBot.Webhook.YouTube; diff --git a/src/src/StevesBot.Webhook/YouTube/IPubSubClient.cs b/src/src/StevesBot.Webhook/YouTube/IPubSubClient.cs index 4eb39f5..1d19566 100644 --- a/src/src/StevesBot.Webhook/YouTube/IPubSubClient.cs +++ b/src/src/StevesBot.Webhook/YouTube/IPubSubClient.cs @@ -2,13 +2,7 @@ namespace StevesBot.Webhook.YouTube; internal interface IPubSubClient { - Task SubscribeAsync( - string callbackUrl, - string topicUrl, - CancellationToken cancellationToken = default - ); - - Task UnsubscribeAsync( + Task SubscribeAsync( string callbackUrl, string topicUrl, CancellationToken cancellationToken = default diff --git a/src/src/StevesBot.Webhook/YouTube/PubSubClient.cs b/src/src/StevesBot.Webhook/YouTube/PubSubClient.cs index 0c79a26..0329764 100644 --- a/src/src/StevesBot.Webhook/YouTube/PubSubClient.cs +++ b/src/src/StevesBot.Webhook/YouTube/PubSubClient.cs @@ -1,12 +1,15 @@ - namespace StevesBot.Webhook.YouTube; -internal sealed class PubSubClient(HttpClient httpClient) : IPubSubClient +internal sealed class PubSubClient( + HttpClient httpClient, + ILogger logger +) : IPubSubClient { private const string SubscribeEndpoint = "subscribe"; private readonly HttpClient _httpClient = httpClient; + private readonly ILogger _logger = logger; - public async Task SubscribeAsync(string callbackUrl, string topicUrl, CancellationToken cancellationToken = default) + public async Task SubscribeAsync(string callbackUrl, string topicUrl, CancellationToken cancellationToken = default) { var uri = new Uri(SubscribeEndpoint, UriKind.Relative); var formFields = new Dictionary() @@ -21,12 +24,16 @@ internal sealed class PubSubClient(HttpClient httpClient) : IPubSubClient if (response.IsSuccessStatusCode is false) { - throw new PubSubClientException("Failed to subscribe"); - } - } + var responseContent = await response.Content.ReadAsStringAsync(cancellationToken); - public Task UnsubscribeAsync(string callbackUrl, string topicUrl, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); + _logger.LogDebug( + "Failed to subscribe to topic {TopicUrl} with callback {CallbackUrl}: {ErrorMessage}", + topicUrl, + callbackUrl, + responseContent + ); + } + + return response.IsSuccessStatusCode; } -} +} \ No newline at end of file diff --git a/src/src/StevesBot.Webhook/YouTube/PubSubClientException.cs b/src/src/StevesBot.Webhook/YouTube/PubSubClientException.cs deleted file mode 100644 index 27aa0ec..0000000 --- a/src/src/StevesBot.Webhook/YouTube/PubSubClientException.cs +++ /dev/null @@ -1,16 +0,0 @@ -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/SubscriptionWorker.cs b/src/src/StevesBot.Webhook/YouTube/SubscriptionWorker.cs similarity index 79% rename from src/src/StevesBot.Webhook/SubscriptionWorker.cs rename to src/src/StevesBot.Webhook/YouTube/SubscriptionWorker.cs index f4bc64b..bf6e100 100644 --- a/src/src/StevesBot.Webhook/SubscriptionWorker.cs +++ b/src/src/StevesBot.Webhook/YouTube/SubscriptionWorker.cs @@ -1,4 +1,4 @@ -namespace StevesBot.Webhook; +namespace StevesBot.Webhook.YouTube; internal sealed class SubscriptionWorker( ILogger logger, @@ -24,11 +24,19 @@ internal sealed class SubscriptionWorker( { _logger.LogInformation("Subscribing to notifications"); - await _pubSubClient.SubscribeAsync( + var isSubscribed = await _pubSubClient.SubscribeAsync( _options.CallbackUrl, _options.TopicUrl, cancellationToken ); + + if (isSubscribed) + { + _logger.LogInformation("Successfully subscribed to notifications"); + return; + } + + _logger.LogWarning("Failed to subscribe to notifications"); } diff --git a/src/src/StevesBot.Webhook/YouTube/Tasks/SubscribeTask.cs b/src/src/StevesBot.Webhook/YouTube/Tasks/SubscribeTask.cs new file mode 100644 index 0000000..4c03a4a --- /dev/null +++ b/src/src/StevesBot.Webhook/YouTube/Tasks/SubscribeTask.cs @@ -0,0 +1,6 @@ +namespace StevesBot.Webhook.YouTube.Tasks; + +internal sealed record SubscribeTask : SubscriptionTask +{ + public DateTimeOffset ExpiresAt { get; init; } +} diff --git a/src/src/StevesBot.Webhook/YouTube/Tasks/SubscriptionTask.cs b/src/src/StevesBot.Webhook/YouTube/Tasks/SubscriptionTask.cs new file mode 100644 index 0000000..888a649 --- /dev/null +++ b/src/src/StevesBot.Webhook/YouTube/Tasks/SubscriptionTask.cs @@ -0,0 +1,7 @@ +namespace StevesBot.Webhook.YouTube.Tasks; + +internal abstract record SubscriptionTask +{ + public string CallbackUrl { get; init; } = string.Empty; + public string TopicUrl { get; init; } = string.Empty; +} diff --git a/src/src/StevesBot.Webhook/YouTube/Tasks/UnsubscribeTask.cs b/src/src/StevesBot.Webhook/YouTube/Tasks/UnsubscribeTask.cs new file mode 100644 index 0000000..b678e77 --- /dev/null +++ b/src/src/StevesBot.Webhook/YouTube/Tasks/UnsubscribeTask.cs @@ -0,0 +1,5 @@ +namespace StevesBot.Webhook.YouTube.Tasks; + +internal sealed record UnsubscribeTask : SubscriptionTask +{ +} \ No newline at end of file diff --git a/src/src/StevesBot.Webhook/YouTube/VerifySubscriptionHandler.cs b/src/src/StevesBot.Webhook/YouTube/VerifySubscriptionHandler.cs new file mode 100644 index 0000000..c20275f --- /dev/null +++ b/src/src/StevesBot.Webhook/YouTube/VerifySubscriptionHandler.cs @@ -0,0 +1,55 @@ +namespace StevesBot.Webhook.YouTube; + +internal static class VerifySubscriptionHandler +{ + public static IResult HandleAsync( + [FromQuery(Name = "hub.mode")] string mode, + [FromQuery(Name = "hub.topic")] string topic, + [FromQuery(Name = "hub.reason")] string? reason, + [FromQuery(Name = "hub.challenge")] string? challenge, + [FromQuery(Name = "hub.lease_seconds")] string? leaseSeconds, + [FromServices] IOptions subOptions, + [FromServices] ILogger logger + ) + { + if (mode is "denied") + { + logger.LogInformation("Received subscription denial for topic: {Topic}, reason: {Reason}", topic, reason); + return Results.BadRequest("Subscription denied"); + } + + if (topic != subOptions.Value.TopicUrl) + { + logger.LogInformation("Received verification request for wrong topic: {Topic}", topic); + return Results.NotFound(); + } + + if (mode is "subscribe") + { + // TODO: If it is a subscription request + // we need to queue up a resubscription + // request to be executed just before + // the hub.lease expires. + logger.LogInformation( + "Received subscription request for topic: {Topic}, challenge: {Challenge}, lease: {LeaseSeconds}", + topic, + challenge, + leaseSeconds + ); + } + + if (mode is "unsubscribe") + { + // TODO: If it is a unsubscription request + // we need to queue up an ubsubscription + // request to be executed immediately + logger.LogInformation( + "Received unsubscription request for topic: {Topic}, challenge: {Challenge}", + topic, + challenge + ); + } + + return Results.Text(challenge); + } +} \ No newline at end of file