2023-08-12 20:13:47 +01:00
|
|
|
using System.Diagnostics.CodeAnalysis;
|
2023-08-13 17:33:54 +01:00
|
|
|
using Humanizer;
|
|
|
|
using Markdig;
|
2023-08-12 20:13:47 +01:00
|
|
|
using Microsoft.EntityFrameworkCore;
|
2024-05-05 02:18:20 +01:00
|
|
|
using OliverBooth.Common.Data;
|
|
|
|
using OliverBooth.Common.Data.Blog;
|
|
|
|
using OliverBooth.Common.Services;
|
2023-08-13 17:33:54 +01:00
|
|
|
using OliverBooth.Data.Blog;
|
2023-08-12 20:13:47 +01:00
|
|
|
|
2023-08-13 17:33:54 +01:00
|
|
|
namespace OliverBooth.Services;
|
2023-08-12 20:13:47 +01:00
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Represents an implementation of <see cref="IBlogPostService" />.
|
|
|
|
/// </summary>
|
|
|
|
internal sealed class BlogPostService : IBlogPostService
|
|
|
|
{
|
|
|
|
private readonly IDbContextFactory<BlogContext> _dbContextFactory;
|
2023-08-13 17:33:54 +01:00
|
|
|
private readonly IBlogUserService _blogUserService;
|
|
|
|
private readonly MarkdownPipeline _markdownPipeline;
|
2023-08-12 20:13:47 +01:00
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Initializes a new instance of the <see cref="BlogPostService" /> class.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="dbContextFactory">
|
|
|
|
/// The <see cref="IDbContextFactory{TContext}" /> used to create a <see cref="BlogContext" />.
|
|
|
|
/// </param>
|
2023-08-13 17:33:54 +01:00
|
|
|
/// <param name="blogUserService">The <see cref="IBlogUserService" />.</param>
|
|
|
|
/// <param name="markdownPipeline">The <see cref="MarkdownPipeline" />.</param>
|
|
|
|
public BlogPostService(IDbContextFactory<BlogContext> dbContextFactory,
|
|
|
|
IBlogUserService blogUserService,
|
|
|
|
MarkdownPipeline markdownPipeline)
|
2023-08-12 20:13:47 +01:00
|
|
|
{
|
|
|
|
_dbContextFactory = dbContextFactory;
|
2023-08-13 17:33:54 +01:00
|
|
|
_blogUserService = blogUserService;
|
|
|
|
_markdownPipeline = markdownPipeline;
|
2023-08-12 20:13:47 +01:00
|
|
|
}
|
|
|
|
|
2023-08-13 17:33:54 +01:00
|
|
|
/// <inheritdoc />
|
2024-07-15 19:38:56 +01:00
|
|
|
public int GetBlogPostCount(Visibility visibility = Visibility.None, string[]? tags = null)
|
2023-08-13 17:33:54 +01:00
|
|
|
{
|
|
|
|
using BlogContext context = _dbContextFactory.CreateDbContext();
|
2024-07-15 19:38:56 +01:00
|
|
|
if (tags is { Length: > 0 })
|
|
|
|
{
|
|
|
|
return visibility == Visibility.None
|
|
|
|
? context.BlogPosts.AsEnumerable().Count(p => !p.IsRedirect && p.Tags.Intersect(tags).Any())
|
|
|
|
: context.BlogPosts.AsEnumerable().Count(p => !p.IsRedirect && p.Visibility == visibility && p.Tags.Intersect(tags).Any());
|
|
|
|
}
|
|
|
|
|
2024-05-05 18:13:06 +01:00
|
|
|
return visibility == Visibility.None
|
2024-05-06 17:39:28 +01:00
|
|
|
? context.BlogPosts.Count(p => !p.IsRedirect)
|
|
|
|
: context.BlogPosts.Count(p => !p.IsRedirect && p.Visibility == visibility);
|
2023-08-13 17:33:54 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
2023-08-12 20:13:47 +01:00
|
|
|
public IReadOnlyList<IBlogPost> GetAllBlogPosts(int limit = -1)
|
|
|
|
{
|
|
|
|
using BlogContext context = _dbContextFactory.CreateDbContext();
|
2023-08-20 14:22:52 +01:00
|
|
|
IQueryable<BlogPost> ordered = context.BlogPosts
|
2024-04-26 17:30:42 +01:00
|
|
|
.Where(p => p.Visibility == Visibility.Published && !p.IsRedirect)
|
2023-08-20 14:22:52 +01:00
|
|
|
.OrderByDescending(post => post.Published);
|
2023-08-13 17:33:54 +01:00
|
|
|
if (limit > -1)
|
|
|
|
{
|
|
|
|
ordered = ordered.Take(limit);
|
|
|
|
}
|
|
|
|
|
|
|
|
return ordered.AsEnumerable().Select(CacheAuthor).ToArray();
|
2023-08-12 20:13:47 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
2024-07-15 19:38:56 +01:00
|
|
|
public IReadOnlyList<IBlogPost> GetBlogPosts(int page, int pageSize = 10, string[]? tags = null)
|
2023-08-12 20:13:47 +01:00
|
|
|
{
|
|
|
|
using BlogContext context = _dbContextFactory.CreateDbContext();
|
2024-07-15 19:38:56 +01:00
|
|
|
IEnumerable<BlogPost> posts = context.BlogPosts
|
2024-04-26 17:30:42 +01:00
|
|
|
.Where(p => p.Visibility == Visibility.Published && !p.IsRedirect)
|
2023-08-12 20:13:47 +01:00
|
|
|
.OrderByDescending(post => post.Published)
|
2024-07-15 19:38:56 +01:00
|
|
|
.AsEnumerable();
|
|
|
|
|
|
|
|
if (tags is { Length: > 0 })
|
|
|
|
{
|
|
|
|
posts = posts.Where(p => p.Tags.Intersect(tags).Any());
|
|
|
|
}
|
|
|
|
|
|
|
|
return posts.Skip(page * pageSize)
|
2023-08-12 20:13:47 +01:00
|
|
|
.Take(pageSize)
|
2023-08-13 17:33:54 +01:00
|
|
|
.ToArray().Select(CacheAuthor).ToArray();
|
2023-08-12 20:13:47 +01:00
|
|
|
}
|
|
|
|
|
2024-05-01 16:47:31 +01:00
|
|
|
/// <inheritdoc />
|
|
|
|
public int GetLegacyCommentCount(IBlogPost post)
|
|
|
|
{
|
|
|
|
using BlogContext context = _dbContextFactory.CreateDbContext();
|
|
|
|
return context.LegacyComments.Count(c => c.PostId == post.Id);
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
public IReadOnlyList<ILegacyComment> GetLegacyComments(IBlogPost post)
|
|
|
|
{
|
|
|
|
using BlogContext context = _dbContextFactory.CreateDbContext();
|
|
|
|
return context.LegacyComments.Where(c => c.PostId == post.Id && c.ParentComment == null).ToArray();
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
public IReadOnlyList<ILegacyComment> GetLegacyReplies(ILegacyComment comment)
|
|
|
|
{
|
|
|
|
using BlogContext context = _dbContextFactory.CreateDbContext();
|
|
|
|
return context.LegacyComments.Where(c => c.ParentComment == comment.Id).ToArray();
|
|
|
|
}
|
|
|
|
|
2023-09-19 19:29:21 +01:00
|
|
|
/// <inheritdoc />
|
|
|
|
public IBlogPost? GetNextPost(IBlogPost blogPost)
|
|
|
|
{
|
|
|
|
using BlogContext context = _dbContextFactory.CreateDbContext();
|
|
|
|
return context.BlogPosts
|
2024-04-26 17:30:42 +01:00
|
|
|
.Where(p => p.Visibility == Visibility.Published && !p.IsRedirect)
|
2023-09-19 19:29:21 +01:00
|
|
|
.OrderBy(post => post.Published)
|
|
|
|
.FirstOrDefault(post => post.Published > blogPost.Published);
|
|
|
|
}
|
|
|
|
|
2024-05-05 18:13:06 +01:00
|
|
|
/// <inheritdoc />
|
2024-07-15 19:38:56 +01:00
|
|
|
public int GetPageCount(int pageSize = 10, Visibility visibility = Visibility.None, string[]? tags = null)
|
2024-05-05 18:13:06 +01:00
|
|
|
{
|
2024-07-15 19:38:56 +01:00
|
|
|
float postCount = GetBlogPostCount(visibility, tags);
|
2024-05-05 18:13:06 +01:00
|
|
|
return (int)MathF.Ceiling(postCount / pageSize);
|
|
|
|
}
|
|
|
|
|
2023-09-19 19:29:21 +01:00
|
|
|
/// <inheritdoc />
|
|
|
|
public IBlogPost? GetPreviousPost(IBlogPost blogPost)
|
|
|
|
{
|
|
|
|
using BlogContext context = _dbContextFactory.CreateDbContext();
|
|
|
|
return context.BlogPosts
|
2024-04-26 17:30:42 +01:00
|
|
|
.Where(p => p.Visibility == Visibility.Published && !p.IsRedirect)
|
2023-09-19 19:29:21 +01:00
|
|
|
.OrderByDescending(post => post.Published)
|
|
|
|
.FirstOrDefault(post => post.Published < blogPost.Published);
|
|
|
|
}
|
|
|
|
|
2023-08-12 20:13:47 +01:00
|
|
|
/// <inheritdoc />
|
|
|
|
public string RenderExcerpt(IBlogPost post, out bool wasTrimmed)
|
|
|
|
{
|
2024-04-27 15:41:19 +01:00
|
|
|
if (!string.IsNullOrWhiteSpace(post.Excerpt))
|
|
|
|
{
|
|
|
|
wasTrimmed = false;
|
|
|
|
return Markdig.Markdown.ToHtml(post.Excerpt, _markdownPipeline);
|
|
|
|
}
|
|
|
|
|
2023-08-13 17:33:54 +01:00
|
|
|
string body = post.Body;
|
|
|
|
int moreIndex = body.IndexOf("<!--more-->", StringComparison.Ordinal);
|
|
|
|
|
|
|
|
if (moreIndex == -1)
|
|
|
|
{
|
|
|
|
string excerpt = body.Truncate(255, "...");
|
|
|
|
wasTrimmed = body.Length > 255;
|
|
|
|
return Markdig.Markdown.ToHtml(excerpt, _markdownPipeline);
|
|
|
|
}
|
|
|
|
|
|
|
|
wasTrimmed = true;
|
|
|
|
return Markdig.Markdown.ToHtml(body[..moreIndex], _markdownPipeline);
|
2023-08-12 20:13:47 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
public string RenderPost(IBlogPost post)
|
|
|
|
{
|
2023-08-13 17:33:54 +01:00
|
|
|
return Markdig.Markdown.ToHtml(post.Body, _markdownPipeline);
|
2023-08-12 20:13:47 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
public bool TryGetPost(Guid id, [NotNullWhen(true)] out IBlogPost? post)
|
|
|
|
{
|
|
|
|
using BlogContext context = _dbContextFactory.CreateDbContext();
|
|
|
|
post = context.BlogPosts.Find(id);
|
|
|
|
if (post is null)
|
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
CacheAuthor((BlogPost)post);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
public bool TryGetPost(int id, [NotNullWhen(true)] out IBlogPost? post)
|
|
|
|
{
|
|
|
|
using BlogContext context = _dbContextFactory.CreateDbContext();
|
|
|
|
post = context.BlogPosts.FirstOrDefault(p => p.WordPressId == id);
|
|
|
|
if (post is null)
|
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
CacheAuthor((BlogPost)post);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
public bool TryGetPost(DateOnly publishDate, string slug, [NotNullWhen(true)] out IBlogPost? post)
|
|
|
|
{
|
|
|
|
using BlogContext context = _dbContextFactory.CreateDbContext();
|
|
|
|
post = context.BlogPosts.FirstOrDefault(post => post.Published.Year == publishDate.Year &&
|
|
|
|
post.Published.Month == publishDate.Month &&
|
|
|
|
post.Published.Day == publishDate.Day &&
|
|
|
|
post.Slug == slug);
|
|
|
|
|
|
|
|
if (post is null)
|
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
CacheAuthor((BlogPost)post);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
private BlogPost CacheAuthor(BlogPost post)
|
|
|
|
{
|
|
|
|
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
|
|
|
|
if (post.Author is not null)
|
|
|
|
{
|
|
|
|
return post;
|
|
|
|
}
|
|
|
|
|
2023-08-13 17:33:54 +01:00
|
|
|
if (_blogUserService.TryGetUser(post.AuthorId, out IUser? user) && user is IBlogAuthor author)
|
2023-08-12 20:13:47 +01:00
|
|
|
{
|
|
|
|
post.Author = author;
|
|
|
|
}
|
|
|
|
|
|
|
|
return post;
|
|
|
|
}
|
|
|
|
}
|