feat: begin working through how we will handle resubscribing and unsubscribing

This commit is contained in:
Stevan Freeborn
2025-05-26 22:55:08 -05:00
parent 0b42739cbb
commit 6543387270
10 changed files with 104 additions and 57 deletions
+1 -21
View File
@@ -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<SubscriptionOptions> subOptions,
[FromServices] ILogger<Program> 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
+2 -1
View File
@@ -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;
global using StevesBot.Webhook.YouTube;
@@ -2,13 +2,7 @@ namespace StevesBot.Webhook.YouTube;
internal interface IPubSubClient
{
Task SubscribeAsync(
string callbackUrl,
string topicUrl,
CancellationToken cancellationToken = default
);
Task UnsubscribeAsync(
Task<bool> SubscribeAsync(
string callbackUrl,
string topicUrl,
CancellationToken cancellationToken = default
@@ -1,12 +1,15 @@
namespace StevesBot.Webhook.YouTube;
internal sealed class PubSubClient(HttpClient httpClient) : IPubSubClient
internal sealed class PubSubClient(
HttpClient httpClient,
ILogger<PubSubClient> logger
) : IPubSubClient
{
private const string SubscribeEndpoint = "subscribe";
private readonly HttpClient _httpClient = httpClient;
private readonly ILogger<PubSubClient> _logger = logger;
public async Task SubscribeAsync(string callbackUrl, string topicUrl, CancellationToken cancellationToken = default)
public async Task<bool> SubscribeAsync(string callbackUrl, string topicUrl, CancellationToken cancellationToken = default)
{
var uri = new Uri(SubscribeEndpoint, UriKind.Relative);
var formFields = new Dictionary<string, string>()
@@ -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;
}
}
}
@@ -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)
{
}
}
@@ -1,4 +1,4 @@
namespace StevesBot.Webhook;
namespace StevesBot.Webhook.YouTube;
internal sealed class SubscriptionWorker(
ILogger<SubscriptionWorker> 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");
}
@@ -0,0 +1,6 @@
namespace StevesBot.Webhook.YouTube.Tasks;
internal sealed record SubscribeTask : SubscriptionTask
{
public DateTimeOffset ExpiresAt { get; init; }
}
@@ -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;
}
@@ -0,0 +1,5 @@
namespace StevesBot.Webhook.YouTube.Tasks;
internal sealed record UnsubscribeTask : SubscriptionTask
{
}
@@ -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<SubscriptionOptions> subOptions,
[FromServices] ILogger<Program> 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);
}
}