feat: begin working through how we will handle resubscribing and unsubscribing
This commit is contained in:
@@ -1,5 +1,3 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
builder.Services
|
builder.Services
|
||||||
@@ -28,25 +26,7 @@ app.UseHttpsRedirection();
|
|||||||
|
|
||||||
const string ytCallback = "yt-callback";
|
const string ytCallback = "yt-callback";
|
||||||
|
|
||||||
app.MapGet(
|
app.MapGet(ytCallback, VerifySubscriptionHandler.HandleAsync);
|
||||||
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);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// TODO: Implement logic to do the following:
|
// TODO: Implement logic to do the following:
|
||||||
// - extract video id from notification
|
// - extract video id from notification
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
global using System.ComponentModel.DataAnnotations;
|
global using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
global using Microsoft.AspNetCore.Mvc;
|
||||||
global using Microsoft.Extensions.Options;
|
global using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
global using StevesBot.Webhook;
|
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
|
internal interface IPubSubClient
|
||||||
{
|
{
|
||||||
Task SubscribeAsync(
|
Task<bool> SubscribeAsync(
|
||||||
string callbackUrl,
|
|
||||||
string topicUrl,
|
|
||||||
CancellationToken cancellationToken = default
|
|
||||||
);
|
|
||||||
|
|
||||||
Task UnsubscribeAsync(
|
|
||||||
string callbackUrl,
|
string callbackUrl,
|
||||||
string topicUrl,
|
string topicUrl,
|
||||||
CancellationToken cancellationToken = default
|
CancellationToken cancellationToken = default
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
|
|
||||||
namespace StevesBot.Webhook.YouTube;
|
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 const string SubscribeEndpoint = "subscribe";
|
||||||
private readonly HttpClient _httpClient = httpClient;
|
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 uri = new Uri(SubscribeEndpoint, UriKind.Relative);
|
||||||
var formFields = new Dictionary<string, string>()
|
var formFields = new Dictionary<string, string>()
|
||||||
@@ -21,12 +24,16 @@ internal sealed class PubSubClient(HttpClient httpClient) : IPubSubClient
|
|||||||
|
|
||||||
if (response.IsSuccessStatusCode is false)
|
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)
|
_logger.LogDebug(
|
||||||
{
|
"Failed to subscribe to topic {TopicUrl} with callback {CallbackUrl}: {ErrorMessage}",
|
||||||
throw new NotImplementedException();
|
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)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+10
-2
@@ -1,4 +1,4 @@
|
|||||||
namespace StevesBot.Webhook;
|
namespace StevesBot.Webhook.YouTube;
|
||||||
|
|
||||||
internal sealed class SubscriptionWorker(
|
internal sealed class SubscriptionWorker(
|
||||||
ILogger<SubscriptionWorker> logger,
|
ILogger<SubscriptionWorker> logger,
|
||||||
@@ -24,11 +24,19 @@ internal sealed class SubscriptionWorker(
|
|||||||
{
|
{
|
||||||
_logger.LogInformation("Subscribing to notifications");
|
_logger.LogInformation("Subscribing to notifications");
|
||||||
|
|
||||||
await _pubSubClient.SubscribeAsync(
|
var isSubscribed = await _pubSubClient.SubscribeAsync(
|
||||||
_options.CallbackUrl,
|
_options.CallbackUrl,
|
||||||
_options.TopicUrl,
|
_options.TopicUrl,
|
||||||
cancellationToken
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user