diff --git a/Deployment/mssql-migration.sql b/Deployment/mssql-migration.sql index 1aa9aec1d..2f00e2792 100644 --- a/Deployment/mssql-migration.sql +++ b/Deployment/mssql-migration.sql @@ -1,33 +1,10 @@ --- v14.3.x - v14.4.0 -CREATE TABLE [dbo].[LoginHistory]( - [Id] [int] IDENTITY(1,1) NOT NULL, - [LoginTimeUtc] [datetime] NOT NULL, - [LoginIp] [nvarchar](64) NULL, - [LoginUserAgent] [nvarchar](128) NULL, - [DeviceFingerprint] [nvarchar](128) NULL, - CONSTRAINT [PK_LoginHistory] PRIMARY KEY CLUSTERED -( - [Id] ASC -)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY] -) ON [PRIMARY] -GO +-- v14.4.1 - v14.5.x -DROP TABLE [LocalAccount] +EXEC sp_rename 'Pingback', 'Mention' GO -EXEC sys.sp_rename - @objname = N'Category.RouteName', - @newname = 'Slug', - @objtype = 'COLUMN' +ALTER TABLE Mention ADD Worker NVARCHAR(16) GO -IF EXISTS ( - SELECT 1 - FROM sys.columns c - JOIN sys.objects o ON c.object_id = o.object_id - WHERE o.name = 'Post' AND c.name = 'InlineCss' -) -BEGIN - ALTER TABLE Post DROP COLUMN InlineCss; -END; -GO \ No newline at end of file +UPDATE Mention SET Worker = N'Pingback' +GO diff --git a/src/Moonglade.Configuration/AdvancedSettings.cs b/src/Moonglade.Configuration/AdvancedSettings.cs index 6338b24d2..824d97ff1 100644 --- a/src/Moonglade.Configuration/AdvancedSettings.cs +++ b/src/Moonglade.Configuration/AdvancedSettings.cs @@ -18,6 +18,9 @@ public class AdvancedSettings : IBlogSettings [Display(Name = "Enable Pingback")] public bool EnablePingback { get; set; } = true; + [Display(Name = "Enable Webmention")] + public bool EnableWebmention { get; set; } = true; + [Display(Name = "Enable OpenSearch")] public bool EnableOpenSearch { get; set; } = true; diff --git a/src/Moonglade.Data.MySql/Configurations/MentionConfiguration.cs b/src/Moonglade.Data.MySql/Configurations/MentionConfiguration.cs new file mode 100644 index 000000000..9c8d05852 --- /dev/null +++ b/src/Moonglade.Data.MySql/Configurations/MentionConfiguration.cs @@ -0,0 +1,15 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Moonglade.Data.Entities; + +namespace Moonglade.Data.MySql.Configurations; + + +internal class MentionConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.Property(e => e.Id).ValueGeneratedNever(); + builder.Property(e => e.PingTimeUtc).HasColumnType("datetime"); + } +} \ No newline at end of file diff --git a/src/Moonglade.Data.MySql/Configurations/PingbackConfiguration.cs b/src/Moonglade.Data.MySql/Configurations/PingbackConfiguration.cs deleted file mode 100644 index 6b988f6da..000000000 --- a/src/Moonglade.Data.MySql/Configurations/PingbackConfiguration.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; -using Moonglade.Data.Entities; - -namespace Moonglade.Data.MySql.Configurations; - - -internal class PingbackConfiguration : IEntityTypeConfiguration -{ - public void Configure(EntityTypeBuilder builder) - { - builder.Property(e => e.Id).ValueGeneratedNever(); - builder.Property(e => e.PingTimeUtc).HasColumnType("datetime"); - builder.Property(e => e.TargetPostTitle).HasMaxLength(128); - builder.Property(e => e.SourceIp).HasMaxLength(64); - builder.Property(e => e.SourceTitle).HasMaxLength(256); - builder.Property(e => e.SourceUrl).HasMaxLength(256); - builder.Property(e => e.Domain).HasMaxLength(256); - } -} \ No newline at end of file diff --git a/src/Moonglade.Data.MySql/MySqlBlogDbContext.cs b/src/Moonglade.Data.MySql/MySqlBlogDbContext.cs index ee6c135f3..7dd1921d8 100644 --- a/src/Moonglade.Data.MySql/MySqlBlogDbContext.cs +++ b/src/Moonglade.Data.MySql/MySqlBlogDbContext.cs @@ -21,7 +21,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.ApplyConfiguration(new CommentReplyConfiguration()); modelBuilder.ApplyConfiguration(new PostConfiguration()); modelBuilder.ApplyConfiguration(new PostCategoryConfiguration()); - modelBuilder.ApplyConfiguration(new PingbackConfiguration()); + modelBuilder.ApplyConfiguration(new MentionConfiguration()); modelBuilder.ApplyConfiguration(new BlogThemeConfiguration()); modelBuilder.ApplyConfiguration(new BlogAssetConfiguration()); modelBuilder.ApplyConfiguration(new StyleSheetConfiguration()); diff --git a/src/Moonglade.Data.PostgreSql/Configurations/MentionConfiguration.cs b/src/Moonglade.Data.PostgreSql/Configurations/MentionConfiguration.cs new file mode 100644 index 000000000..85daeffa8 --- /dev/null +++ b/src/Moonglade.Data.PostgreSql/Configurations/MentionConfiguration.cs @@ -0,0 +1,15 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Moonglade.Data.Entities; + +namespace Moonglade.Data.PostgreSql.Configurations; + + +internal class MentionConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.Property(e => e.Id).ValueGeneratedNever(); + builder.Property(e => e.PingTimeUtc).HasColumnType("timestamp"); + } +} \ No newline at end of file diff --git a/src/Moonglade.Data.PostgreSql/Configurations/PingbackConfiguration.cs b/src/Moonglade.Data.PostgreSql/Configurations/PingbackConfiguration.cs deleted file mode 100644 index 18ff9a596..000000000 --- a/src/Moonglade.Data.PostgreSql/Configurations/PingbackConfiguration.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; -using Moonglade.Data.Entities; - -namespace Moonglade.Data.PostgreSql.Configurations; - - -internal class PingbackConfiguration : IEntityTypeConfiguration -{ - public void Configure(EntityTypeBuilder builder) - { - builder.Property(e => e.Id).ValueGeneratedNever(); - builder.Property(e => e.PingTimeUtc).HasColumnType("timestamp"); - builder.Property(e => e.TargetPostTitle).HasMaxLength(128); - builder.Property(e => e.SourceIp).HasMaxLength(64); - builder.Property(e => e.SourceTitle).HasMaxLength(256); - builder.Property(e => e.SourceUrl).HasMaxLength(256); - builder.Property(e => e.Domain).HasMaxLength(256); - } -} \ No newline at end of file diff --git a/src/Moonglade.Data.PostgreSql/PostgreSqlBlogDbContext.cs b/src/Moonglade.Data.PostgreSql/PostgreSqlBlogDbContext.cs index 44815882f..7d57e5f30 100644 --- a/src/Moonglade.Data.PostgreSql/PostgreSqlBlogDbContext.cs +++ b/src/Moonglade.Data.PostgreSql/PostgreSqlBlogDbContext.cs @@ -20,7 +20,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.ApplyConfiguration(new PostConfiguration()); modelBuilder.ApplyConfiguration(new PostCategoryConfiguration()); modelBuilder.ApplyConfiguration(new LoginHistoryConfiguration()); - modelBuilder.ApplyConfiguration(new PingbackConfiguration()); + modelBuilder.ApplyConfiguration(new MentionConfiguration()); modelBuilder.ApplyConfiguration(new BlogThemeConfiguration()); modelBuilder.ApplyConfiguration(new BlogAssetConfiguration()); modelBuilder.ApplyConfiguration(new StyleSheetConfiguration()); diff --git a/src/Moonglade.Data.SqlServer/Configurations/MentionConfiguration.cs b/src/Moonglade.Data.SqlServer/Configurations/MentionConfiguration.cs new file mode 100644 index 000000000..5673df2aa --- /dev/null +++ b/src/Moonglade.Data.SqlServer/Configurations/MentionConfiguration.cs @@ -0,0 +1,15 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Moonglade.Data.Entities; + +namespace Moonglade.Data.SqlServer.Configurations; + + +internal class MentionConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.Property(e => e.Id).ValueGeneratedNever(); + builder.Property(e => e.PingTimeUtc).HasColumnType("datetime"); + } +} \ No newline at end of file diff --git a/src/Moonglade.Data.SqlServer/Configurations/PingbackConfiguration.cs b/src/Moonglade.Data.SqlServer/Configurations/PingbackConfiguration.cs deleted file mode 100644 index 088eb665c..000000000 --- a/src/Moonglade.Data.SqlServer/Configurations/PingbackConfiguration.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; -using Moonglade.Data.Entities; - -namespace Moonglade.Data.SqlServer.Configurations; - - -internal class PingbackConfiguration : IEntityTypeConfiguration -{ - public void Configure(EntityTypeBuilder builder) - { - builder.Property(e => e.Id).ValueGeneratedNever(); - builder.Property(e => e.PingTimeUtc).HasColumnType("datetime"); - builder.Property(e => e.TargetPostTitle).HasMaxLength(128); - builder.Property(e => e.SourceIp).HasMaxLength(64); - builder.Property(e => e.SourceTitle).HasMaxLength(256); - builder.Property(e => e.SourceUrl).HasMaxLength(256); - builder.Property(e => e.Domain).HasMaxLength(256); - } -} \ No newline at end of file diff --git a/src/Moonglade.Data.SqlServer/SqlServerBlogDbContext.cs b/src/Moonglade.Data.SqlServer/SqlServerBlogDbContext.cs index 6c39cead6..41de9b4d3 100644 --- a/src/Moonglade.Data.SqlServer/SqlServerBlogDbContext.cs +++ b/src/Moonglade.Data.SqlServer/SqlServerBlogDbContext.cs @@ -22,7 +22,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.ApplyConfiguration(new PostConfiguration()); modelBuilder.ApplyConfiguration(new PostCategoryConfiguration()); modelBuilder.ApplyConfiguration(new LoginHistoryConfiguration()); - modelBuilder.ApplyConfiguration(new PingbackConfiguration()); + modelBuilder.ApplyConfiguration(new MentionConfiguration()); modelBuilder.ApplyConfiguration(new BlogThemeConfiguration()); modelBuilder.ApplyConfiguration(new BlogAssetConfiguration()); modelBuilder.ApplyConfiguration(new StyleSheetConfiguration()); diff --git a/src/Moonglade.Data/BlogDbContext.cs b/src/Moonglade.Data/BlogDbContext.cs index 28a6bbac7..1067293b3 100644 --- a/src/Moonglade.Data/BlogDbContext.cs +++ b/src/Moonglade.Data/BlogDbContext.cs @@ -23,7 +23,7 @@ public BlogDbContext(DbContextOptions options) public virtual DbSet FriendLink { get; set; } public virtual DbSet CustomPage { get; set; } public virtual DbSet LoginHistory { get; set; } - public virtual DbSet Pingback { get; set; } + public virtual DbSet Mention { get; set; } public virtual DbSet BlogTheme { get; set; } public virtual DbSet StyleSheet { get; set; } public virtual DbSet BlogAsset { get; set; } @@ -62,7 +62,7 @@ public static async Task ClearAllData(this BlogDbContext context) context.Tag.RemoveRange(); context.Comment.RemoveRange(); context.FriendLink.RemoveRange(); - context.Pingback.RemoveRange(); + context.Mention.RemoveRange(); context.Post.RemoveRange(); context.BlogConfiguration.RemoveRange(); context.BlogAsset.RemoveRange(); diff --git a/src/Moonglade.Data/Entities/PingbackEntity.cs b/src/Moonglade.Data/Entities/MentionEntity.cs similarity index 54% rename from src/Moonglade.Data/Entities/PingbackEntity.cs rename to src/Moonglade.Data/Entities/MentionEntity.cs index 283513826..c08129819 100644 --- a/src/Moonglade.Data/Entities/PingbackEntity.cs +++ b/src/Moonglade.Data/Entities/MentionEntity.cs @@ -1,13 +1,29 @@ -namespace Moonglade.Data.Entities; +using System.ComponentModel.DataAnnotations; -public class PingbackEntity +namespace Moonglade.Data.Entities; + +public class MentionEntity { public Guid Id { get; set; } + + [MaxLength(256)] public string Domain { get; set; } + + [MaxLength(256)] public string SourceUrl { get; set; } + + [MaxLength(256)] public string SourceTitle { get; set; } + + [MaxLength(64)] public string SourceIp { get; set; } + public Guid TargetPostId { get; set; } public DateTime PingTimeUtc { get; set; } + + [MaxLength(100)] public string TargetPostTitle { get; set; } + + [MaxLength(16)] + public string Worker { get; set; } } \ No newline at end of file diff --git a/src/Moonglade.Data/Specifications/PingbackReadOnlySpec.cs b/src/Moonglade.Data/Specifications/MentionReadOnlySpec.cs similarity index 53% rename from src/Moonglade.Data/Specifications/PingbackReadOnlySpec.cs rename to src/Moonglade.Data/Specifications/MentionReadOnlySpec.cs index 5bb80f712..b13b0aeba 100644 --- a/src/Moonglade.Data/Specifications/PingbackReadOnlySpec.cs +++ b/src/Moonglade.Data/Specifications/MentionReadOnlySpec.cs @@ -2,9 +2,9 @@ namespace Moonglade.Data.Specifications; -public sealed class PingbackReadOnlySpec : Specification +public sealed class MentionReadOnlySpec : Specification { - public PingbackReadOnlySpec() + public MentionReadOnlySpec() { Query.AsNoTracking(); } diff --git a/src/Moonglade.Data/Specifications/PingbackSpec.cs b/src/Moonglade.Data/Specifications/MentionSpec.cs similarity index 63% rename from src/Moonglade.Data/Specifications/PingbackSpec.cs rename to src/Moonglade.Data/Specifications/MentionSpec.cs index 42d2a6e49..11063d429 100644 --- a/src/Moonglade.Data/Specifications/PingbackSpec.cs +++ b/src/Moonglade.Data/Specifications/MentionSpec.cs @@ -2,9 +2,9 @@ namespace Moonglade.Data.Specifications; -public sealed class PingbackSpec : Specification +public sealed class MentionSpec : Specification { - public PingbackSpec(Guid postId, string sourceUrl, string sourceIp) + public MentionSpec(Guid postId, string sourceUrl, string sourceIp) { Query.Where(p => p.TargetPostId == postId && diff --git a/src/Moonglade.Mention.Common/ClearMentionsCommand.cs b/src/Moonglade.Mention.Common/ClearMentionsCommand.cs new file mode 100644 index 000000000..0cb5c2b83 --- /dev/null +++ b/src/Moonglade.Mention.Common/ClearMentionsCommand.cs @@ -0,0 +1,12 @@ +using MediatR; +using Moonglade.Data; +using Moonglade.Data.Entities; + +namespace Moonglade.Mention.Common; + +public record ClearMentionsCommand : IRequest; + +public class ClearPingbackCommandHandler(MoongladeRepository repo) : IRequestHandler +{ + public Task Handle(ClearMentionsCommand request, CancellationToken ct) => repo.Clear(ct); +} \ No newline at end of file diff --git a/src/Moonglade.Mention.Common/DeleteMentionCommand.cs b/src/Moonglade.Mention.Common/DeleteMentionCommand.cs new file mode 100644 index 000000000..ce06ef69e --- /dev/null +++ b/src/Moonglade.Mention.Common/DeleteMentionCommand.cs @@ -0,0 +1,19 @@ +using MediatR; +using Moonglade.Data; +using Moonglade.Data.Entities; + +namespace Moonglade.Mention.Common; + +public record DeleteMentionCommand(Guid Id) : IRequest; + +public class DeletePingbackCommandHandler(MoongladeRepository repo) : IRequestHandler +{ + public async Task Handle(DeleteMentionCommand request, CancellationToken ct) + { + var entity = await repo.GetByIdAsync(request.Id, ct); + if (entity != null) + { + await repo.DeleteAsync(entity, ct); + } + } +} \ No newline at end of file diff --git a/src/Moonglade.Mention.Common/GetMentionsQuery.cs b/src/Moonglade.Mention.Common/GetMentionsQuery.cs new file mode 100644 index 000000000..97489d4e9 --- /dev/null +++ b/src/Moonglade.Mention.Common/GetMentionsQuery.cs @@ -0,0 +1,15 @@ +using MediatR; +using Moonglade.Data; +using Moonglade.Data.Entities; +using Moonglade.Data.Specifications; + +namespace Moonglade.Mention.Common; + +public record GetMentionsQuery : IRequest>; + +public class GetMentionsQueryHandler(MoongladeRepository repo) : + IRequestHandler> +{ + public Task> Handle(GetMentionsQuery request, CancellationToken ct) => + repo.ListAsync(new MentionReadOnlySpec(), ct); +} \ No newline at end of file diff --git a/src/Moonglade.Pingback/PingRequest.cs b/src/Moonglade.Mention.Common/MentionRequest.cs similarity index 60% rename from src/Moonglade.Pingback/PingRequest.cs rename to src/Moonglade.Mention.Common/MentionRequest.cs index 9ff0ff808..af244cacc 100644 --- a/src/Moonglade.Pingback/PingRequest.cs +++ b/src/Moonglade.Mention.Common/MentionRequest.cs @@ -1,6 +1,6 @@ -namespace Moonglade.Pingback; +namespace Moonglade.Mention.Common; -public record PingRequest +public record MentionRequest { public string SourceUrl { get; set; } @@ -10,5 +10,5 @@ public record PingRequest public bool ContainsHtml { get; set; } - public bool SourceHasLink { get; set; } + public bool SourceHasTarget { get; set; } } \ No newline at end of file diff --git a/src/Moonglade.Pingback/PingSourceInspector.cs b/src/Moonglade.Mention.Common/MentionSourceInspector.cs similarity index 61% rename from src/Moonglade.Pingback/PingSourceInspector.cs rename to src/Moonglade.Mention.Common/MentionSourceInspector.cs index fcf563746..1c275169b 100644 --- a/src/Moonglade.Pingback/PingSourceInspector.cs +++ b/src/Moonglade.Mention.Common/MentionSourceInspector.cs @@ -2,16 +2,16 @@ using System.Net; using System.Text.RegularExpressions; -namespace Moonglade.Pingback; +namespace Moonglade.Mention.Common; -public interface IPingSourceInspector +public interface IMentionSourceInspector { - Task ExamineSourceAsync(string sourceUrl, string targetUrl); + Task ExamineSourceAsync(string sourceUrl, string targetUrl); } -public class PingSourceInspector(ILogger logger, HttpClient httpClient) : IPingSourceInspector +public class MentionSourceInspector(ILogger logger, HttpClient httpClient) : IMentionSourceInspector { - public async Task ExamineSourceAsync(string sourceUrl, string targetUrl) + public async Task ExamineSourceAsync(string sourceUrl, string targetUrl) { try { @@ -25,18 +25,18 @@ public async Task ExamineSourceAsync(string sourceUrl, string targe var html = await httpClient.GetStringAsync(sourceUrl); var title = regexTitle.Match(html).Value.Trim(); var containsHtml = regexHtml.IsMatch(title); - var sourceHasLink = html.ToUpperInvariant().Contains(targetUrl.ToUpperInvariant()); + var sourceHasTarget = html.ToUpperInvariant().Contains(targetUrl.TrimEnd('/').ToUpperInvariant()); - var pingRequest = new PingRequest + var mentionRequest = new MentionRequest { Title = title, ContainsHtml = containsHtml, - SourceHasLink = sourceHasLink, + SourceHasTarget = sourceHasTarget, TargetUrl = targetUrl, SourceUrl = sourceUrl }; - return pingRequest; + return mentionRequest; } catch (WebException ex) { diff --git a/src/Moonglade.Mention.Common/Moonglade.Mention.Common.csproj b/src/Moonglade.Mention.Common/Moonglade.Mention.Common.csproj new file mode 100644 index 000000000..97c1a5e55 --- /dev/null +++ b/src/Moonglade.Mention.Common/Moonglade.Mention.Common.csproj @@ -0,0 +1,16 @@ + + + net8.0 + enable + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Moonglade.Mention.Common/ServiceCollectionExtensions.cs b/src/Moonglade.Mention.Common/ServiceCollectionExtensions.cs new file mode 100644 index 000000000..14c8ff75b --- /dev/null +++ b/src/Moonglade.Mention.Common/ServiceCollectionExtensions.cs @@ -0,0 +1,15 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace Moonglade.Mention.Common; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddMentionCommon(this IServiceCollection services) + { + services.AddHttpClient() + .ConfigureHttpClient(p => p.Timeout = TimeSpan.FromSeconds(30)) + .AddStandardResilienceHandler(); + + return services; + } +} \ No newline at end of file diff --git a/src/Moonglade.Pingback/ClearPingbackCommand.cs b/src/Moonglade.Pingback/ClearPingbackCommand.cs deleted file mode 100644 index 564ce684f..000000000 --- a/src/Moonglade.Pingback/ClearPingbackCommand.cs +++ /dev/null @@ -1,12 +0,0 @@ -using MediatR; -using Moonglade.Data; -using Moonglade.Data.Entities; - -namespace Moonglade.Pingback; - -public record ClearPingbackCommand : IRequest; - -public class ClearPingbackCommandHandler(MoongladeRepository repo) : IRequestHandler -{ - public Task Handle(ClearPingbackCommand request, CancellationToken ct) => repo.Clear(ct); -} \ No newline at end of file diff --git a/src/Moonglade.Pingback/DeletePingbackCommand.cs b/src/Moonglade.Pingback/DeletePingbackCommand.cs deleted file mode 100644 index 4e6289907..000000000 --- a/src/Moonglade.Pingback/DeletePingbackCommand.cs +++ /dev/null @@ -1,19 +0,0 @@ -using MediatR; -using Moonglade.Data; -using Moonglade.Data.Entities; - -namespace Moonglade.Pingback; - -public record DeletePingbackCommand(Guid Id) : IRequest; - -public class DeletePingbackCommandHandler(MoongladeRepository repo) : IRequestHandler -{ - public async Task Handle(DeletePingbackCommand request, CancellationToken ct) - { - var pingback = await repo.GetByIdAsync(request.Id, ct); - if (pingback != null) - { - await repo.DeleteAsync(pingback, ct); - } - } -} \ No newline at end of file diff --git a/src/Moonglade.Pingback/GetPingbacksQuery.cs b/src/Moonglade.Pingback/GetPingbacksQuery.cs deleted file mode 100644 index 368e3dd0c..000000000 --- a/src/Moonglade.Pingback/GetPingbacksQuery.cs +++ /dev/null @@ -1,15 +0,0 @@ -using MediatR; -using Moonglade.Data; -using Moonglade.Data.Entities; -using Moonglade.Data.Specifications; - -namespace Moonglade.Pingback; - -public record GetPingbacksQuery : IRequest>; - -public class GetPingbacksQueryHandler(MoongladeRepository repo) : - IRequestHandler> -{ - public Task> Handle(GetPingbacksQuery request, CancellationToken ct) => - repo.ListAsync(new PingbackReadOnlySpec(), ct); -} \ No newline at end of file diff --git a/src/Moonglade.Pingback/Moonglade.Pingback.csproj b/src/Moonglade.Pingback/Moonglade.Pingback.csproj index 0dd3204cf..4ac0143a8 100644 --- a/src/Moonglade.Pingback/Moonglade.Pingback.csproj +++ b/src/Moonglade.Pingback/Moonglade.Pingback.csproj @@ -1,11 +1,7 @@ - + Library net8.0 - Moonglade.Pingback - Moonglade.Pingback - false - false enable .NET implementation for Pingback http://www.hixie.ch/specs/pingback/pingback @@ -13,10 +9,6 @@ - - - - - + \ No newline at end of file diff --git a/src/Moonglade.Pingback/PingbackResponse.cs b/src/Moonglade.Pingback/PingbackResponse.cs index 73ed47759..c2088e7d7 100644 --- a/src/Moonglade.Pingback/PingbackResponse.cs +++ b/src/Moonglade.Pingback/PingbackResponse.cs @@ -1,6 +1,8 @@ -namespace Moonglade.Pingback; +using Moonglade.Data.Entities; -public enum PingbackResponse +namespace Moonglade.Pingback; + +public enum PingbackStatus { Success, GenericError, @@ -9,4 +11,18 @@ public enum PingbackResponse Error48PingbackAlreadyRegistered, Error17SourceNotContainTargetUri, SpamDetectedFakeNotFound +} + +public class PingbackResponse(PingbackStatus status) +{ + public PingbackStatus Status { get; set; } = status; + + public MentionEntity MentionEntity { get; set; } + + public static PingbackResponse GenericError => new(PingbackStatus.GenericError); + public static PingbackResponse InvalidPingRequest => new(PingbackStatus.InvalidPingRequest); + public static PingbackResponse Error32TargetUriNotExist => new(PingbackStatus.Error32TargetUriNotExist); + public static PingbackResponse Error48PingbackAlreadyRegistered => new(PingbackStatus.Error48PingbackAlreadyRegistered); + public static PingbackResponse Error17SourceNotContainTargetUri => new(PingbackStatus.Error17SourceNotContainTargetUri); + public static PingbackResponse SpamDetectedFakeNotFound => new(PingbackStatus.SpamDetectedFakeNotFound); } \ No newline at end of file diff --git a/src/Moonglade.Pingback/PingbackResult.cs b/src/Moonglade.Pingback/PingbackResult.cs index 47fc13feb..88e78a8f3 100644 --- a/src/Moonglade.Pingback/PingbackResult.cs +++ b/src/Moonglade.Pingback/PingbackResult.cs @@ -4,9 +4,9 @@ namespace Moonglade.Pingback; -public class PingbackResult(PingbackResponse pingbackResponse) : IActionResult +public class PingbackResult(PingbackStatus pingbackStatus) : IActionResult { - public PingbackResponse PingbackResponse { get; } = pingbackResponse; + public PingbackStatus PingbackStatus { get; } = pingbackStatus; public Task ExecuteResultAsync(ActionContext context) { @@ -19,29 +19,29 @@ public Task ExecuteResultAsync(ActionContext context) int statusCode = StatusCodes.Status201Created; IActionResult actionResult = null; - switch (PingbackResponse) + switch (PingbackStatus) { - case PingbackResponse.Success: + case PingbackStatus.Success: content = "Thanks!"; break; - case PingbackResponse.Error17SourceNotContainTargetUri: + case PingbackStatus.Error17SourceNotContainTargetUri: content = BuildErrorResponseBody(17, "The source URI does not contain a link to the target URI, and so cannot be used as a source."); break; - case PingbackResponse.Error32TargetUriNotExist: + case PingbackStatus.Error32TargetUriNotExist: content = BuildErrorResponseBody(32, "The specified target URI does not exist."); break; - case PingbackResponse.Error48PingbackAlreadyRegistered: + case PingbackStatus.Error48PingbackAlreadyRegistered: content = BuildErrorResponseBody(48, "The pingback has already been registered."); break; - case PingbackResponse.SpamDetectedFakeNotFound: + case PingbackStatus.SpamDetectedFakeNotFound: actionResult = new NotFoundResult(); break; - case PingbackResponse.GenericError: + case PingbackStatus.GenericError: actionResult = new StatusCodeResult(StatusCodes.Status500InternalServerError); break; - case PingbackResponse.InvalidPingRequest: + case PingbackStatus.InvalidPingRequest: actionResult = new BadRequestResult(); break; default: diff --git a/src/Moonglade.Pingback/ReceivePingCommand.cs b/src/Moonglade.Pingback/ReceivePingCommand.cs index b6f83571a..a426294e4 100644 --- a/src/Moonglade.Pingback/ReceivePingCommand.cs +++ b/src/Moonglade.Pingback/ReceivePingCommand.cs @@ -3,25 +3,23 @@ using Moonglade.Data; using Moonglade.Data.Entities; using Moonglade.Data.Specifications; +using Moonglade.Mention.Common; using System.Text.RegularExpressions; using System.Xml; namespace Moonglade.Pingback; -public class ReceivePingCommand(string requestBody, string ip, Action action) - : IRequest +public class ReceivePingCommand(string requestBody, string ip) : IRequest { public string RequestBody { get; set; } = requestBody; public string IP { get; set; } = ip; - - public Action Action { get; set; } = action; } public class ReceivePingCommandHandler( ILogger logger, - IPingSourceInspector pingSourceInspector, - MoongladeRepository pingbackRepo, + IMentionSourceInspector pingSourceInspector, + MoongladeRepository mentionRepo, MoongladeRepository postRepo) : IRequestHandler { private string _sourceUrl; @@ -44,7 +42,7 @@ public async Task Handle(ReceivePingCommand request, Cancellat var pingRequest = await pingSourceInspector.ExamineSourceAsync(_sourceUrl, _targetUrl); if (null == pingRequest) return PingbackResponse.InvalidPingRequest; - if (!pingRequest.SourceHasLink) + if (!pingRequest.SourceHasTarget) { logger.LogError("Pingback error: The source URI does not contain a link to the target URI."); return PingbackResponse.Error17SourceNotContainTargetUri; @@ -66,13 +64,13 @@ public async Task Handle(ReceivePingCommand request, Cancellat logger.LogInformation($"Post '{id}:{title}' is found for ping."); - var pinged = await pingbackRepo.AnyAsync(new PingbackSpec(id, pingRequest.SourceUrl, request.IP), ct); + var pinged = await mentionRepo.AnyAsync(new MentionSpec(id, pingRequest.SourceUrl, request.IP), ct); if (pinged) return PingbackResponse.Error48PingbackAlreadyRegistered; logger.LogInformation("Adding received pingback..."); var uri = new Uri(_sourceUrl); - var obj = new PingbackEntity + var obj = new MentionEntity { Id = Guid.NewGuid(), PingTimeUtc = DateTime.UtcNow, @@ -81,13 +79,16 @@ public async Task Handle(ReceivePingCommand request, Cancellat SourceTitle = pingRequest.Title, TargetPostId = id, TargetPostTitle = title, - SourceIp = request.IP + SourceIp = request.IP, + Worker = "Pingback" }; - await pingbackRepo.AddAsync(obj, ct); - request.Action?.Invoke(obj); + await mentionRepo.AddAsync(obj, ct); - return PingbackResponse.Success; + return new(PingbackStatus.Success) + { + MentionEntity = obj + }; } catch (Exception e) { diff --git a/src/Moonglade.Pingback/ServiceCollectionExtensions.cs b/src/Moonglade.Pingback/ServiceCollectionExtensions.cs index a56c97762..d578ce612 100644 --- a/src/Moonglade.Pingback/ServiceCollectionExtensions.cs +++ b/src/Moonglade.Pingback/ServiceCollectionExtensions.cs @@ -7,10 +7,6 @@ public static class ServiceCollectionExtensions { public static IServiceCollection AddPingback(this IServiceCollection services) { - services.AddHttpClient() - .ConfigureHttpClient(p => p.Timeout = TimeSpan.FromSeconds(30)) - .AddStandardResilienceHandler(); - services.AddHttpClient() .AddStandardResilienceHandler(); diff --git a/src/Moonglade.Web/Controllers/PingbackController.cs b/src/Moonglade.Web/Controllers/MentionController.cs similarity index 50% rename from src/Moonglade.Web/Controllers/PingbackController.cs rename to src/Moonglade.Web/Controllers/MentionController.cs index e4636d046..d06e8a0d7 100644 --- a/src/Moonglade.Web/Controllers/PingbackController.cs +++ b/src/Moonglade.Web/Controllers/MentionController.cs @@ -1,34 +1,58 @@ using Moonglade.Data.Entities; using Moonglade.Email.Client; +using Moonglade.Mention.Common; using Moonglade.Pingback; using Moonglade.Web.Attributes; +using System.ComponentModel.DataAnnotations; namespace Moonglade.Web.Controllers; [ApiController] -[Route("pingback")] -public class PingbackController( - ILogger logger, - IBlogConfig blogConfig, - IMediator mediator) : ControllerBase +[Route("api/[controller]")] +public class MentionController( + ILogger logger, + IBlogConfig blogConfig, + IMediator mediator) : ControllerBase { - [HttpPost] + [HttpPost("/webmention")] + public async Task ReceiveWebmention( + [FromForm][Required] string source, + [FromForm][Required] string target) + { + if (!blogConfig.AdvancedSettings.EnableWebmention) return Forbid(); + + var ip = Helper.GetClientIP(HttpContext); + // Verify that the source URL links to the target URL + // TODO + + // Process the Webmention + // TODO + + // For demonstration purposes, we'll just return a success message. + return Ok("Webmention received and verified."); + } + + [HttpPost("/pingback")] [IgnoreAntiforgeryToken] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] - public async Task Process() + public async Task ReceivePingback() { if (!blogConfig.AdvancedSettings.EnablePingback) return Forbid(); var ip = Helper.GetClientIP(HttpContext); var requestBody = await new StreamReader(HttpContext.Request.Body, Encoding.Default).ReadToEndAsync(); - var response = await mediator.Send(new ReceivePingCommand(requestBody, ip, SendPingbackEmailAction)); + var response = await mediator.Send(new ReceivePingCommand(requestBody, ip)); + if (response.Status == PingbackStatus.Success) + { + SendPingbackEmailAction(response.MentionEntity); + } - return new PingbackResult(response); + return new PingbackResult(response.Status); } - private async void SendPingbackEmailAction(PingbackEntity history) + private async void SendPingbackEmailAction(MentionEntity history) { try { @@ -45,7 +69,7 @@ private async void SendPingbackEmailAction(PingbackEntity history) [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task Delete([NotEmpty] Guid pingbackId) { - await mediator.Send(new DeletePingbackCommand(pingbackId)); + await mediator.Send(new DeleteMentionCommand(pingbackId)); return NoContent(); } @@ -54,7 +78,7 @@ public async Task Delete([NotEmpty] Guid pingbackId) [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task Clear() { - await mediator.Send(new ClearPingbackCommand()); + await mediator.Send(new ClearMentionsCommand()); return NoContent(); } } \ No newline at end of file diff --git a/src/Moonglade.Web/Pages/Admin/Pingback.cshtml b/src/Moonglade.Web/Pages/Admin/Mention.cshtml similarity index 64% rename from src/Moonglade.Web/Pages/Admin/Pingback.cshtml rename to src/Moonglade.Web/Pages/Admin/Mention.cshtml index aac30dbe5..fd7669638 100644 --- a/src/Moonglade.Web/Pages/Admin/Pingback.cshtml +++ b/src/Moonglade.Web/Pages/Admin/Mention.cshtml @@ -1,16 +1,16 @@ -@page "/admin/pingback" -@using Moonglade.Pingback +@page "/admin/mention" +@using Moonglade.Mention.Common @inject IMediator Mediator @{ - ViewBag.Title = "Pingback"; - var pingbackRecords = await Mediator.Send(new GetPingbacksQuery()); + ViewBag.Title = "Mentions"; + var mentionRecords = await Mediator.Send(new GetMentionsQuery()); } @Html.AntiForgeryToken() @section head {