From 4e03c2523af9b873556bc3d3ccd475ff8670fc2e Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Sun, 28 May 2023 23:44:10 +0900 Subject: [PATCH] Prune webhook data from database --- .../Contracts/BaseDbContextFactory.cs | 2 +- BTCPayServer.Client/BTCPayServerClient.cs | 3 +- BTCPayServer.Client/Models/WebhookEvent.cs | 4 + BTCPayServer.Data/Data/WebhookDeliveryData.cs | 12 +- ...20230529135505_WebhookDeliveriesCleanup.cs | 83 +++++ .../ApplicationDbContextModelSnapshot.cs | 10 +- BTCPayServer.Tests/GreenfieldAPITests.cs | 10 +- .../GreenfieldStoreWebhooksController.cs | 9 + BTCPayServer/Data/WebhookDataExtensions.cs | 29 +- BTCPayServer/Extensions.cs | 10 + .../CleanupWebhookDeliveriesTask.cs | 61 ++++ BTCPayServer/HostedServices/IPeriodicTask.cs | 10 + .../PeriodicTaskLauncherHostedService.cs | 92 ++++++ BTCPayServer/HostedServices/ScheduledTask.cs | 16 + BTCPayServer/HostedServices/WebhookSender.cs | 2 + BTCPayServer/Hosting/BTCPayServerServices.cs | 3 + .../Hosting/ToPostgresMigrationStartupTask.cs | 3 + .../StoreViewModels/EditWebhookViewModel.cs | 5 +- BTCPayServer/Views/UIInvoice/Invoice.cshtml | 304 +++++++++--------- .../Views/UIStores/ModifyWebhook.cshtml | 2 + .../swagger/v1/swagger.template.webhooks.json | 6 + 21 files changed, 515 insertions(+), 161 deletions(-) create mode 100644 BTCPayServer.Data/Migrations/20230529135505_WebhookDeliveriesCleanup.cs create mode 100644 BTCPayServer/HostedServices/CleanupWebhookDeliveriesTask.cs create mode 100644 BTCPayServer/HostedServices/IPeriodicTask.cs create mode 100644 BTCPayServer/HostedServices/PeriodicTaskLauncherHostedService.cs create mode 100644 BTCPayServer/HostedServices/ScheduledTask.cs diff --git a/BTCPayServer.Abstractions/Contracts/BaseDbContextFactory.cs b/BTCPayServer.Abstractions/Contracts/BaseDbContextFactory.cs index 9acd84a77..96006b735 100644 --- a/BTCPayServer.Abstractions/Contracts/BaseDbContextFactory.cs +++ b/BTCPayServer.Abstractions/Contracts/BaseDbContextFactory.cs @@ -1,4 +1,5 @@ using System; +using System.Data.Common; using BTCPayServer.Abstractions.Models; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata; @@ -21,7 +22,6 @@ namespace BTCPayServer.Abstractions.Contracts } public abstract T CreateContext(); - class CustomNpgsqlMigrationsSqlGenerator : NpgsqlMigrationsSqlGenerator { #pragma warning disable EF1001 // Internal EF Core API usage. diff --git a/BTCPayServer.Client/BTCPayServerClient.cs b/BTCPayServer.Client/BTCPayServerClient.cs index 20457a4d0..4ca23a191 100644 --- a/BTCPayServer.Client/BTCPayServerClient.cs +++ b/BTCPayServer.Client/BTCPayServerClient.cs @@ -51,7 +51,8 @@ namespace BTCPayServer.Client { if (message.StatusCode == System.Net.HttpStatusCode.UnprocessableEntity) { - var err = JsonConvert.DeserializeObject(await message.Content.ReadAsStringAsync()); + var aa = await message.Content.ReadAsStringAsync(); + var err = JsonConvert.DeserializeObject(aa); throw new GreenfieldValidationException(err); } if (message.StatusCode == System.Net.HttpStatusCode.Forbidden) diff --git a/BTCPayServer.Client/Models/WebhookEvent.cs b/BTCPayServer.Client/Models/WebhookEvent.cs index afedd6459..1b43dbc34 100644 --- a/BTCPayServer.Client/Models/WebhookEvent.cs +++ b/BTCPayServer.Client/Models/WebhookEvent.cs @@ -51,6 +51,10 @@ namespace BTCPayServer.Client.Models public DateTimeOffset Timestamp { get; set; } [JsonExtensionData] public IDictionary AdditionalData { get; set; } + public bool IsPruned() + { + return DeliveryId is null; + } public T ReadAs() { var str = JsonConvert.SerializeObject(this, DefaultSerializerSettings); diff --git a/BTCPayServer.Data/Data/WebhookDeliveryData.cs b/BTCPayServer.Data/Data/WebhookDeliveryData.cs index d6e7591ef..53ecbdb5b 100644 --- a/BTCPayServer.Data/Data/WebhookDeliveryData.cs +++ b/BTCPayServer.Data/Data/WebhookDeliveryData.cs @@ -5,7 +5,7 @@ using Microsoft.EntityFrameworkCore.Infrastructure; namespace BTCPayServer.Data { - public class WebhookDeliveryData : IHasBlobUntyped + public class WebhookDeliveryData { [Key] [MaxLength(25)] @@ -17,10 +17,8 @@ namespace BTCPayServer.Data [Required] public DateTimeOffset Timestamp { get; set; } - [Obsolete("Use Blob2 instead")] - public byte[] Blob { get; set; } - public string Blob2 { get; set; } - + public string Blob { get; set; } + public bool Pruned { get; set; } internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade) { @@ -28,11 +26,11 @@ namespace BTCPayServer.Data .HasOne(o => o.Webhook) .WithMany(a => a.Deliveries).OnDelete(DeleteBehavior.Cascade); builder.Entity().HasIndex(o => o.WebhookId); - + builder.Entity().HasIndex(o => o.Timestamp); if (databaseFacade.IsNpgsql()) { builder.Entity() - .Property(o => o.Blob2) + .Property(o => o.Blob) .HasColumnType("JSONB"); } } diff --git a/BTCPayServer.Data/Migrations/20230529135505_WebhookDeliveriesCleanup.cs b/BTCPayServer.Data/Migrations/20230529135505_WebhookDeliveriesCleanup.cs new file mode 100644 index 000000000..021208a7d --- /dev/null +++ b/BTCPayServer.Data/Migrations/20230529135505_WebhookDeliveriesCleanup.cs @@ -0,0 +1,83 @@ +using System; +using BTCPayServer.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NBitcoin; +using Newtonsoft.Json; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +#nullable disable + +namespace BTCPayServer.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20230529135505_WebhookDeliveriesCleanup")] + public partial class WebhookDeliveriesCleanup : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + if (migrationBuilder.IsNpgsql()) + { + migrationBuilder.Sql("DROP TABLE IF EXISTS \"InvoiceWebhookDeliveries\", \"WebhookDeliveries\";"); + + migrationBuilder.CreateTable( + name: "WebhookDeliveries", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + WebhookId = table.Column(type: "TEXT", nullable: false), + Timestamp = table.Column(type: "timestamp with time zone", nullable: false), + Pruned = table.Column(type: "BOOLEAN", nullable: false), + Blob = table.Column(type: "JSONB", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_WebhookDeliveries", x => x.Id); + table.ForeignKey( + name: "FK_WebhookDeliveries_Webhooks_WebhookId", + column: x => x.WebhookId, + principalTable: "Webhooks", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + + migrationBuilder.CreateIndex( + name: "IX_WebhookDeliveries_WebhookId", + table: "WebhookDeliveries", + column: "WebhookId"); + migrationBuilder.Sql("CREATE INDEX \"IX_WebhookDeliveries_Timestamp\" ON \"WebhookDeliveries\"(\"Timestamp\") WHERE \"Pruned\" IS FALSE"); + + migrationBuilder.CreateTable( + name: "InvoiceWebhookDeliveries", + columns: table => new + { + InvoiceId = table.Column(type: "TEXT", nullable: false), + DeliveryId = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_InvoiceWebhookDeliveries", x => new { x.InvoiceId, x.DeliveryId }); + table.ForeignKey( + name: "FK_InvoiceWebhookDeliveries_WebhookDeliveries_DeliveryId", + column: x => x.DeliveryId, + principalTable: "WebhookDeliveries", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_InvoiceWebhookDeliveries_Invoices_InvoiceId", + column: x => x.InvoiceId, + principalTable: "Invoices", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + } + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/BTCPayServer.Data/Migrations/ApplicationDbContextModelSnapshot.cs b/BTCPayServer.Data/Migrations/ApplicationDbContextModelSnapshot.cs index 3a0ead2a8..2da325a36 100644 --- a/BTCPayServer.Data/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/BTCPayServer.Data/Migrations/ApplicationDbContextModelSnapshot.cs @@ -1019,12 +1019,12 @@ namespace BTCPayServer.Migrations .HasMaxLength(25) .HasColumnType("TEXT"); - b.Property("Blob") - .HasColumnType("BLOB"); - - b.Property("Blob2") + b.Property("Blob") .HasColumnType("TEXT"); + b.Property("Pruned") + .HasColumnType("INTEGER"); + b.Property("Timestamp") .HasColumnType("TEXT"); @@ -1035,6 +1035,8 @@ namespace BTCPayServer.Migrations b.HasKey("Id"); + b.HasIndex("Timestamp"); + b.HasIndex("WebhookId"); b.ToTable("WebhookDeliveries"); diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index d872ef089..53553ac1f 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -1432,7 +1432,7 @@ namespace BTCPayServer.Tests Assert.False(hook.AutomaticRedelivery); Assert.Equal(fakeServer.ServerUri.AbsoluteUri, hook.Url); } - using var tester = CreateServerTester(); + using var tester = CreateServerTester(newDb: true); using var fakeServer = new FakeServer(); await fakeServer.Start(); await tester.StartAsync(); @@ -1509,6 +1509,14 @@ namespace BTCPayServer.Tests clientProfile = await user.CreateClient(Policies.CanModifyStoreSettings, Policies.CanCreateInvoice); await clientProfile.GetWebhookDeliveryRequest(user.StoreId, hook.Id, newDeliveryId); + + TestLogs.LogInformation("Can prune deliveries"); + var cleanup = tester.PayTester.GetService(); + cleanup.BatchSize = 1; + cleanup.PruneAfter = TimeSpan.Zero; + await cleanup.Do(default); + await AssertHttpError(409, () => clientProfile.RedeliverWebhook(user.StoreId, hook.Id, delivery.Id)); + TestLogs.LogInformation("Testing corner cases"); Assert.Null(await clientProfile.GetWebhookDeliveryRequest(user.StoreId, "lol", newDeliveryId)); Assert.Null(await clientProfile.GetWebhookDeliveryRequest(user.StoreId, hook.Id, "lol")); diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldStoreWebhooksController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldStoreWebhooksController.cs index 5c620fd96..d53bf580a 100644 --- a/BTCPayServer/Controllers/GreenField/GreenfieldStoreWebhooksController.cs +++ b/BTCPayServer/Controllers/GreenField/GreenfieldStoreWebhooksController.cs @@ -153,15 +153,24 @@ namespace BTCPayServer.Controllers.Greenfield var delivery = await StoreRepository.GetWebhookDelivery(CurrentStoreId, webhookId, deliveryId); if (delivery is null) return WebhookDeliveryNotFound(); + if (delivery.GetBlob().IsPruned()) + return WebhookDeliveryPruned(); return this.Ok(new JValue(await WebhookSender.Redeliver(deliveryId))); } + private IActionResult WebhookDeliveryPruned() + { + return this.CreateAPIError(409, "webhookdelivery-pruned", "This webhook delivery has been pruned, so it can't be redelivered"); + } + [HttpGet("~/api/v1/stores/{storeId}/webhooks/{webhookId}/deliveries/{deliveryId}/request")] public async Task GetDeliveryRequest(string storeId, string webhookId, string deliveryId) { var delivery = await StoreRepository.GetWebhookDelivery(CurrentStoreId, webhookId, deliveryId); if (delivery is null) return WebhookDeliveryNotFound(); + if (delivery.GetBlob().IsPruned()) + return WebhookDeliveryPruned(); return File(delivery.GetBlob().Request, "application/json"); } diff --git a/BTCPayServer/Data/WebhookDataExtensions.cs b/BTCPayServer/Data/WebhookDataExtensions.cs index 932be8208..406e890ab 100644 --- a/BTCPayServer/Data/WebhookDataExtensions.cs +++ b/BTCPayServer/Data/WebhookDataExtensions.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -29,12 +30,30 @@ namespace BTCPayServer.Data [JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))] public WebhookDeliveryStatus Status { get; set; } public int? HttpCode { get; set; } + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] public string ErrorMessage { get; set; } + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] public byte[] Request { get; set; } + public void Prune() + { + var request = JObject.Parse(UTF8Encoding.UTF8.GetString(Request)); + foreach (var prop in request.Properties().ToList()) + { + if (prop.Name == "type") + continue; + prop.Remove(); + } + Request = UTF8Encoding.UTF8.GetBytes(request.ToString(Formatting.None)); + } public T ReadRequestAs() { return JsonConvert.DeserializeObject(UTF8Encoding.UTF8.GetString(Request), HostedServices.WebhookSender.DefaultSerializerSettings); } + + public bool IsPruned() + { + return ReadRequestAs().IsPruned(); + } } public class WebhookBlob { @@ -56,11 +75,17 @@ namespace BTCPayServer.Data } public static WebhookDeliveryBlob GetBlob(this WebhookDeliveryData webhook) { - return webhook.HasTypedBlob().GetBlob(HostedServices.WebhookSender.DefaultSerializerSettings); + if (webhook.Blob is null) + return null; + else + return JsonConvert.DeserializeObject(webhook.Blob, HostedServices.WebhookSender.DefaultSerializerSettings); } public static void SetBlob(this WebhookDeliveryData webhook, WebhookDeliveryBlob blob) { - webhook.HasTypedBlob().SetBlob(blob, HostedServices.WebhookSender.DefaultSerializerSettings); + if (blob is null) + webhook.Blob = null; + else + webhook.Blob = JsonConvert.SerializeObject(blob, HostedServices.WebhookSender.DefaultSerializerSettings); } } } diff --git a/BTCPayServer/Extensions.cs b/BTCPayServer/Extensions.cs index 724bdae68..78d4e9225 100644 --- a/BTCPayServer/Extensions.cs +++ b/BTCPayServer/Extensions.cs @@ -13,6 +13,7 @@ using System.Threading.Tasks; using BTCPayServer.BIP78.Sender; using BTCPayServer.Configuration; using BTCPayServer.Data; +using BTCPayServer.HostedServices; using BTCPayServer.Lightning; using BTCPayServer.Logging; using BTCPayServer.Models; @@ -25,6 +26,7 @@ using BTCPayServer.Services.Wallets; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using NBitcoin; using NBitcoin.Payment; @@ -115,6 +117,14 @@ namespace BTCPayServer return value; } + public static IServiceCollection AddScheduledTask(this IServiceCollection services, TimeSpan every) + where T : class, IPeriodicTask + { + services.AddSingleton(); + services.AddTransient(o => new ScheduledTask(typeof(T), every)); + return services; + } + public static PaymentMethodId GetpaymentMethodId(this InvoiceCryptoInfo info) { return new PaymentMethodId(info.CryptoCode, PaymentTypes.Parse(info.PaymentType)); diff --git a/BTCPayServer/HostedServices/CleanupWebhookDeliveriesTask.cs b/BTCPayServer/HostedServices/CleanupWebhookDeliveriesTask.cs new file mode 100644 index 000000000..57d8e41a6 --- /dev/null +++ b/BTCPayServer/HostedServices/CleanupWebhookDeliveriesTask.cs @@ -0,0 +1,61 @@ +using System; +using Dapper; +using System.Data.Common; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using BTCPayServer.Data; +using Microsoft.EntityFrameworkCore; +using System.Collections.Generic; + +namespace BTCPayServer.HostedServices +{ + public class CleanupWebhookDeliveriesTask : IPeriodicTask + { + public CleanupWebhookDeliveriesTask(ApplicationDbContextFactory dbContextFactory) + { + DbContextFactory = dbContextFactory; + } + + public ApplicationDbContextFactory DbContextFactory { get; } + public int BatchSize { get; set; } = 500; + public TimeSpan PruneAfter { get; set; } = TimeSpan.FromDays(60); + + public async Task Do(CancellationToken cancellationToken) + { + await using var ctx = DbContextFactory.CreateContext(); + if (!ctx.Database.IsNpgsql()) + return; + var conn = ctx.Database.GetDbConnection(); + bool pruned = false; + int offset = 0; +retry: + var rows = await conn.QueryAsync(@" +SELECT ""Id"", ""Blob"" +FROM ""WebhookDeliveries"" +WHERE ((now() - ""Timestamp"") > @PruneAfter) AND ""Pruned"" IS FALSE +ORDER BY ""Timestamp"" +LIMIT @BatchSize OFFSET @offset +", new { PruneAfter, BatchSize, offset }); + + foreach (var d in rows) + { + var blob = d.GetBlob(); + blob.Prune(); + d.SetBlob(blob); + d.Pruned = true; + pruned = true; + } + if (pruned) + { + pruned = false; + await conn.ExecuteAsync("UPDATE \"WebhookDeliveries\" SET \"Blob\"=@Blob::JSONB, \"Pruned\"=@Pruned WHERE \"Id\"=@Id", rows); + if (rows.Count() == BatchSize) + { + offset += BatchSize; + goto retry; + } + } + } + } +} diff --git a/BTCPayServer/HostedServices/IPeriodicTask.cs b/BTCPayServer/HostedServices/IPeriodicTask.cs new file mode 100644 index 000000000..383e81b87 --- /dev/null +++ b/BTCPayServer/HostedServices/IPeriodicTask.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using System.Threading; + +namespace BTCPayServer.HostedServices +{ + public interface IPeriodicTask + { + Task Do(CancellationToken cancellationToken); + } +} diff --git a/BTCPayServer/HostedServices/PeriodicTaskLauncherHostedService.cs b/BTCPayServer/HostedServices/PeriodicTaskLauncherHostedService.cs new file mode 100644 index 000000000..834026a4a --- /dev/null +++ b/BTCPayServer/HostedServices/PeriodicTaskLauncherHostedService.cs @@ -0,0 +1,92 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System; +using System.Linq; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; + +namespace BTCPayServer.HostedServices +{ + public class PeriodicTaskLauncherHostedService : IHostedService + { + public PeriodicTaskLauncherHostedService(IServiceProvider serviceProvider, ILoggerFactory loggerFactory) + { + ServiceProvider = serviceProvider; + Logger = loggerFactory.CreateLogger("BTCPayServer.PeriodicTasks"); + } + + public IServiceProvider ServiceProvider { get; } + public ILogger Logger { get; } + + Channel jobs = Channel.CreateBounded(100); + CancellationTokenSource cts; + public Task StartAsync(CancellationToken cancellationToken) + { + cts = new CancellationTokenSource(); + foreach (var task in ServiceProvider.GetServices()) + jobs.Writer.TryWrite(task); + + loop = Task.WhenAll(Enumerable.Range(0, 3).Select(_ => Loop(cts.Token)).ToArray()); + return Task.CompletedTask; + } + Task loop; + private async Task Loop(CancellationToken token) + { + try + { + await foreach (var job in jobs.Reader.ReadAllAsync(token)) + { + if (job.NextScheduled <= DateTimeOffset.UtcNow) + { + var t = (IPeriodicTask)ServiceProvider.GetService(job.PeriodicTaskType); + try + { + await t.Do(token); + } + catch when (token.IsCancellationRequested) + { + throw; + } + catch (Exception ex) + { + Logger.LogError(ex, $"Unhandled error in periodic task {job.PeriodicTaskType.Name}"); + } + finally + { + job.NextScheduled = DateTimeOffset.UtcNow + job.Every; + } + } + _ = Wait(job, token); + } + } + catch when (token.IsCancellationRequested) + { + } + } + + private async Task Wait(ScheduledTask job, CancellationToken token) + { + var timeToWait = job.NextScheduled - DateTimeOffset.UtcNow; + try + { + await Task.Delay(timeToWait, token); + } + catch { } + while (await jobs.Writer.WaitToWriteAsync()) + { + if (jobs.Writer.TryWrite(job)) + break; + } + } + + public async Task StopAsync(CancellationToken cancellationToken) + { + cts?.Cancel(); + jobs.Writer.TryComplete(); + if (loop is not null) + await loop; + } + } +} diff --git a/BTCPayServer/HostedServices/ScheduledTask.cs b/BTCPayServer/HostedServices/ScheduledTask.cs new file mode 100644 index 000000000..0a3de9dca --- /dev/null +++ b/BTCPayServer/HostedServices/ScheduledTask.cs @@ -0,0 +1,16 @@ +using System; + +namespace BTCPayServer.HostedServices +{ + public class ScheduledTask + { + public ScheduledTask(Type periodicTypeTask, TimeSpan every) + { + PeriodicTaskType = periodicTypeTask; + Every = every; + } + public Type PeriodicTaskType { get; set; } + public TimeSpan Every { get; set; } = TimeSpan.FromMinutes(5.0); + public DateTimeOffset NextScheduled { get; set; } = DateTimeOffset.UtcNow; + } +} diff --git a/BTCPayServer/HostedServices/WebhookSender.cs b/BTCPayServer/HostedServices/WebhookSender.cs index cf02605f1..28657bfdd 100644 --- a/BTCPayServer/HostedServices/WebhookSender.cs +++ b/BTCPayServer/HostedServices/WebhookSender.cs @@ -105,6 +105,8 @@ namespace BTCPayServer.HostedServices var newDeliveryBlob = new WebhookDeliveryBlob(); newDeliveryBlob.Request = oldDeliveryBlob.Request; var webhookEvent = newDeliveryBlob.ReadRequestAs(); + if (webhookEvent.IsPruned()) + return null; webhookEvent.DeliveryId = newDelivery.Id; webhookEvent.WebhookId = webhookDelivery.Webhook.Id; // if we redelivered a redelivery, we still want the initial delivery here diff --git a/BTCPayServer/Hosting/BTCPayServerServices.cs b/BTCPayServer/Hosting/BTCPayServerServices.cs index 65a11c966..987ebcfde 100644 --- a/BTCPayServer/Hosting/BTCPayServerServices.cs +++ b/BTCPayServer/Hosting/BTCPayServerServices.cs @@ -350,6 +350,9 @@ namespace BTCPayServer.Hosting services.AddSingleton(); services.AddSingleton(o => o.GetRequiredService()); services.AddSingleton(); + services.AddSingleton(); + services.AddScheduledTask(TimeSpan.FromHours(6.0)); + services.AddHttpClient(WebhookSender.OnionNamedClient) .ConfigurePrimaryHttpMessageHandler(); services.AddHttpClient(WebhookSender.LoopbackNamedClient) diff --git a/BTCPayServer/Hosting/ToPostgresMigrationStartupTask.cs b/BTCPayServer/Hosting/ToPostgresMigrationStartupTask.cs index 2df3d1581..fdf272981 100644 --- a/BTCPayServer/Hosting/ToPostgresMigrationStartupTask.cs +++ b/BTCPayServer/Hosting/ToPostgresMigrationStartupTask.cs @@ -213,6 +213,9 @@ namespace BTCPayServer.Hosting { var typeMapping = t.EntityTypeMappings.Single(); var query = (IQueryable)otherContext.GetType().GetMethod("Set", new Type[0])!.MakeGenericMethod(typeMapping.EntityType.ClrType).Invoke(otherContext, null)!; + if (t.Name == "WebhookDeliveries" || + t.Name == "InvoiceWebhookDeliveries") + continue; Logger.LogInformation($"Migrating table: " + t.Name); List datetimeProperties = new List(); foreach (var col in t.Columns) diff --git a/BTCPayServer/Models/StoreViewModels/EditWebhookViewModel.cs b/BTCPayServer/Models/StoreViewModels/EditWebhookViewModel.cs index df5ff47ff..69a6ab17e 100644 --- a/BTCPayServer/Models/StoreViewModels/EditWebhookViewModel.cs +++ b/BTCPayServer/Models/StoreViewModels/EditWebhookViewModel.cs @@ -22,13 +22,16 @@ namespace BTCPayServer.Models.StoreViewModels Success = blob.Status == WebhookDeliveryStatus.HttpSuccess; ErrorMessage = blob.ErrorMessage ?? "Success"; Time = s.Timestamp; - Type = blob.ReadRequestAs().Type; + var evt = blob.ReadRequestAs(); + Type = evt.Type; + Pruned = evt.IsPruned(); WebhookId = s.Id; PayloadUrl = s.Webhook?.GetBlob().Url; } public string Id { get; set; } public DateTimeOffset Time { get; set; } public WebhookEventType Type { get; private set; } + public bool Pruned { get; set; } public string WebhookId { get; set; } public bool Success { get; set; } public string ErrorMessage { get; set; } diff --git a/BTCPayServer/Views/UIInvoice/Invoice.cshtml b/BTCPayServer/Views/UIInvoice/Invoice.cshtml index 1f061fcf5..2b688b06e 100644 --- a/BTCPayServer/Views/UIInvoice/Invoice.cshtml +++ b/BTCPayServer/Views/UIInvoice/Invoice.cshtml @@ -9,16 +9,26 @@ @section PageHeadContent { } @section PageFootContent {