Webiant Logo Webiant Logo
  1. No results found.

    Try your search with a different keyword or use * as a wildcard.

ForumService.cs

using Nop.Core;
using Nop.Core.Caching;
using Nop.Core.Domain.Customers;
using Nop.Core.Domain.Forums;
using Nop.Core.Domain.Seo;
using Nop.Data;
using Nop.Services.Common;
using Nop.Services.Customers;
using Nop.Services.Html;
using Nop.Services.Messages;
using Nop.Services.Seo;

namespace Nop.Services.Forums;

/// <summary>
/// Forum service
/// </summary>
public partial class ForumService : IForumService
{
    #region Fields

    protected readonly ForumSettings _forumSettings;
    protected readonly ICustomerService _customerService;
    protected readonly IGenericAttributeService _genericAttributeService;
    protected readonly IHtmlFormatter _htmlFormatter;
    protected readonly IRepository<Customer> _customerRepository;
    protected readonly IRepository<Forum> _forumRepository;
    protected readonly IRepository<ForumGroup> _forumGroupRepository;
    protected readonly IRepository<ForumPost> _forumPostRepository;
    protected readonly IRepository<ForumPostVote> _forumPostVoteRepository;
    protected readonly IRepository<ForumSubscription> _forumSubscriptionRepository;
    protected readonly IRepository<ForumTopic> _forumTopicRepository;
    protected readonly IRepository<PrivateMessage> _forumPrivateMessageRepository;
    protected readonly IStaticCacheManager _staticCacheManager;
    protected readonly IUrlRecordService _urlRecordService;
    protected readonly IWorkContext _workContext;
    protected readonly IWorkflowMessageService _workflowMessageService;
    protected readonly SeoSettings _seoSettings;

    #endregion

    #region Ctor

    public ForumService(ForumSettings forumSettings,
        ICustomerService customerService,
        IGenericAttributeService genericAttributeService,
        IHtmlFormatter htmlFormatter,
        IRepository<Customer> customerRepository,
        IRepository<Forum> forumRepository,
        IRepository<ForumGroup> forumGroupRepository,
        IRepository<ForumPost> forumPostRepository,
        IRepository<ForumPostVote> forumPostVoteRepository,
        IRepository<ForumSubscription> forumSubscriptionRepository,
        IRepository<ForumTopic> forumTopicRepository,
        IRepository<PrivateMessage> forumPrivateMessageRepository,
        IStaticCacheManager staticCacheManager,
        IUrlRecordService urlRecordService,
        IWorkContext workContext,
        IWorkflowMessageService workflowMessageService,
        SeoSettings seoSettings)
    {
        _forumSettings = forumSettings;
        _customerService = customerService;
        _genericAttributeService = genericAttributeService;
        _htmlFormatter = htmlFormatter;
        _customerRepository = customerRepository;
        _forumRepository = forumRepository;
        _forumGroupRepository = forumGroupRepository;
        _forumPostRepository = forumPostRepository;
        _forumPostVoteRepository = forumPostVoteRepository;
        _forumSubscriptionRepository = forumSubscriptionRepository;
        _forumTopicRepository = forumTopicRepository;
        _forumPrivateMessageRepository = forumPrivateMessageRepository;
        _staticCacheManager = staticCacheManager;
        _urlRecordService = urlRecordService;
        _workContext = workContext;
        _workflowMessageService = workflowMessageService;
        _seoSettings = seoSettings;
    }

    #endregion

    #region Utilities

    /// <summary>
    /// Update forum stats
    /// </summary>
    /// <param name="forumId">The forum identifier</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    protected virtual async Task UpdateForumStatsAsync(int forumId)
    {
        if (forumId == 0)
            return;

        var forum = await GetForumByIdAsync(forumId);
        if (forum == null)
            return;

        //number of topics
        var queryNumTopics = from ft in _forumTopicRepository.Table
            where ft.ForumId == forumId
            select ft.Id;
        var numTopics = await queryNumTopics.CountAsync();

        //number of posts
        var queryNumPosts = from ft in _forumTopicRepository.Table
            join fp in _forumPostRepository.Table on ft.Id equals fp.TopicId
            where ft.ForumId == forumId
            select fp.Id;
        var numPosts = await queryNumPosts.CountAsync();

        //last values
        var lastTopicId = 0;
        var lastPostId = 0;
        var lastPostCustomerId = 0;
        DateTime? lastPostTime = null;
        var queryLastValues = from ft in _forumTopicRepository.Table
            join fp in _forumPostRepository.Table on ft.Id equals fp.TopicId
            where ft.ForumId == forumId
            orderby fp.CreatedOnUtc descending, ft.CreatedOnUtc descending
            select new
            {
                LastTopicId = ft.Id,
                LastPostId = fp.Id,
                LastPostCustomerId = fp.CustomerId,
                LastPostTime = fp.CreatedOnUtc
            };
        var lastValues = await queryLastValues.FirstOrDefaultAsync();
        if (lastValues != null)
        {
            lastTopicId = lastValues.LastTopicId;
            lastPostId = lastValues.LastPostId;
            lastPostCustomerId = lastValues.LastPostCustomerId;
            lastPostTime = lastValues.LastPostTime;
        }

        //update forum
        forum.NumTopics = numTopics;
        forum.NumPosts = numPosts;
        forum.LastTopicId = lastTopicId;
        forum.LastPostId = lastPostId;
        forum.LastPostCustomerId = lastPostCustomerId;
        forum.LastPostTime = lastPostTime;
        await UpdateForumAsync(forum);
    }

    /// <summary>
    /// Update forum topic stats
    /// </summary>
    /// <param name="forumTopicId">The forum topic identifier</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    protected virtual async Task UpdateForumTopicStatsAsync(int forumTopicId)
    {
        if (forumTopicId == 0)
            return;

        var forumTopic = await GetTopicByIdAsync(forumTopicId);
        if (forumTopic == null)
            return;

        //number of posts
        var queryNumPosts = from fp in _forumPostRepository.Table
            where fp.TopicId == forumTopicId
            select fp.Id;
        var numPosts = await queryNumPosts.CountAsync();

        //last values
        var lastPostId = 0;
        var lastPostCustomerId = 0;
        DateTime? lastPostTime = null;
        var queryLastValues = from fp in _forumPostRepository.Table
            where fp.TopicId == forumTopicId
            orderby fp.CreatedOnUtc descending
            select new
            {
                LastPostId = fp.Id,
                LastPostCustomerId = fp.CustomerId,
                LastPostTime = fp.CreatedOnUtc
            };
        var lastValues = await queryLastValues.FirstOrDefaultAsync();
        if (lastValues != null)
        {
            lastPostId = lastValues.LastPostId;
            lastPostCustomerId = lastValues.LastPostCustomerId;
            lastPostTime = lastValues.LastPostTime;
        }

        //update topic
        forumTopic.NumPosts = numPosts;
        forumTopic.LastPostId = lastPostId;
        forumTopic.LastPostCustomerId = lastPostCustomerId;
        forumTopic.LastPostTime = lastPostTime;

        await UpdateTopicAsync(forumTopic);
    }

    /// <summary>
    /// Update customer stats
    /// </summary>
    /// <param name="customerId">The customer identifier</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    protected virtual async Task UpdateCustomerStatsAsync(int customerId)
    {
        if (customerId == 0)
            return;

        var customer = await _customerService.GetCustomerByIdAsync(customerId);

        if (customer == null)
            return;

        var query = from fp in _forumPostRepository.Table
            where fp.CustomerId == customerId
            select fp.Id;
        var numPosts = await query.CountAsync();

        await _genericAttributeService.SaveAttributeAsync(customer, NopCustomerDefaults.ForumPostCountAttribute, numPosts);
    }

    /// <summary>
    /// Gets a forum topic
    /// </summary>
    /// <param name="forumTopicId">The forum topic identifier</param>
    /// <param name="increaseViews">The value indicating whether to increase forum topic views</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the forum Topic
    /// </returns>
    protected virtual async Task<ForumTopic> GetTopicByIdAsync(int forumTopicId, bool increaseViews)
    {
        var forumTopic = await _forumTopicRepository.GetByIdAsync(forumTopicId, cache => default);

        if (forumTopic == null)
            return null;

        if (!increaseViews)
            return forumTopic;

        forumTopic.Views = ++forumTopic.Views;
        await UpdateTopicAsync(forumTopic);

        return forumTopic;
    }

    #endregion

    #region Methods

    /// <summary>
    /// Deletes a forum group
    /// </summary>
    /// <param name="forumGroup">Forum group</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public virtual async Task DeleteForumGroupAsync(ForumGroup forumGroup)
    {
        await _forumGroupRepository.DeleteAsync(forumGroup);
    }

    /// <summary>
    /// Gets a forum group
    /// </summary>
    /// <param name="forumGroupId">The forum group identifier</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the forum group
    /// </returns>
    public virtual async Task<ForumGroup> GetForumGroupByIdAsync(int forumGroupId)
    {
        return await _forumGroupRepository.GetByIdAsync(forumGroupId, cache => default);
    }

    /// <summary>
    /// Gets all forum groups
    /// </summary>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the forum groups
    /// </returns>
    public virtual async Task<IList<ForumGroup>> GetAllForumGroupsAsync()
    {
        return await _forumGroupRepository.GetAllAsync(query =>
        {
            return from fg in query
                orderby fg.DisplayOrder, fg.Id
                select fg;
        }, cache => default);
    }

    /// <summary>
    /// Inserts a forum group
    /// </summary>
    /// <param name="forumGroup">Forum group</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public virtual async Task InsertForumGroupAsync(ForumGroup forumGroup)
    {
        await _forumGroupRepository.InsertAsync(forumGroup);
    }

    /// <summary>
    /// Updates the forum group
    /// </summary>
    /// <param name="forumGroup">Forum group</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public virtual async Task UpdateForumGroupAsync(ForumGroup forumGroup)
    {
        await _forumGroupRepository.UpdateAsync(forumGroup);
    }

    /// <summary>
    /// Deletes a forum
    /// </summary>
    /// <param name="forum">Forum</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public virtual async Task DeleteForumAsync(Forum forum)
    {
        ArgumentNullException.ThrowIfNull(forum);

        //delete forum subscriptions (topics)
        var queryTopicIds = from ft in _forumTopicRepository.Table
            where ft.ForumId == forum.Id
            select ft.Id;
        var queryFs1 = from fs in _forumSubscriptionRepository.Table
            where queryTopicIds.Contains(fs.TopicId)
            select fs;

        await _forumSubscriptionRepository.DeleteAsync(await queryFs1.ToListAsync());

        //delete forum subscriptions (forum)
        var queryFs2 = from fs in _forumSubscriptionRepository.Table
            where fs.ForumId == forum.Id
            select fs;

        await _forumSubscriptionRepository.DeleteAsync(await queryFs2.ToListAsync());

        //delete forum
        await _forumRepository.DeleteAsync(forum);
    }

    /// <summary>
    /// Gets a forum
    /// </summary>
    /// <param name="forumId">The forum identifier</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the forum
    /// </returns>
    public virtual async Task<Forum> GetForumByIdAsync(int forumId)
    {
        return await _forumRepository.GetByIdAsync(forumId, cache => default);
    }

    /// <summary>
    /// Gets forums by forum group identifier
    /// </summary>
    /// <param name="forumGroupId">The forum group identifier</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the forums
    /// </returns>
    public virtual async Task<IList<Forum>> GetAllForumsByGroupIdAsync(int forumGroupId)
    {
        var forums = await _forumRepository.GetAllAsync(query =>
        {
            return from f in query
                orderby f.DisplayOrder, f.Id
                where f.ForumGroupId == forumGroupId
                select f;
        }, cache => cache.PrepareKeyForDefaultCache(NopForumDefaults.ForumByForumGroupCacheKey, forumGroupId));

        return forums;
    }

    /// <summary>
    /// Inserts a forum
    /// </summary>
    /// <param name="forum">Forum</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public virtual async Task InsertForumAsync(Forum forum)
    {
        await _forumRepository.InsertAsync(forum);
    }

    /// <summary>
    /// Updates the forum
    /// </summary>
    /// <param name="forum">Forum</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public virtual async Task UpdateForumAsync(Forum forum)
    {
        // if the forum group is changed then clear cache for the previous group 
        // (we can't use the event consumer because it will work after saving the changes in DB)
        var forumToUpdate = await _forumRepository.LoadOriginalCopyAsync(forum);
        if (forumToUpdate.ForumGroupId != forum.ForumGroupId)
            await _staticCacheManager.RemoveAsync(NopForumDefaults.ForumByForumGroupCacheKey, forumToUpdate.ForumGroupId);

        await _forumRepository.UpdateAsync(forum);
    }

    /// <summary>
    /// Deletes a forum topic
    /// </summary>
    /// <param name="forumTopic">Forum topic</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public virtual async Task DeleteTopicAsync(ForumTopic forumTopic)
    {
        ArgumentNullException.ThrowIfNull(forumTopic);

        var customerId = forumTopic.CustomerId;
        var forumId = forumTopic.ForumId;

        //delete topic
        await _forumTopicRepository.DeleteAsync(forumTopic);

        //delete forum subscriptions
        var queryFs = from ft in _forumSubscriptionRepository.Table
            where ft.TopicId == forumTopic.Id
            select ft;
        var forumSubscriptions = await queryFs.ToListAsync();

        await _forumSubscriptionRepository.DeleteAsync(forumSubscriptions);

        //update stats
        await UpdateForumStatsAsync(forumId);
        await UpdateCustomerStatsAsync(customerId);
    }

    /// <summary>
    /// Gets a forum topic
    /// </summary>
    /// <param name="forumTopicId">The forum topic identifier</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the forum Topic
    /// </returns>
    public virtual async Task<ForumTopic> GetTopicByIdAsync(int forumTopicId)
    {
        return await GetTopicByIdAsync(forumTopicId, false);
    }

    /// <summary>
    /// Gets all forum topics
    /// </summary>
    /// <param name="forumId">The forum identifier</param>
    /// <param name="customerId">The customer identifier</param>
    /// <param name="keywords">Keywords</param>
    /// <param name="searchType">Search type</param>
    /// <param name="limitDays">Limit by the last number days; 0 to load all topics</param>
    /// <param name="pageIndex">Page index</param>
    /// <param name="pageSize">Page size</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the forum Topics
    /// </returns>
    public virtual async Task<IPagedList<ForumTopic>> GetAllTopicsAsync(int forumId = 0,
        int customerId = 0, string keywords = "", ForumSearchType searchType = ForumSearchType.All,
        int limitDays = 0, int pageIndex = 0, int pageSize = int.MaxValue)
    {
        DateTime? limitDate = null;
        if (limitDays > 0)
            limitDate = DateTime.UtcNow.AddDays(-limitDays);

        var searchKeywords = !string.IsNullOrEmpty(keywords);
        var searchTopicTitles = searchType == ForumSearchType.All || searchType == ForumSearchType.TopicTitlesOnly;
        var searchPostText = searchType == ForumSearchType.All || searchType == ForumSearchType.PostTextOnly;

        var topics = await _forumTopicRepository.GetAllPagedAsync(query =>
        {
            var query1 = from ft in query
                join fp in _forumPostRepository.Table on ft.Id equals fp.TopicId
                where
                    (forumId == 0 || ft.ForumId == forumId) &&
                    (customerId == 0 || ft.CustomerId == customerId) &&
                    (!searchKeywords ||
                     (searchTopicTitles && ft.Subject.Contains(keywords)) ||
                     (searchPostText && fp.Text.Contains(keywords))) &&
                    (!limitDate.HasValue || limitDate.Value <= ft.LastPostTime)
                select ft.Id;

            var query2 = from ft in query
                where query1.Contains(ft.Id)
                orderby ft.TopicTypeId descending, ft.LastPostTime descending, ft.Id descending
                select ft;

            return query2;
        }, pageIndex, pageSize);

        return topics;
    }

    /// <summary>
    /// Gets active forum topics
    /// </summary>
    /// <param name="forumId">The forum identifier</param>
    /// <param name="pageIndex">Page index</param>
    /// <param name="pageSize">Page size</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the forum Topics
    /// </returns>
    public virtual async Task<IPagedList<ForumTopic>> GetActiveTopicsAsync(int forumId = 0,
        int pageIndex = 0, int pageSize = int.MaxValue)
    {
        var query1 = from ft in _forumTopicRepository.Table
            where
                (forumId == 0 || ft.ForumId == forumId) &&
                ft.LastPostTime.HasValue
            select ft.Id;

        var query2 = from ft in _forumTopicRepository.Table
            where query1.Contains(ft.Id)
            orderby ft.LastPostTime descending
            select ft;

        var topics = await query2.ToPagedListAsync(pageIndex, pageSize);

        return topics;
    }

    /// <summary>
    /// Inserts a forum topic
    /// </summary>
    /// <param name="forumTopic">Forum topic</param>
    /// <param name="sendNotifications">A value indicating whether to send notifications to subscribed customers</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public virtual async Task InsertTopicAsync(ForumTopic forumTopic, bool sendNotifications)
    {
        await _forumTopicRepository.InsertAsync(forumTopic);

        //update stats
        await UpdateForumStatsAsync(forumTopic.ForumId);

        if (!sendNotifications)
            return;

        //send notifications
        var forum = await GetForumByIdAsync(forumTopic.ForumId);
        var subscriptions = await GetAllSubscriptionsAsync(forumId: forum.Id);
        var languageId = (await _workContext.GetWorkingLanguageAsync()).Id;

        foreach (var subscription in subscriptions)
        {
            if (subscription.CustomerId == forumTopic.CustomerId)
                continue;

            var customer = await _customerService.GetCustomerByIdAsync(subscription.CustomerId);

            if (!string.IsNullOrEmpty(customer?.Email))
                await _workflowMessageService.SendNewForumTopicMessageAsync(customer, forumTopic, forum, languageId);
        }
    }

    /// <summary>
    /// Updates the forum topic
    /// </summary>
    /// <param name="forumTopic">Forum topic</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public virtual async Task UpdateTopicAsync(ForumTopic forumTopic)
    {
        await _forumTopicRepository.UpdateAsync(forumTopic);
    }

    /// <summary>
    /// Moves the forum topic
    /// </summary>
    /// <param name="forumTopicId">The forum topic identifier</param>
    /// <param name="newForumId">New forum identifier</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the moved forum topic
    /// </returns>
    public virtual async Task<ForumTopic> MoveTopicAsync(int forumTopicId, int newForumId)
    {
        var forumTopic = await GetTopicByIdAsync(forumTopicId);
        if (forumTopic == null)
            return null;

        if (!await IsCustomerAllowedToMoveTopicAsync(await _workContext.GetCurrentCustomerAsync(), forumTopic))
            return forumTopic;

        var previousForumId = forumTopic.ForumId;
        var newForum = await GetForumByIdAsync(newForumId);

        if (newForum == null)
            return forumTopic;

        if (previousForumId == newForumId)
            return forumTopic;

        forumTopic.ForumId = newForum.Id;
        forumTopic.UpdatedOnUtc = DateTime.UtcNow;
        await UpdateTopicAsync(forumTopic);

        //update forum stats
        await UpdateForumStatsAsync(previousForumId);
        await UpdateForumStatsAsync(newForumId);
        return forumTopic;
    }

    /// <summary>
    /// Deletes a forum post
    /// </summary>
    /// <param name="forumPost">Forum post</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public virtual async Task DeletePostAsync(ForumPost forumPost)
    {
        ArgumentNullException.ThrowIfNull(forumPost);

        var forumTopicId = forumPost.TopicId;
        var customerId = forumPost.CustomerId;
        var forumTopic = await GetTopicByIdAsync(forumTopicId);
        var forumId = forumTopic.ForumId;

        //delete topic if it was the first post
        var deleteTopic = false;
        var firstPost = await GetFirstPostAsync(forumTopic);
        if (firstPost != null && firstPost.Id == forumPost.Id)
            deleteTopic = true;

        //delete forum post
        await _forumPostRepository.DeleteAsync(forumPost);

        //delete topic
        if (deleteTopic)
            await DeleteTopicAsync(forumTopic);

        //update stats
        if (!deleteTopic)
            await UpdateForumTopicStatsAsync(forumTopicId);

        await UpdateForumStatsAsync(forumId);
        await UpdateCustomerStatsAsync(customerId);
    }

    /// <summary>
    /// Gets a forum post
    /// </summary>
    /// <param name="forumPostId">The forum post identifier</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the forum Post
    /// </returns>
    public virtual async Task<ForumPost> GetPostByIdAsync(int forumPostId)
    {
        return await _forumPostRepository.GetByIdAsync(forumPostId, cache => default, useShortTermCache: true);
    }

    /// <summary>
    /// Gets all forum posts
    /// </summary>
    /// <param name="forumTopicId">The forum topic identifier</param>
    /// <param name="customerId">The customer identifier</param>
    /// <param name="keywords">Keywords</param>
    /// <param name="pageIndex">Page index</param>
    /// <param name="pageSize">Page size</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the posts
    /// </returns>
    public virtual async Task<IPagedList<ForumPost>> GetAllPostsAsync(int forumTopicId = 0,
        int customerId = 0, string keywords = "",
        int pageIndex = 0, int pageSize = int.MaxValue)
    {
        return await GetAllPostsAsync(forumTopicId, customerId, keywords, true,
            pageIndex, pageSize);
    }

    /// <summary>
    /// Gets all forum posts
    /// </summary>
    /// <param name="forumTopicId">The forum topic identifier</param>
    /// <param name="customerId">The customer identifier</param>
    /// <param name="keywords">Keywords</param>
    /// <param name="ascSort">Sort order</param>
    /// <param name="pageIndex">Page index</param>
    /// <param name="pageSize">Page size</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the forum Posts
    /// </returns>
    public virtual async Task<IPagedList<ForumPost>> GetAllPostsAsync(int forumTopicId = 0, int customerId = 0,
        string keywords = "", bool ascSort = false,
        int pageIndex = 0, int pageSize = int.MaxValue)
    {
        var forumPosts = await _forumPostRepository.GetAllPagedAsync(query =>
        {
            if (forumTopicId > 0)
                query = query.Where(fp => forumTopicId == fp.TopicId);

            if (customerId > 0)
                query = query.Where(fp => customerId == fp.CustomerId);

            if (!string.IsNullOrEmpty(keywords))
                query = query.Where(fp => fp.Text.Contains(keywords));

            query = ascSort
                ? query.OrderBy(fp => fp.CreatedOnUtc).ThenBy(fp => fp.Id)
                : query.OrderByDescending(fp => fp.CreatedOnUtc).ThenBy(fp => fp.Id);

            return query;
        }, pageIndex, pageSize);

        return forumPosts;
    }

    /// <summary>
    /// Inserts a forum post
    /// </summary>
    /// <param name="forumPost">The forum post</param>
    /// <param name="sendNotifications">A value indicating whether to send notifications to subscribed customers</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public virtual async Task InsertPostAsync(ForumPost forumPost, bool sendNotifications)
    {
        await _forumPostRepository.InsertAsync(forumPost);

        //update stats
        var customerId = forumPost.CustomerId;
        var forumTopic = await GetTopicByIdAsync(forumPost.TopicId);
        var forumId = forumTopic.ForumId;

        await UpdateForumTopicStatsAsync(forumPost.TopicId);
        await UpdateForumStatsAsync(forumId);
        await UpdateCustomerStatsAsync(customerId);

        //notifications
        if (!sendNotifications)
            return;

        var forum = await GetForumByIdAsync(forumTopic.ForumId);
        var subscriptions = await GetAllSubscriptionsAsync(topicId: forumTopic.Id);

        var languageId = (await _workContext.GetWorkingLanguageAsync()).Id;

        var friendlyTopicPageIndex = await CalculateTopicPageIndexAsync(forumPost.TopicId,
            _forumSettings.PostsPageSize > 0 ? _forumSettings.PostsPageSize : 10,
            forumPost.Id) + 1;

        foreach (var subscription in subscriptions)
        {
            if (subscription.CustomerId == forumPost.CustomerId)
                continue;

            var customer = await _customerService.GetCustomerByIdAsync(subscription.CustomerId);

            if (!string.IsNullOrEmpty(customer?.Email))
                await _workflowMessageService.SendNewForumPostMessageAsync(customer, forumPost, forumTopic, forum, friendlyTopicPageIndex, languageId);
        }
    }

    /// <summary>
    /// Updates the forum post
    /// </summary>
    /// <param name="forumPost">Forum post</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public virtual async Task UpdatePostAsync(ForumPost forumPost)
    {
        await _forumPostRepository.UpdateAsync(forumPost);
    }

    /// <summary>
    /// Deletes a private message
    /// </summary>
    /// <param name="privateMessage">Private message</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public virtual async Task DeletePrivateMessageAsync(PrivateMessage privateMessage)
    {
        await _forumPrivateMessageRepository.DeleteAsync(privateMessage);
    }

    /// <summary>
    /// Gets a private message
    /// </summary>
    /// <param name="privateMessageId">The private message identifier</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the private message
    /// </returns>
    public virtual async Task<PrivateMessage> GetPrivateMessageByIdAsync(int privateMessageId)
    {
        return await _forumPrivateMessageRepository.GetByIdAsync(privateMessageId, cache => default, useShortTermCache: true);
    }

    /// <summary>
    /// Gets private messages
    /// </summary>
    /// <param name="storeId">The store identifier; pass 0 to load all messages</param>
    /// <param name="fromCustomerId">The customer identifier who sent the message</param>
    /// <param name="toCustomerId">The customer identifier who should receive the message</param>
    /// <param name="isRead">A value indicating whether loaded messages are read. false - to load not read messages only, 1 to load read messages only, null to load all messages</param>
    /// <param name="isDeletedByAuthor">A value indicating whether loaded messages are deleted by author. false - messages are not deleted by author, null to load all messages</param>
    /// <param name="isDeletedByRecipient">A value indicating whether loaded messages are deleted by recipient. false - messages are not deleted by recipient, null to load all messages</param>
    /// <param name="keywords">Keywords</param>
    /// <param name="pageIndex">Page index</param>
    /// <param name="pageSize">Page size</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the private messages
    /// </returns>
    public virtual async Task<IPagedList<PrivateMessage>> GetAllPrivateMessagesAsync(int storeId, int fromCustomerId,
        int toCustomerId, bool? isRead, bool? isDeletedByAuthor, bool? isDeletedByRecipient,
        string keywords, int pageIndex = 0, int pageSize = int.MaxValue)
    {
        var privateMessages = await _forumPrivateMessageRepository.GetAllPagedAsync(query =>
        {
            if (storeId > 0)
                query = query.Where(pm => storeId == pm.StoreId);
            if (fromCustomerId > 0)
                query = query.Where(pm => fromCustomerId == pm.FromCustomerId);
            if (toCustomerId > 0)
                query = query.Where(pm => toCustomerId == pm.ToCustomerId);
            if (isRead.HasValue)
                query = query.Where(pm => isRead.Value == pm.IsRead);
            if (isDeletedByAuthor.HasValue)
                query = query.Where(pm => isDeletedByAuthor.Value == pm.IsDeletedByAuthor);
            if (isDeletedByRecipient.HasValue)
                query = query.Where(pm => isDeletedByRecipient.Value == pm.IsDeletedByRecipient);
            if (!string.IsNullOrEmpty(keywords))
            {
                query = query.Where(pm => pm.Subject.Contains(keywords));
                query = query.Where(pm => pm.Text.Contains(keywords));
            }

            query = query.OrderByDescending(pm => pm.CreatedOnUtc);

            return query;
        }, pageIndex, pageSize);

        return privateMessages;
    }

    /// <summary>
    /// Inserts a private message
    /// </summary>
    /// <param name="privateMessage">Private message</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public virtual async Task InsertPrivateMessageAsync(PrivateMessage privateMessage)
    {
        await _forumPrivateMessageRepository.InsertAsync(privateMessage);

        var customerTo = await _customerService.GetCustomerByIdAsync(privateMessage.ToCustomerId)
                         ?? throw new NopException("Recipient could not be loaded");

        //UI notification
        await _genericAttributeService.SaveAttributeAsync(customerTo, NopCustomerDefaults.NotifiedAboutNewPrivateMessagesAttribute, false, privateMessage.StoreId);

        //Email notification
        if (_forumSettings.NotifyAboutPrivateMessages)
            await _workflowMessageService.SendPrivateMessageNotificationAsync(privateMessage, (await _workContext.GetWorkingLanguageAsync()).Id);
    }

    /// <summary>
    /// Updates the private message
    /// </summary>
    /// <param name="privateMessage">Private message</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public virtual async Task UpdatePrivateMessageAsync(PrivateMessage privateMessage)
    {
        ArgumentNullException.ThrowIfNull(privateMessage);

        if (privateMessage.IsDeletedByAuthor && privateMessage.IsDeletedByRecipient)
            await _forumPrivateMessageRepository.DeleteAsync(privateMessage);
        else
            await _forumPrivateMessageRepository.UpdateAsync(privateMessage);
    }

    /// <summary>
    /// Deletes a forum subscription
    /// </summary>
    /// <param name="forumSubscription">Forum subscription</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public virtual async Task DeleteSubscriptionAsync(ForumSubscription forumSubscription)
    {
        await _forumSubscriptionRepository.DeleteAsync(forumSubscription);
    }

    /// <summary>
    /// Gets a forum subscription
    /// </summary>
    /// <param name="forumSubscriptionId">The forum subscription identifier</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the forum subscription
    /// </returns>
    public virtual async Task<ForumSubscription> GetSubscriptionByIdAsync(int forumSubscriptionId)
    {
        return await _forumSubscriptionRepository.GetByIdAsync(forumSubscriptionId, cache => default, useShortTermCache: true);
    }

    /// <summary>
    /// Gets forum subscriptions
    /// </summary>
    /// <param name="customerId">The customer identifier</param>
    /// <param name="forumId">The forum identifier</param>
    /// <param name="topicId">The topic identifier</param>
    /// <param name="pageIndex">Page index</param>
    /// <param name="pageSize">Page size</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the forum subscriptions
    /// </returns>
    public virtual async Task<IPagedList<ForumSubscription>> GetAllSubscriptionsAsync(int customerId = 0, int forumId = 0,
        int topicId = 0, int pageIndex = 0, int pageSize = int.MaxValue)
    {
        var forumSubscriptions = await _forumSubscriptionRepository.GetAllPagedAsync(query =>
        {
            var fsQuery = from fs in query
                join c in _customerRepository.Table on fs.CustomerId equals c.Id
                where
                    (customerId == 0 || fs.CustomerId == customerId) &&
                    (forumId == 0 || fs.ForumId == forumId) &&
                    (topicId == 0 || fs.TopicId == topicId) &&
                    c.Active &&
                    !c.Deleted
                select fs.SubscriptionGuid;

            var rez = from fs in query
                where fsQuery.Contains(fs.SubscriptionGuid)
                orderby fs.CreatedOnUtc descending, fs.SubscriptionGuid descending
                select fs;

            return rez;
        }, pageIndex, pageSize);

        return forumSubscriptions;
    }

    /// <summary>
    /// Inserts a forum subscription
    /// </summary>
    /// <param name="forumSubscription">Forum subscription</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public virtual async Task InsertSubscriptionAsync(ForumSubscription forumSubscription)
    {
        await _forumSubscriptionRepository.InsertAsync(forumSubscription);
    }

    /// <summary>
    /// Check whether customer is allowed to create new topics
    /// </summary>
    /// <param name="customer">Customer</param>
    /// <param name="forum">Forum</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains true if allowed, otherwise false
    /// </returns>
    public virtual async Task<bool> IsCustomerAllowedToCreateTopicAsync(Customer customer, Forum forum)
    {
        if (forum == null)
            return false;

        if (customer == null)
            return false;

        if (await _customerService.IsGuestAsync(customer) && !_forumSettings.AllowGuestsToCreateTopics)
            return false;

        return true;
    }

    /// <summary>
    /// Check whether customer is allowed to edit topic
    /// </summary>
    /// <param name="customer">Customer</param>
    /// <param name="topic">Topic</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains true if allowed, otherwise false
    /// </returns>
    public virtual async Task<bool> IsCustomerAllowedToEditTopicAsync(Customer customer, ForumTopic topic)
    {
        if (topic == null)
            return false;

        if (customer == null)
            return false;

        if (await _customerService.IsGuestAsync(customer))
            return false;

        if (await _customerService.IsForumModeratorAsync(customer))
            return true;

        if (!_forumSettings.AllowCustomersToEditPosts)
            return false;

        var ownTopic = customer.Id == topic.CustomerId;

        return ownTopic;
    }

    /// <summary>
    /// Check whether customer is allowed to move topic
    /// </summary>
    /// <param name="customer">Customer</param>
    /// <param name="topic">Topic</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains true if allowed, otherwise false
    /// </returns>
    public virtual async Task<bool> IsCustomerAllowedToMoveTopicAsync(Customer customer, ForumTopic topic)
    {
        if (topic == null)
            return false;

        if (customer == null)
            return false;

        if (await _customerService.IsGuestAsync(customer))
            return false;

        return await _customerService.IsForumModeratorAsync(customer);
    }

    /// <summary>
    /// Check whether customer is allowed to delete topic
    /// </summary>
    /// <param name="customer">Customer</param>
    /// <param name="topic">Topic</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains true if allowed, otherwise false
    /// </returns>
    public virtual async Task<bool> IsCustomerAllowedToDeleteTopicAsync(Customer customer, ForumTopic topic)
    {
        if (topic == null)
            return false;

        if (customer == null)
            return false;

        if (await _customerService.IsGuestAsync(customer))
            return false;

        if (await _customerService.IsForumModeratorAsync(customer))
            return true;

        if (!_forumSettings.AllowCustomersToDeletePosts)
            return false;

        var ownTopic = customer.Id == topic.CustomerId;

        return ownTopic;
    }

    /// <summary>
    /// Check whether customer is allowed to create new post
    /// </summary>
    /// <param name="customer">Customer</param>
    /// <param name="topic">Topic</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains true if allowed, otherwise false
    /// </returns>
    public virtual async Task<bool> IsCustomerAllowedToCreatePostAsync(Customer customer, ForumTopic topic)
    {
        if (topic == null)
            return false;

        if (customer == null)
            return false;

        if (await _customerService.IsGuestAsync(customer) && !_forumSettings.AllowGuestsToCreatePosts)
            return false;

        return true;
    }

    /// <summary>
    /// Check whether customer is allowed to edit post
    /// </summary>
    /// <param name="customer">Customer</param>
    /// <param name="post">Topic</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains true if allowed, otherwise false
    /// </returns>
    public virtual async Task<bool> IsCustomerAllowedToEditPostAsync(Customer customer, ForumPost post)
    {
        if (post == null)
            return false;

        if (customer == null)
            return false;

        if (await _customerService.IsGuestAsync(customer))
            return false;

        if (await _customerService.IsForumModeratorAsync(customer))
            return true;

        if (!_forumSettings.AllowCustomersToEditPosts)
            return false;

        var ownPost = customer.Id == post.CustomerId;

        return ownPost;
    }

    /// <summary>
    /// Check whether customer is allowed to delete post
    /// </summary>
    /// <param name="customer">Customer</param>
    /// <param name="post">Topic</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains true if allowed, otherwise false
    /// </returns>
    public virtual async Task<bool> IsCustomerAllowedToDeletePostAsync(Customer customer, ForumPost post)
    {
        if (post == null)
            return false;

        if (customer == null)
            return false;

        if (await _customerService.IsGuestAsync(customer))
            return false;

        if (await _customerService.IsForumModeratorAsync(customer))
            return true;

        if (!_forumSettings.AllowCustomersToDeletePosts)
            return false;

        var ownPost = customer.Id == post.CustomerId;

        return ownPost;
    }

    /// <summary>
    /// Check whether customer is allowed to set topic priority
    /// </summary>
    /// <param name="customer">Customer</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains true if allowed, otherwise false
    /// </returns>
    public virtual async Task<bool> IsCustomerAllowedToSetTopicPriorityAsync(Customer customer)
    {
        if (customer == null)
            return false;

        if (await _customerService.IsGuestAsync(customer))
            return false;

        return await _customerService.IsForumModeratorAsync(customer);
    }

    /// <summary>
    /// Check whether customer is allowed to watch topics
    /// </summary>
    /// <param name="customer">Customer</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains true if allowed, otherwise false
    /// </returns>
    public virtual async Task<bool> IsCustomerAllowedToSubscribeAsync(Customer customer)
    {
        if (customer == null)
            return false;

        if (customer.IsSearchEngineAccount() || await _customerService.IsGuestAsync(customer))
            return false;

        return true;
    }

    /// <summary>
    /// Calculates topic page index by post identifier
    /// </summary>
    /// <param name="forumTopicId">Forum topic identifier</param>
    /// <param name="pageSize">Page size</param>
    /// <param name="postId">Post identifier</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the page index
    /// </returns>
    public virtual async Task<int> CalculateTopicPageIndexAsync(int forumTopicId, int pageSize, int postId)
    {
        var pageIndex = 0;
        var forumPosts = await GetAllPostsAsync(forumTopicId, ascSort: true);

        for (var i = 0; i < forumPosts.TotalCount; i++)
        {
            if (forumPosts[i].Id != postId)
                continue;

            if (pageSize > 0)
                pageIndex = i / pageSize;
        }

        return pageIndex;
    }

    /// <summary>
    /// Get a post vote 
    /// </summary>
    /// <param name="postId">Post identifier</param>
    /// <param name="customer">Customer</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the post vote
    /// </returns>
    public virtual async Task<ForumPostVote> GetPostVoteAsync(int postId, Customer customer)
    {
        if (customer == null)
            return null;

        return await _forumPostVoteRepository.Table
            .FirstOrDefaultAsync(pv => pv.ForumPostId == postId && pv.CustomerId == customer.Id);
    }

    /// <summary>
    /// Get post vote made since the parameter date
    /// </summary>
    /// <param name="customer">Customer</param>
    /// <param name="createdFromUtc">Date</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the post votes count
    /// </returns>
    public virtual async Task<int> GetNumberOfPostVotesAsync(Customer customer, DateTime createdFromUtc)
    {
        if (customer == null)
            return 0;

        return await _forumPostVoteRepository.Table
            .CountAsync(pv => pv.CustomerId == customer.Id && pv.CreatedOnUtc > createdFromUtc);
    }

    /// <summary>
    /// Insert a post vote
    /// </summary>
    /// <param name="postVote">Post vote</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public virtual async Task InsertPostVoteAsync(ForumPostVote postVote)
    {
        await _forumPostVoteRepository.InsertAsync(postVote);

        //update post
        var post = await GetPostByIdAsync(postVote.ForumPostId);
        post.VoteCount = postVote.IsUp ? ++post.VoteCount : --post.VoteCount;

        await UpdatePostAsync(post);
    }

    /// <summary>
    /// Delete a post vote
    /// </summary>
    /// <param name="postVote">Post vote</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public virtual async Task DeletePostVoteAsync(ForumPostVote postVote)
    {
        ArgumentNullException.ThrowIfNull(postVote);

        await _forumPostVoteRepository.DeleteAsync(postVote);

        // update post
        var post = await GetPostByIdAsync(postVote.ForumPostId);
        post.VoteCount = postVote.IsUp ? --post.VoteCount : ++post.VoteCount;

        await UpdatePostAsync(post);
    }

    /// <summary>
    /// Formats the forum post text
    /// </summary>
    /// <param name="forumPost">Forum post</param>
    /// <returns>Formatted text</returns>
    public virtual string FormatPostText(ForumPost forumPost)
    {
        var text = forumPost.Text;

        if (string.IsNullOrEmpty(text))
            return string.Empty;

        switch (_forumSettings.ForumEditor)
        {
            case EditorType.SimpleTextBox:
            {
                text = _htmlFormatter.FormatText(text, false, true, false, false, false, false);
            }

                break;
            case EditorType.BBCodeEditor:
            {
                text = _htmlFormatter.FormatText(text, false, true, false, true, false, false);
            }

                break;
            default:
                break;
        }

        return text;
    }

    /// <summary>
    /// Strips the topic subject
    /// </summary>
    /// <param name="forumTopic">Forum topic</param>
    /// <returns>Formatted subject</returns>
    public virtual string StripTopicSubject(ForumTopic forumTopic)
    {
        var subject = forumTopic.Subject;
        if (string.IsNullOrEmpty(subject))
            return subject;

        var strippedTopicMaxLength = _forumSettings.StrippedTopicMaxLength;
        if (strippedTopicMaxLength <= 0)
            return subject;

        if (subject.Length <= strippedTopicMaxLength)
            return subject;

        var index = subject.IndexOf(" ", strippedTopicMaxLength, StringComparison.Ordinal);

        if (index <= 0)
            return subject;

        subject = subject[0..index];
        subject += "...";

        return subject;
    }

    /// <summary>
    /// Formats the forum signature text
    /// </summary>
    /// <param name="text">Text</param>
    /// <returns>Formatted text</returns>
    public virtual string FormatForumSignatureText(string text)
    {
        if (string.IsNullOrEmpty(text))
            return string.Empty;

        text = _htmlFormatter.FormatText(text, false, true, false, false, false, false);
        return text;
    }

    /// <summary>
    /// Formats the private message text
    /// </summary>
    /// <param name="pm">Private message</param>
    /// <returns>Formatted text</returns>
    public virtual string FormatPrivateMessageText(PrivateMessage pm)
    {
        var text = pm.Text;

        if (string.IsNullOrEmpty(text))
            return string.Empty;

        text = _htmlFormatter.FormatText(text, false, true, false, true, false, false);

        return text;
    }

    /// <summary>
    /// Get first post
    /// </summary>
    /// <param name="forumTopic">Forum topic</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the forum post
    /// </returns>
    public virtual async Task<ForumPost> GetFirstPostAsync(ForumTopic forumTopic)
    {
        ArgumentNullException.ThrowIfNull(forumTopic);

        var forumPosts = await GetAllPostsAsync(forumTopic.Id, 0, string.Empty, 0, 1);
        if (forumPosts.Any())
            return forumPosts[0];

        return null;
    }

    /// <summary>
    /// Gets ForumGroup SE (search engine) name
    /// </summary>
    /// <param name="forumGroup">ForumGroup</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the forumGroup SE (search engine) name
    /// </returns>
    public virtual async Task<string> GetForumGroupSeNameAsync(ForumGroup forumGroup)
    {
        ArgumentNullException.ThrowIfNull(forumGroup);

        var seName = await _urlRecordService.GetSeNameAsync(forumGroup.Name, _seoSettings.ConvertNonWesternChars, _seoSettings.AllowUnicodeCharsInUrls);

        return seName;
    }

    /// <summary>
    /// Gets Forum SE (search engine) name
    /// </summary>
    /// <param name="forum">Forum</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the forum SE (search engine) name
    /// </returns>
    public virtual async Task<string> GetForumSeNameAsync(Forum forum)
    {
        ArgumentNullException.ThrowIfNull(forum);

        var seName = await _urlRecordService.GetSeNameAsync(forum.Name, _seoSettings.ConvertNonWesternChars, _seoSettings.AllowUnicodeCharsInUrls);

        return seName;
    }

    /// <summary>
    /// Gets ForumTopic SE (search engine) name
    /// </summary>
    /// <param name="forumTopic">ForumTopic</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the forumTopic SE (search engine) name
    /// </returns>
    public virtual async Task<string> GetTopicSeNameAsync(ForumTopic forumTopic)
    {
        ArgumentNullException.ThrowIfNull(forumTopic);

        var seName = await _urlRecordService.GetSeNameAsync(forumTopic.Subject, _seoSettings.ConvertNonWesternChars, _seoSettings.AllowUnicodeCharsInUrls);

        // Trim SE name to avoid URLs that are too long
        var maxLength = NopSeoDefaults.ForumTopicLength;
        if (seName.Length > maxLength)
            seName = seName[0..maxLength];

        return seName;
    }

    #endregion
}