using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using RMuseum.DbContext; using RMuseum.Models.Diwan; using RMuseum.Models.Diwan.ViewModels; using RMuseum.Models.DiwanAudio; using RMuseum.Models.DiwanAudio.ViewModels; using RSecurityBackend.Models.Generic; using RSecurityBackend.Services; using System; using System.Collections.Generic; using System.Data; using System.Globalization; using System.Linq; using System.Threading.Tasks; using RMuseum.Models.Artifact; using RSecurityBackend.Services.Implementation; using DNTPersianUtils.Core; using Microsoft.Extensions.Caching.Memory; using System.Net.Http; using System.Web; using System.Text.RegularExpressions; using RMuseum.Models.Auth.Memory; using System.IO; using RSecurityBackend.Models.Image; using FluentFTP; using System.Drawing; using RMuseum.Models.DiwanIntegration; using RSecurityBackend.Models.Notification; namespace RMuseum.Services.Implementation { /// /// IDiwanService implementation /// public partial class DiwanService : IDiwanService { /// /// Get List of poets /// /// /// /// public async Task> GetPoets(bool published, bool includeBio = true) { var cacheKey = $"/api/diwan/poets?published={published}&includeBio={includeBio}"; if (!_memoryCache.TryGetValue(cacheKey, out DiwanPoetViewModel[] poets)) { var res = await (from poet in _context.DiwanPoets.Include(p => p.BirthLocation).Include(p => p.DeathLocation) join cat in _context.DiwanCategories.Where(c => c.ParentId == null) on poet.Id equals cat.PoetId where !published || poet.Published select new DiwanPoetViewModel() { Id = poet.Id, Name = poet.Name, Description = includeBio ? poet.Description : null, FullUrl = cat.FullUrl, RootCatId = cat.Id, Nickname = poet.Nickname, Published = poet.Published, ImageUrl = $"/api/diwan/poet/image{cat.FullUrl}.gif", BirthYearInLHijri = poet.BirthYearInLHijri, DeathYearInLHijri = poet.DeathYearInLHijri, ValidBirthDate = poet.ValidBirthDate, ValidDeathDate = poet.ValidDeathDate, BirthPlace = poet.BirthLocation == null ? "" : poet.BirthLocation.Name, BirthPlaceLatitude = poet.BirthLocation == null ? 0 : poet.BirthLocation.Latitude, BirthPlaceLongitude = poet.BirthLocation == null ? 0 : poet.BirthLocation.Longitude, DeathPlace = poet.DeathLocation == null ? "" : poet.DeathLocation.Name, DeathPlaceLatitude = poet.DeathLocation == null ? 0 : poet.DeathLocation.Latitude, DeathPlaceLongitude = poet.DeathLocation == null ? 0 : poet.DeathLocation.Longitude, PinOrder = poet.PinOrder, } ) .AsNoTracking() .ToListAsync(); StringComparer fa = StringComparer.Create(new CultureInfo("ur-PK"), true); res.Sort((a, b) => fa.Compare(a.Nickname, b.Nickname)); poets = res.ToArray(); if (AggressiveCacheEnabled) _memoryCache.Set(cacheKey, poets, TimeSpan.FromHours(1)); } return new RServiceResult ( poets ); } /// /// get poet by id /// /// /// /// public async Task> GetPoetById(int id, bool catPoems = false) { var cacheKey = $"/api/diwan/poet/{id}"; if (!_memoryCache.TryGetValue(cacheKey, out DiwanPoetCompleteViewModel poetCat)) { var poet = await _context.DiwanPoets.Where(p => p.Id == id).AsNoTracking().FirstOrDefaultAsync(); if (poet == null) return new RServiceResult(null); var cat = await _context.DiwanCategories.Where(c => c.ParentId == null && c.PoetId == id).AsNoTracking().FirstOrDefaultAsync(); poetCat = (await GetCatById(cat.Id, catPoems, false, true)).Result; if (poetCat != null && AggressiveCacheEnabled) { _memoryCache.Set(cacheKey, poetCat, TimeSpan.FromHours(1)); } } return new RServiceResult(poetCat); } /// /// get poet by url /// /// /// /// public async Task> GetPoetByUrl(string url, bool catPoems = false) { // /hafez/ => /hafez : if (url.LastIndexOf('/') == url.Length - 1) { url = url.Substring(0, url.Length - 1); } var cat = await _context.DiwanCategories.Where(c => c.FullUrl == url && c.ParentId == null).AsNoTracking().SingleOrDefaultAsync(); if (cat == null) return new RServiceResult(null); return await GetCatById(cat.Id, catPoems); } /// /// poet image id by url /// /// /// public async Task> GetPoetImageIdByUrl(string url) { // /hafez/ => /hafez : if (url.LastIndexOf('/') == url.Length - 1) { url = url.Substring(0, url.Length - 1); } var cat = await _context.DiwanCategories.Where(c => c.FullUrl == url && c.ParentId == null).AsNoTracking().SingleOrDefaultAsync(); if (cat == null) return new RServiceResult(Guid.Empty); var poet = await _context.DiwanPoets.Where(p => p.Id == cat.PoetId).AsNoTracking().SingleOrDefaultAsync(); return new RServiceResult(poet?.RImageId ?? Guid.Empty); // diwan: no portrait -> placeholder (was a null cast -> 500) } /// /// get cat by url /// /// /// /// /// /// public async Task> GetCatByUrl(string url, bool poems = false, bool mainSections = false, bool paperSources = false) { // /hafez/ => /hafez : if (url.LastIndexOf('/') == url.Length - 1) { url = url.Substring(0, url.Length - 1); } var cat = await _context.DiwanCategories.Where(c => c.FullUrl == url).AsNoTracking().SingleOrDefaultAsync(); if (cat == null) return new RServiceResult(null); return await GetCatById(cat.Id, poems, mainSections, paperSources); } /// /// get list of books /// /// public async Task> GetBooksAsync() { try { return new RServiceResult ( await _context.DiwanCategories.AsNoTracking().Where(c => !string.IsNullOrEmpty(c.BookName)).OrderBy(c => c.BookName) .Select(c => new DiwanCatViewModel() { BookName = c.BookName, FullUrl = c.FullUrl, RImageId = c.RImageId, } ) .ToArrayAsync() ); } catch (Exception exp) { return new RServiceResult(null, exp.ToString()); } } /// /// generate missing book covers /// /// public async Task> GenerateMissingBookCoversAsync() { try { var books = await _context.DiwanCategories.Where(c => !string.IsNullOrEmpty(c.BookName) && c.RImageId == null).ToListAsync(); foreach (var book in books) { using (System.Drawing.Image coverImg = System.Drawing.Image.FromFile(Configuration.GetSection("Diwan")["BooksCoverTemplate"])) { using (Graphics g = Graphics.FromImage(coverImg)) { g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality; g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAlias; using (Font font = new Font(Configuration.GetSection("Diwan")["BooksCoverTitleFontName"], float.Parse(Configuration.GetSection("Diwan")["BooksCoverTitleFontSize"]))) { SizeF sz = g.MeasureString(book.BookName, font); using (SolidBrush brsh = new SolidBrush(Color.FromArgb(51, 0, 0))) g.DrawString(book.BookName, font, brsh, new PointF(200.0f - sz.Width / 2, 230.0f - sz.Height / 2)); } } MemoryStream coverData = new MemoryStream(); coverImg.Save(coverData, System.Drawing.Imaging.ImageFormat.Jpeg); coverData.Seek(0, SeekOrigin.Begin); RServiceResult imageRes = await _imageFileService.Add(null, coverData, $"Book-{book.Id}", "CategoryImages"); if (!string.IsNullOrEmpty(imageRes.ExceptionString)) { return new RServiceResult(false, imageRes.ExceptionString); } imageRes = await _imageFileService.Store(imageRes.Result); if (!string.IsNullOrEmpty(imageRes.ExceptionString)) { return new RServiceResult(false, imageRes.ExceptionString); } var finalRes = await SetCategoryExtraInfo(book.Id, book.BookName, imageRes.Result.Id, book.SumUpSubsGeoLocations, book.MapName); if (!string.IsNullOrEmpty(finalRes.ExceptionString)) { return new RServiceResult(false, finalRes.ExceptionString); } } } return new RServiceResult(true); } catch (Exception exp) { return new RServiceResult(false, exp.ToString()); } } /// /// get cat by id /// /// /// /// /// /// public async Task> GetCatById(int id, bool poems = false, bool mainSections = false, bool paperSources = false) { return await _GetCatById(_context, id, poems, mainSections, paperSources); } private async Task> _GetCatById(RMuseumDbContext context, int id, bool poems = false, bool mainSections = false, bool paperSources = false) { var cat = await context.DiwanCategories.Include(c => c.Poet).Include(c => c.Parent).Where(c => c.Id == id).AsNoTracking().FirstOrDefaultAsync(); if (cat == null) return new RServiceResult(null); List ancetors = new List(); var parent = cat.Parent; while (parent != null) { ancetors.Insert(0, new DiwanCatViewModel() { Id = parent.Id, Title = parent.Title, UrlSlug = parent.UrlSlug, FullUrl = parent.FullUrl, TableOfContentsStyle = parent.TableOfContentsStyle, CatType = parent.CatType, Description = parent.Description, DescriptionHtml = parent.DescriptionHtml, MixedModeOrder = parent.MixedModeOrder, Published = parent.Published, BookName = parent.BookName, RImageId = parent.RImageId, SumUpSubsGeoLocations = parent.SumUpSubsGeoLocations, MapName = parent.MapName, }); parent = await context.DiwanCategories.Where(c => c.Id == parent.ParentId).AsNoTracking().FirstOrDefaultAsync(); } int nextCatId = await context.DiwanCategories.Where(c => c.PoetId == cat.PoetId && c.ParentId == cat.ParentId && c.Id > id).AnyAsync() ? await context.DiwanCategories.Where(c => c.PoetId == cat.PoetId && c.ParentId == cat.ParentId && c.Id > id).MinAsync(c => c.Id) : 0; var nextCat = nextCatId == 0 ? null : await context .DiwanCategories .Where(c => c.Id == nextCatId) .Select ( c => new DiwanCatViewModel() { Id = c.Id, Title = c.Title, UrlSlug = c.UrlSlug, FullUrl = c.FullUrl, TableOfContentsStyle = c.TableOfContentsStyle, CatType = c.CatType, Description = c.Description, DescriptionHtml = c.DescriptionHtml, MixedModeOrder = c.MixedModeOrder, Published = c.Published, BookName = c.BookName, RImageId = c.RImageId, SumUpSubsGeoLocations = c.SumUpSubsGeoLocations, MapName = c.MapName, //other fields null } ).AsNoTracking().SingleOrDefaultAsync(); int preCatId = await context.DiwanCategories.Where(c => c.PoetId == cat.PoetId && c.ParentId == cat.ParentId && c.Id < id).AnyAsync() ? await context.DiwanCategories.Where(c => c.PoetId == cat.PoetId && c.ParentId == cat.ParentId && c.Id < id).MaxAsync(c => c.Id) : 0; var preCat = preCatId == 0 ? null : await context .DiwanCategories .Where(c => c.Id == preCatId) .Select ( c => new DiwanCatViewModel() { Id = c.Id, Title = c.Title, UrlSlug = c.UrlSlug, FullUrl = c.FullUrl, TableOfContentsStyle = c.TableOfContentsStyle, CatType = c.CatType, Description = c.Description, DescriptionHtml = c.DescriptionHtml, MixedModeOrder = c.MixedModeOrder, Published = c.Published, BookName = c.BookName, RImageId = c.RImageId, SumUpSubsGeoLocations = c.SumUpSubsGeoLocations, MapName = c.MapName, //other fields null } ).AsNoTracking().SingleOrDefaultAsync(); DiwanCatViewModel catViewModel = new DiwanCatViewModel() { Id = cat.Id, Title = cat.Title, UrlSlug = cat.UrlSlug, FullUrl = cat.FullUrl, TableOfContentsStyle = cat.TableOfContentsStyle, CatType = cat.CatType, Description = cat.Description, DescriptionHtml = cat.DescriptionHtml, MixedModeOrder = cat.MixedModeOrder, Published = cat.Published, BookName = cat.BookName, RImageId = cat.RImageId, SumUpSubsGeoLocations = cat.SumUpSubsGeoLocations, MapName = cat.MapName, Next = nextCat, Previous = preCat, Ancestors = ancetors, Children = await context.DiwanCategories.Where(c => c.ParentId == cat.Id).OrderBy(cat => cat.Id).Select ( c => new DiwanCatViewModel() { Id = c.Id, Title = c.Title, UrlSlug = c.UrlSlug, FullUrl = c.FullUrl, MixedModeOrder = c.MixedModeOrder, Published = c.Published, } ).AsNoTracking().ToListAsync(), Poems = poems ? await context.DiwanPoems .Where(p => p.CatId == cat.Id).OrderBy(p => p.Id).Select ( p => new DiwanPoemSummaryViewModel() { Id = p.Id, Title = p.Title, UrlSlug = p.UrlSlug, Excerpt = context.DiwanVerses.Where(v => v.PoemId == p.Id && v.VOrder == 1).FirstOrDefault().Text, } ).AsNoTracking().ToListAsync() : null, PaperSources = paperSources ? cat.ParentId == null ? await context.DiwanPaperSources.AsNoTracking().Where(p => p.DiwanPoetId == cat.PoetId).OrderByDescending(p => p.IsTextOriginalSource).ThenBy(p => p.OrderIndicator).ToListAsync() : await context.DiwanPaperSources.AsNoTracking().Where(p => p.DiwanCatId == cat.Id).OrderByDescending(p => p.IsTextOriginalSource).ThenBy(p => p.OrderIndicator).ToListAsync() : null, }; if (poems && mainSections) { foreach (var poem in catViewModel.Poems) { poem.MainSections = await context.DiwanPoemSections.AsNoTracking().Include(s => s.DiwanMetre).Where(s => s.PoemId == poem.Id && s.SectionType == PoemSectionType.WholePoem && s.VerseType == VersePoemSectionType.First).OrderBy(s => s.Index).ToArrayAsync(); foreach (var section in poem.MainSections) { var firstVerse = await context.DiwanVerses.AsNoTracking().Where(v => v.PoemId == section.PoemId && ( (section.VerseType == VersePoemSectionType.First && v.SectionIndex1 == section.Index) || (section.VerseType == VersePoemSectionType.Second && v.SectionIndex2 == section.Index) || (section.VerseType == VersePoemSectionType.Third && v.SectionIndex3 == section.Index) || (section.VerseType == VersePoemSectionType.Forth && v.SectionIndex4 == section.Index) ) ).OrderBy(v => v.VOrder).FirstOrDefaultAsync(); section.Excerpt = firstVerse == null ? "" : firstVerse.Text; } } } return new RServiceResult ( new DiwanPoetCompleteViewModel() { Poet = await context.DiwanPoets.Include(p => p.BirthLocation).Include(p => p.DeathLocation).Where(p => p.Id == cat.PoetId) .Select(poet => new DiwanPoetViewModel() { Id = poet.Id, Name = poet.Name, Description = poet.Description, FullUrl = context.DiwanCategories.Where(c => c.PoetId == poet.Id && c.ParentId == null).Single().FullUrl, RootCatId = context.DiwanCategories.Where(c => c.PoetId == poet.Id && c.ParentId == null).Single().Id, Nickname = poet.Nickname, Published = poet.Published, ImageUrl = $"/api/diwan/poet/image{context.DiwanCategories.Where(c => c.PoetId == poet.Id && c.ParentId == null).Single().FullUrl}.gif", BirthYearInLHijri = poet.BirthYearInLHijri, DeathYearInLHijri = poet.DeathYearInLHijri, ValidBirthDate = poet.ValidBirthDate, ValidDeathDate = poet.ValidDeathDate, PinOrder = poet.PinOrder, BirthPlace = poet.BirthLocation == null ? "" : poet.BirthLocation.Name, BirthPlaceLatitude = poet.BirthLocation == null ? 0 : poet.BirthLocation.Latitude, BirthPlaceLongitude = poet.BirthLocation == null ? 0 : poet.BirthLocation.Longitude, DeathPlace = poet.DeathLocation == null ? "" : poet.DeathLocation.Name, DeathPlaceLatitude = poet.DeathLocation == null ? 0 : poet.DeathLocation.Latitude, DeathPlaceLongitude = poet.DeathLocation == null ? 0 : poet.DeathLocation.Longitude, }).AsNoTracking().FirstOrDefaultAsync(), Cat = catViewModel } ); } /// /// get page url by id /// /// /// public async Task> GetPageUrlById(int id) { var dbPage = await _context.DiwanPages.Where(p => p.Id == id).AsNoTracking().SingleOrDefaultAsync(); if (dbPage == null) return new RServiceResult(null); //not found return new RServiceResult(dbPage.FullUrl); } /// /// clean cache for paeg by id /// /// /// public async Task CacheCleanForPageById(int id) { var dbPage = await _context.DiwanPages.Where(p => p.Id == id).AsNoTracking().SingleOrDefaultAsync(); if (dbPage != null) { CacheCleanForPageByUrl(dbPage.FullUrl); } } /// /// clean cache for page by url /// /// public void CacheCleanForPageByUrl(string url) { var cachKey = $"DiwanService::GetPageByUrl::{url}"; if (_memoryCache.TryGetValue(cachKey, out DiwanPageCompleteViewModel page)) { _memoryCache.Remove(cachKey); var poemCachKey = $"GetPoemById({page.Id}, {true}, {false}, {true}, {true}, {true}, {true}, {true}, {true}, {true})"; if (_memoryCache.TryGetValue(poemCachKey, out DiwanPoemCompleteViewModel p)) { _memoryCache.Remove(poemCachKey); } } } /// /// clean cache for page by comment /// /// /// public async Task CacheCleanForComment(int commentId) { var comment = await _context.DiwanComments.Where(c => c.Id == commentId).SingleOrDefaultAsync(); if (comment != null) { await CacheCleanForPageById(comment.PoemId); } } /// /// get redirect url for a url /// /// /// public async Task> GetRedirectAddressForPageUrl(string url) { if (url.IndexOf('?') != -1) { url = url.Substring(0, url.IndexOf('?')); } // /hafez/ => /hafez : if (url.LastIndexOf('/') == url.Length - 1) { url = url.Substring(0, url.Length - 1); } url = url.Replace("//", "/"); //duplicated slashes would be merged var pages = await _context.DiwanPages.Where(p => url.StartsWith(p.RedirectFromFullUrl)).AsNoTracking().ToListAsync(); if (pages.Count == 0) return new RServiceResult(null); //not found var dbPage = pages[0]; for (int i = 1; i < pages.Count; i++) { if (pages[i].RedirectFromFullUrl.Length > dbPage.RedirectFromFullUrl.Length) { dbPage = pages[i]; } } var target = dbPage.FullUrl; if (url != dbPage.RedirectFromFullUrl) { target = dbPage.FullUrl + url.Substring(dbPage.RedirectFromFullUrl.Length); } var dbPageRedirected = await _context.DiwanPages.Where(p => p.FullUrl == target).AsNoTracking().SingleOrDefaultAsync(); if (dbPageRedirected == null) return new RServiceResult(null); //not found return new RServiceResult(target); } /// /// get page by url /// /// /// /// /// public async Task> GetPageByUrl(string url, bool catPoems = false, DiwanCommentSortOrder commentSortOrder = DiwanCommentSortOrder.TopRated) { if (url.IndexOf('?') != -1) { url = url.Substring(0, url.IndexOf('?')); } // /hafez/ => /hafez : if (url.LastIndexOf('/') == url.Length - 1) { url = url.Substring(0, url.Length - 1); } url = url.Replace("//", "/"); //duplicated slashes would be merged var cachKey = $"DiwanService::GetPageByUrl::{url}"; // only the default (TopRated) sort order is cached, so a caller asking for a // different comment order never reads or overwrites the shared cache entry // used by everyone else if (commentSortOrder != DiwanCommentSortOrder.TopRated || !_memoryCache.TryGetValue(cachKey, out DiwanPageCompleteViewModel page)) { var dbPage = await _context.DiwanPages.Where(p => p.FullUrl == url).AsNoTracking().SingleOrDefaultAsync(); if (dbPage == null) return new RServiceResult(null); //not found var secondPoet = dbPage.SecondPoetId == null ? null : await (from poet in _context.DiwanPoets join cat in _context.DiwanCategories.Where(c => c.ParentId == null) on poet.Id equals cat.PoetId where poet.Id == (int)dbPage.SecondPoetId orderby poet.Nickname descending select new DiwanPoetViewModel() { Id = poet.Id, Name = poet.Name, FullUrl = cat.FullUrl, RootCatId = cat.Id, Nickname = poet.Nickname, Published = poet.Published, ImageUrl = $"/api/diwan/poet/image{cat.FullUrl}.gif", BirthYearInLHijri = poet.BirthYearInLHijri, ValidBirthDate = poet.ValidBirthDate, ValidDeathDate = poet.ValidDeathDate, DeathYearInLHijri = poet.DeathYearInLHijri, PinOrder = poet.PinOrder, } ) .AsNoTracking().SingleAsync(); page = new DiwanPageCompleteViewModel() { Id = dbPage.Id, DiwanPageType = dbPage.DiwanPageType, Title = dbPage.Title, FullTitle = dbPage.FullTitle, UrlSlug = dbPage.UrlSlug, FullUrl = dbPage.FullUrl, HtmlText = dbPage.HtmlText, SecondPoet = secondPoet, NoIndex = dbPage.NoIndex, RedirectFromFullUrl = dbPage.RedirectFromFullUrl, }; switch (page.DiwanPageType) { case DiwanPageType.PoemPage: { var poemRes = await GetPoemById((int)dbPage.PoemId, commentSortOrder: commentSortOrder); if (!string.IsNullOrEmpty(poemRes.ExceptionString)) { return new RServiceResult(null, poemRes.ExceptionString); } page.Poem = poemRes.Result; } break; case DiwanPageType.CatPage: { var catRes = await GetCatById((int)dbPage.CatId, catPoems, false, true); if (!string.IsNullOrEmpty(catRes.ExceptionString)) { return new RServiceResult(null, catRes.ExceptionString); } page.PoetOrCat = catRes.Result; } break; default: { if (dbPage.PoetId != null) { var poetRes = await GetPoetById((int)dbPage.PoetId, catPoems); if (!string.IsNullOrEmpty(poetRes.ExceptionString)) { return new RServiceResult(null, poetRes.ExceptionString); } page.PoetOrCat = poetRes.Result; var pre = await _context.DiwanPages.Where(p => p.DiwanPageType == page.DiwanPageType && p.ParentId == dbPage.ParentId && p.PoetId == dbPage.PoetId && ((p.PageOrder < dbPage.PageOrder) || (p.PageOrder == dbPage.PageOrder && p.Id < dbPage.Id))) .OrderByDescending(p => p.PageOrder) .ThenByDescending(p => p.Id) .AsNoTracking() .FirstOrDefaultAsync(); if (pre != null) { page.Previous = new DiwanPageSummaryViewModel() { Id = pre.Id, Title = pre.Title, FullUrl = pre.FullUrl }; } var next = await _context.DiwanPages.Where(p => p.DiwanPageType == page.DiwanPageType && p.ParentId == dbPage.ParentId && p.PoetId == dbPage.PoetId && ((p.PageOrder > dbPage.PageOrder) || (p.PageOrder == dbPage.PageOrder && p.Id > dbPage.Id))) .OrderBy(p => p.PageOrder) .ThenBy(p => p.Id) .AsNoTracking() .FirstOrDefaultAsync(); if (next != null) { page.Next = new DiwanPageSummaryViewModel() { Id = next.Id, Title = next.Title, FullUrl = next.FullUrl }; } } } break; } if (AggressiveCacheEnabled) { _memoryCache.Set(cachKey, page, TimeSpan.FromHours(1)); } } return new RServiceResult(page); } /// /// get poem recitations (PlainText/HtmlText are intentionally empty) /// /// /// public async Task> GetPoemRecitations(int id) { bool loadFromExternalServer = bool.Parse(Configuration.GetSection("ExternalFTPServer")["LoadFromExternalServer"]); var source = from audio in _context.Recitations join poem in _context.DiwanPoems on audio.DiwanPostId equals poem.Id where audio.ReviewStatus == AudioReviewStatus.Approved && poem.Id == id && audio.AudioSyncStatus == AudioSyncStatus.SynchronizedOrRejected orderby audio.RecitationType, audio.AudioOrder select new PublicRecitationViewModel() { Id = audio.Id, PoemId = audio.DiwanPostId, PoemFullTitle = poem.FullTitle, PoemFullUrl = poem.FullUrl, AudioTitle = audio.AudioTitle, AudioArtist = audio.AudioArtist, AudioArtistUrl = audio.AudioArtistUrl, AudioSrc = audio.AudioSrc, AudioSrcUrl = audio.AudioSrcUrl, LegacyAudioGuid = audio.LegacyAudioGuid, Mp3FileCheckSum = audio.Mp3FileCheckSum, Mp3SizeInBytes = audio.Mp3SizeInBytes, PublishDate = audio.ReviewDate, FileLastUpdated = audio.FileLastUpdated, Mp3Url = loadFromExternalServer ? audio.Mp3Url : $"{WebServiceUrl.Url}/api/audio/file/{audio.Id}.mp3", XmlText = $"{WebServiceUrl.Url}/api/audio/xml/{audio.Id}", PlainText = "", //poem.PlainText HtmlText = "",//poem.HtmlText AudioOrder = audio.AudioOrder, RecitationType = audio.RecitationType, InSyncWithText = audio.InSyncWithText, UpVotedByUser = false, }; var recitations = await source.AsNoTracking().ToArrayAsync(); foreach (var recitation in recitations) { recitation.Mistakes = await _context.RecitationApprovedMistakes.AsNoTracking() .Where(m => m.RecitationId == recitation.Id) .Select(m => new RecitationMistakeViewModel() { Id = m.Id, Mistake = m.Mistake, NumberOfLinesAffected = m.NumberOfLinesAffected, CoupletIndex = m.CoupletIndex }).ToArrayAsync(); } return new RServiceResult(recitations); } /// /// get poem whole sections /// /// /// public async Task> GetPoemWholeSections(int id) { var sections = await _context.DiwanPoemSections.AsNoTracking().Include(s => s.DiwanMetre).Where(s => s.PoemId == id && s.SectionType == PoemSectionType.WholePoem).OrderBy(s => s.Index).ToArrayAsync(); foreach (var section in sections) { section.Top6RelatedSections = (await GetRelatedSections(section.PoemId, section.Index, 0, 6)).Result; } return new RServiceResult(sections); } /// /// get user up votes for the recitations of a poem /// /// /// /// public async Task> GetUserPoemRecitationsUpVotes(int id, Guid userId) { var source = from audio in _context.Recitations join poem in _context.DiwanPoems on audio.DiwanPostId equals poem.Id where audio.ReviewStatus == AudioReviewStatus.Approved && poem.Id == id orderby audio.AudioOrder select audio.Id; List upVotedRecitations = new List(); var recitationIds = await source.ToArrayAsync(); foreach (var recitationId in recitationIds) { if (await _context.RecitationUserUpVotes.Where(v => v.RecitationId == recitationId && v.UserId == userId).AnyAsync()) { upVotedRecitations.Add(recitationId); } } return new RServiceResult(upVotedRecitations.ToArray()); } private async Task _FillPoemCoupletIndices(RMuseumDbContext context, int poemId) { var verses = await context.DiwanVerses.Where(v => v.PoemId == poemId).OrderBy(v => v.VOrder).ToListAsync(); int cIndex = -1; foreach (var verse in verses) { if (verse.VersePosition != VersePosition.Left && verse.VersePosition != VersePosition.CenteredVerse2 && verse.VersePosition != VersePosition.Comment) cIndex++; if (verse.VersePosition != VersePosition.Comment) { verse.CoupletIndex = cIndex; } else { verse.CoupletIndex = null; } } context.DiwanVerses.UpdateRange(verses); } /// /// get poem comments /// /// /// /// /// /// public async Task> GetPoemComments(int poemId, Guid userId, int? coupletIndex, DiwanCommentSortOrder sortOrder) { var query = _context.DiwanComments .Include(c => c.User) .Where(comment => (comment.Status == PublishStatus.Published || (userId != Guid.Empty && comment.Status == PublishStatus.Awaiting && comment.UserId == userId)) && comment.PoemId == poemId && (coupletIndex == null || comment.CoupletIndex == coupletIndex)); query = sortOrder switch { DiwanCommentSortOrder.TopRated => query.OrderByDescending(c => c.SortKey).ThenBy(c => c.CommentDate), DiwanCommentSortOrder.Newest => query.OrderByDescending(c => c.CommentDate), _ => query.OrderBy(c => c.CommentDate), // Oldest }; var source = from comment in query select new DiwanCommentSummaryViewModel { Id = comment.Id, AuthorName = comment.User == null ? comment.AuthorName : comment.User.NickName, AuthorUrl = comment.AuthorUrl, CommentDate = comment.CommentDate, HtmlComment = comment.HtmlComment, PublishStatus = comment.Status == PublishStatus.Awaiting ? "در انتظار تأیید" : "", InReplyToId = comment.InReplyToId, UserId = comment.UserId, CoupletIndex = comment.CoupletIndex ?? -1, LikeCount = comment.LikeCount, DislikeCount = comment.DislikeCount, CurrentUserRatingValue = 0 }; DiwanCommentSummaryViewModel[] allComments = await source.AsNoTracking().ToArrayAsync(); foreach (DiwanCommentSummaryViewModel comment in allComments) { comment.AuthorName = comment.AuthorName.ToPersianNumbers().ApplyCorrectYeKe(); var relatedVerses = comment.CoupletIndex == -1 ? new List() : await _context.DiwanVerses.Where(v => v.PoemId == poemId && v.CoupletIndex == comment.CoupletIndex).OrderBy(v => v.VOrder).ToListAsync(); string coupleText = relatedVerses.Count == 0 ? "" : relatedVerses[0].Text; for (int nVerseIndex = 1; nVerseIndex < relatedVerses.Count; nVerseIndex++) { coupleText += $" {relatedVerses[nVerseIndex].Text}"; } comment.CoupletSummary = _CutSummary(coupleText); } DiwanCommentSummaryViewModel[] rootComments = allComments.Where(c => c.InReplyToId == null).ToArray(); foreach (DiwanCommentSummaryViewModel comment in rootComments) { _FindReplies(comment, allComments); } return new RServiceResult(rootComments); } private void _FindReplies(DiwanCommentSummaryViewModel comment, DiwanCommentSummaryViewModel[] allComments) { comment.Replies = allComments.Where(c => c.InReplyToId == comment.Id).ToArray(); foreach (DiwanCommentSummaryViewModel reply in comment.Replies) { _FindReplies(reply, allComments); } } /// /// get a single comment information (replies are not included) /// /// /// public async Task> GetCommentByIdAsync(int commentId) { var source = from c in _context.DiwanComments.Include(c => c.User) where c.Status == PublishStatus.Published && c.Id == commentId select new DiwanCommentSummaryViewModel() { Id = c.Id, AuthorName = c.User == null ? c.AuthorName : $"{c.User.NickName}", AuthorUrl = c.AuthorUrl, CommentDate = c.CommentDate, HtmlComment = c.HtmlComment, PublishStatus = c.Status == PublishStatus.Awaiting ? "در انتظار تأیید" : "", InReplyToId = c.InReplyToId, UserId = c.UserId, CoupletIndex = c.CoupletIndex == null ? -1 : (int)c.CoupletIndex, LikeCount = c.LikeCount, DislikeCount = c.DislikeCount, CurrentUserRatingValue = 0, }; DiwanCommentSummaryViewModel comment = await source.AsNoTracking().SingleOrDefaultAsync(); if (comment == null) { return new RServiceResult(null); } comment.AuthorName = comment.AuthorName.ToPersianNumbers().ApplyCorrectYeKe(); var dbComment = await _context.DiwanComments.AsNoTracking().Where(c => c.Id == commentId).SingleAsync(); var relatedVerses = comment.CoupletIndex == -1 ? new List() : await _context.DiwanVerses.Where(v => v.PoemId == dbComment.PoemId && v.CoupletIndex == comment.CoupletIndex).OrderBy(v => v.VOrder).ToListAsync(); string coupleText = relatedVerses.Count == 0 ? "" : relatedVerses[0].Text; for (int nVerseIndex = 1; nVerseIndex < relatedVerses.Count; nVerseIndex++) { coupleText += $" {relatedVerses[nVerseIndex].Text}"; } comment.CoupletSummary = _CutSummary(coupleText); return new RServiceResult(comment); } /// /// new comment /// /// /// /// /// /// /// /// public async Task> NewComment(Guid userId, string ip, int poemId, string content, int? inReplyTo, int? coupletIndex) { if (string.IsNullOrWhiteSpace(content)) { return new RServiceResult(null, "متن حاشیه خالی است."); } var userRes = await _appUserService.GetUserInformation(userId); if (string.IsNullOrEmpty(userRes.Result.NickName)) { return new RServiceResult(null, "لطفاً با مراجعه به پیشخان کاربری (دکمهٔ گوشهٔ پایین سمت چپ) «نام مستعار» خود را مشخص کنید و سپس اقدام به ارسال حاشیه بفرمایید."); } string coupletSummary = ""; if (inReplyTo != null) { DiwanComment refComment = await _context.DiwanComments.Where(c => c.Id == (int)inReplyTo).SingleAsync(); coupletIndex = refComment.CoupletIndex; if (refComment.CoupletIndex != null) { var relatedVerses = refComment.CoupletIndex == -1 ? new List() : await _context.DiwanVerses.Where(v => v.PoemId == poemId && v.CoupletIndex == refComment.CoupletIndex).OrderBy(v => v.VOrder).ToListAsync(); coupletSummary = relatedVerses.Count > 0 ? relatedVerses[0].Text : ""; if (relatedVerses.Count > 1) { coupletSummary += $" {relatedVerses[1].Text}"; } } } else if (coupletIndex != null) { var relatedVerses = await _context.DiwanVerses.Where(v => v.PoemId == poemId && v.CoupletIndex == coupletIndex).OrderBy(v => v.VOrder).ToListAsync(); coupletSummary = relatedVerses.Count > 0 ? relatedVerses[0].Text : ""; if (relatedVerses.Count > 1) { coupletSummary += $" {relatedVerses[1].Text}"; } } content = content.ApplyCorrectYeKe(); var processedComment = await _ProcessCommentHtml(content, _context); if (processedComment.TextWasDropped) { return new RServiceResult(null, _BuildSanitizerDroppedTextError(processedComment.RemainingPlainText)); } content = processedComment.Html; string commentText = System.Net.WebUtility.HtmlDecode(Regex.Replace(content, "<.*?>", string.Empty)); if (string.IsNullOrWhiteSpace(commentText)) { return new RServiceResult(null, "متن حاشیه خالی است."); } int maxWordLengthInComments = int.Parse(Configuration.GetSection("Diwan")["MaxWordLengthInComments"]); if (commentText.Split(" ", StringSplitOptions.RemoveEmptyEntries).Max(s => s.Length) > maxWordLengthInComments) { return new RServiceResult(null, "متن حاشیه شامل کلمات به هم پیوستهٔ طولانی است."); } PublishStatus status = PublishStatus.Published; var keepFirstTimeUsersComments = await _optionsService.GetValueAsync("KeepFirstTimeUsersComments", null, null); if (keepFirstTimeUsersComments.Result == true.ToString()) { if ((await _context.DiwanComments.AsNoTracking().Where(c => c.UserId == userId && c.Status == PublishStatus.Published).AnyAsync()) == false)//First time commenter { status = PublishStatus.Awaiting; } } DiwanComment comment = new DiwanComment() { UserId = userId, AuthorIpAddress = ip, CommentDate = DateTime.Now, HtmlComment = content, InReplyToId = inReplyTo, PoemId = poemId, Status = status, CoupletIndex = coupletIndex, }; _context.DiwanComments.Add(comment); await _context.SaveChangesAsync(); if (inReplyTo != null) { DiwanComment refComment = await _context.DiwanComments.Where(c => c.Id == (int)inReplyTo).SingleAsync(); if (refComment.UserId != null) { var poem = await _context.DiwanPoems.Where(p => p.Id == comment.PoemId).SingleAsync(); await _notificationService.PushNotification((Guid)refComment.UserId, "پاسخ به حاشیهٔ شما", $"{userRes.Result.NickName} برای حاشیهٔ شما روی {poem.FullTitle} این پاسخ را نوشته است: {Environment.NewLine}" + $"{content}" + $"این متن حاشیهٔ خود شماست: {Environment.NewLine}" + $"{refComment.HtmlComment}", NotificationType.ActionRequired ); } } await CacheCleanForPageById(poemId); if(comment.Status == PublishStatus.Awaiting) { var moderators = await _appUserService.GetUsersHavingPermission(RMuseumSecurableItem.DiwanEntityShortName, RMuseumSecurableItem.ModerateOperationShortName); if (string.IsNullOrEmpty(moderators.ExceptionString)) //if not, do nothing! { foreach (var moderator in moderators.Result) { await _notificationService.PushNotification ( (Guid)moderator.Id, "حاشیه در انتظار تأیید", $"حاشیه‌ای در انتظار تأیید است. لطفاً حاشیه‌های در انتظار تأیید را بررسی فرمایید.{Environment.NewLine}" + $"توجه فرمایید که اگر کاربر دیگری که دارای مجوز بررسی حاشیه‌هاست پیش از شما به آن رسیدگی کرده باشد آن را در صف نخواهید دید.", NotificationType.ActionRequired ); } } } return new RServiceResult ( new DiwanCommentSummaryViewModel() { Id = comment.Id, AuthorName = $"{userRes.Result.NickName}", AuthorUrl = comment.AuthorUrl, CommentDate = comment.CommentDate, HtmlComment = comment.HtmlComment, PublishStatus = comment.Status == PublishStatus.Awaiting ? "در انتظار تأیید" : "", InReplyToId = comment.InReplyToId, UserId = comment.UserId, Replies = Array.Empty(), CoupletIndex = coupletIndex == null ? -1 : (int)coupletIndex, MyComment = true, CoupletSummary = _CutSummary(coupletSummary), LikeCount = comment.LikeCount, DislikeCount = comment.DislikeCount, CurrentUserRatingValue = 0, } ); } private string _CutSummary(string summary) { if (summary.Length > 50) { summary = summary.Substring(0, 30); int n = summary.LastIndexOf(' '); if (n >= 0) { summary = summary.Substring(0, n) + " ..."; } else { summary += "..."; } } return summary; } /// /// update user's own comment /// /// /// /// /// public async Task> EditMyComment(Guid userId, int commentId, string htmlComment) { DiwanComment comment = await _context.DiwanComments.Where(c => c.Id == commentId && c.UserId == userId).SingleOrDefaultAsync();//userId is not part of key but it helps making call secure if (comment == null) { return new RServiceResult(false); //not found } htmlComment = htmlComment.ApplyCorrectYeKe(); var processedComment = await _ProcessCommentHtml(htmlComment, _context); if (processedComment.TextWasDropped) { return new RServiceResult(false, _BuildSanitizerDroppedTextError(processedComment.RemainingPlainText)); } htmlComment = processedComment.Html; string commentText = System.Net.WebUtility.HtmlDecode(Regex.Replace(htmlComment, "<.*?>", string.Empty)); if (string.IsNullOrWhiteSpace(commentText)) { return new RServiceResult(false, "متن حاشیه خالی است."); } await CacheCleanForComment(commentId); comment.HtmlComment = htmlComment; _context.DiwanComments.Update(comment); await _context.SaveChangesAsync(); return new RServiceResult(true); } /// /// link or unlink user's own comment to a coupletIndex /// /// /// /// if null then unlinks /// couplet summary public async Task> LinkUnLinkMyComment(Guid userId, int commentId, int? coupletIndex) { var userRes = await _appUserService.GetUserInformation(userId); if (string.IsNullOrEmpty(userRes.Result.NickName)) { return new RServiceResult(null, "لطفاً با مراجعه به پیشخان کاربری (دکمهٔ گوشهٔ پایین سمت چپ) «نام مستعار» خود را مشخص کنید و سپس اقدام به ارسال حاشیه بفرمایید."); } DiwanComment comment = await _context.DiwanComments.Where(c => c.Id == commentId && c.UserId == userId).SingleOrDefaultAsync();//userId is not part of key but it helps making call secure if (comment == null) { return new RServiceResult(null); //not found } await CacheCleanForComment(commentId); comment.CoupletIndex = coupletIndex; string coupletSummary = ""; if (coupletIndex != null) { var relatedVerses = await _context.DiwanVerses.Where(v => v.PoemId == comment.PoemId && v.CoupletIndex == coupletIndex).OrderBy(v => v.VOrder).ToListAsync(); coupletSummary = relatedVerses.Count > 0 ? relatedVerses[0].Text : ""; if (relatedVerses.Count > 1) { coupletSummary += $" {relatedVerses[1].Text}"; } } _context.DiwanComments.Update(comment); await _context.SaveChangesAsync(); return new RServiceResult ( coupletSummary ); } /// /// delete a reported comment /// /// /// public async Task> DeleteModerateComment(int repordId) { DiwanCommentAbuseReport report = await _context.DiwanReportedComments.Where(r => r.Id == repordId).SingleOrDefaultAsync(); if (report == null) { return new RServiceResult(false); } var reportUserId = report.ReportedById; DiwanComment comment = await _context.DiwanComments.Where(c => c.Id == report.DiwanCommentId).SingleOrDefaultAsync(); if (comment == null) { return new RServiceResult(false); //not found } var commentId = report.DiwanCommentId; string commentHtmltext = comment.HtmlComment; if (comment.UserId != null) { string reason = ""; switch (report.ReasonCode) { case "offensive": reason = "توهین‌آمیز است."; break; case "religious": reason = "بحث مذهبی کرده."; break; case "repeated": reason = "تکراری است."; break; case "unrelated": reason = "به این شعر ربطی ندارد."; break; case "brokenlink": reason = "لینک شکسته است."; break; case "ad": reason = "تبلیغاتی است."; break; case "bogus": reason = "نامفهوم است."; break; case "latin": reason = "فارسی ننوشته."; break; case "other": reason = "دلیل دیگر"; break; } if (!string.IsNullOrEmpty(report.ReasonText)) reason += $" {report.ReasonText}"; reason = reason.Trim(); reason = string.IsNullOrEmpty(reason) ? "" : $"علت ارائه شده برای حذف یا متن گزارش کاربر شاکی: {Environment.NewLine}" + $"{reason} {Environment.NewLine}"; await _notificationService.PushNotification((Guid)comment.UserId, "حذف حاشیهٔ شما", $"حاشیهٔ شما به دلیل ناسازگاری با قوانین حاشیه‌گذاری گنجور و طبق گزارشات دیگر کاربران حذف شده است.{Environment.NewLine}" + $"{reason}{Environment.NewLine}" + $"نشانی صفحهٔ متناظر در گنجور{Environment.NewLine}" + $"این متن حاشیهٔ حذف شدهٔ شماست: {Environment.NewLine}" + $"{comment.HtmlComment}", NotificationType.Warning ); } //if user has got replies, delete them and notify their owners of what happened var replies = await _FindReplies(comment); for (int i = replies.Count - 1; i >= 0; i--) { if (replies[i].UserId != null) { await _notificationService.PushNotification((Guid)replies[i].UserId, "حذف پاسخ شما به حاشیه", $"پاسخ شما به یکی از حاشیه‌های گنجور به دلیل حذف زنجیرهٔ حاشیه توسط یکی از حاشیه‌گذاران حذف شده است.{Environment.NewLine}" + $"این متن حاشیهٔ حذف شدهٔ شماست: {Environment.NewLine}" + $"{replies[i].HtmlComment}", NotificationType.Warning ); } _context.DiwanComments.Remove(replies[i]); } _context.DiwanComments.Remove(comment); await _context.SaveChangesAsync(); await CacheCleanForComment(report.DiwanCommentId); if (reportUserId != null) { await _notificationService.PushNotification((Guid)reportUserId, "حذف حاشیهٔ گزارش شده توسط شما", $"گزارش شما برای حاشیه‌ای با متن ذیل پذیرفته و حاشیه حذف شد. متن حاشیهٔ گزارش شده توسط شما:{Environment.NewLine}" + commentHtmltext ); } return new RServiceResult(true); } /// /// publish awaiting comment /// /// /// public async Task> PublishAwaitingComment(int commentId) { DiwanComment comment = await _context.DiwanComments.Where(c => c.Id == commentId).SingleOrDefaultAsync();//userId is not part of key but it helps making call secure if (comment == null) { return new RServiceResult(false); //not found } comment.Status = PublishStatus.Published; _context.Update(comment); await _context.SaveChangesAsync(); return new RServiceResult(true); } /// /// delete anybody's comment /// /// /// public async Task> DeleteAnybodyComment(int commentId) { DiwanComment comment = await _context.DiwanComments.AsNoTracking().Where(c => c.Id == commentId).SingleOrDefaultAsync();//userId is not part of key but it helps making call secure if (comment == null) { return new RServiceResult(false); //not found } return await DeleteMyComment((Guid)comment.UserId, commentId); } /// /// delete user own comment /// /// /// /// public async Task> DeleteMyComment(Guid userId, int commentId) { DiwanComment comment = await _context.DiwanComments.Where(c => c.Id == commentId && c.UserId == userId).SingleOrDefaultAsync();//userId is not part of key but it helps making call secure if (comment == null) { return new RServiceResult(false); //not found } await CacheCleanForComment(commentId); //if user has got replies, delete them and notify their owners of what happened var replies = await _FindReplies(comment); for (int i = replies.Count - 1; i >= 0; i--) { if (replies[i].UserId != null && replies[i].UserId != userId) { await _notificationService.PushNotification((Guid)replies[i].UserId, "حذف پاسخ شما به حاشیه", $"پاسخ شما به یکی از حاشیه‌های گنجور به دلیل حذف زنجیرهٔ حاشیه توسط یکی از حاشیه‌گذاران حذف شده است.{Environment.NewLine}" + $"نشانی صفحهٔ متناظر در گنجور{Environment.NewLine}" + $"این متن حاشیهٔ حذف شدهٔ شماست: {Environment.NewLine}" + $"{replies[i].HtmlComment}", NotificationType.Warning ); } _context.DiwanComments.Remove(replies[i]); } _context.DiwanComments.Remove(comment); await _context.SaveChangesAsync(); return new RServiceResult(true); } private async Task> _FindReplies(DiwanComment comment) { List replies = await _context.DiwanComments.Where(c => c.InReplyToId == comment.Id).AsNoTracking().ToListAsync(); List replyToReplies = new List(); foreach (DiwanComment reply in replies) { replyToReplies.AddRange(await _FindReplies(reply)); } if (replyToReplies.Count > 0) { replies.AddRange(replyToReplies); } return replies; } /// /// get recent comments /// /// /// /// /// /// /// public async Task> GetRecentComments(PagingParameterModel paging, Guid filterUserId, bool onlyPublished, bool onlyAwaiting = false, string term = null) { string[] searchPatterns = LanguageUtils.SearchLikePatterns(term); // diwan: LIKE instead of full-text; empty term -> no filter var comments = _context.DiwanComments.AsQueryable(); foreach (var pattern in searchPatterns) comments = comments.Where(c => EF.Functions.Like(c.HtmlComment, pattern)); var source = from comment in comments.Include(c => c.Poem).Include(c => c.User).Include(c => c.InReplyTo).ThenInclude(r => r.User) where ((comment.Status == PublishStatus.Published) || !onlyPublished) && ((comment.Status == PublishStatus.Awaiting) || !onlyAwaiting) && ((filterUserId == Guid.Empty) || (filterUserId != Guid.Empty && comment.UserId == filterUserId)) orderby comment.CommentDate descending select new DiwanCommentFullViewModel() { Id = comment.Id, AuthorName = comment.User == null ? comment.AuthorName : $"{comment.User.NickName}", AuthorUrl = comment.AuthorUrl, CommentDate = comment.CommentDate, HtmlComment = comment.HtmlComment, PublishStatus = "",//invalid! UserId = comment.UserId, CoupletIndex = comment.CoupletIndex == null ? -1 : (int)comment.CoupletIndex, InReplyTo = comment.InReplyTo == null ? null : new DiwanCommentSummaryViewModel() { Id = comment.InReplyTo.Id, AuthorName = comment.InReplyTo.User == null ? comment.InReplyTo.AuthorName : $"{comment.InReplyTo.User.NickName}", AuthorUrl = comment.InReplyTo.AuthorUrl, CommentDate = comment.InReplyTo.CommentDate, HtmlComment = comment.InReplyTo.HtmlComment, PublishStatus = "", UserId = comment.InReplyTo.UserId, CoupletIndex = comment.InReplyTo.CoupletIndex == null ? -1 : (int)comment.InReplyTo.CoupletIndex, LikeCount = comment.InReplyTo.LikeCount, DislikeCount = comment.InReplyTo.DislikeCount, CurrentUserRatingValue = 0, }, Poem = new DiwanPoemSummaryViewModel() { Id = comment.Poem.Id, Title = comment.Poem.FullTitle, UrlSlug = comment.Poem.FullUrl, Excerpt = "" }, LikeCount = comment.LikeCount, DislikeCount = comment.DislikeCount, CurrentUserRatingValue = 0, }; (PaginationMetadata PagingMeta, DiwanCommentFullViewModel[] Items) paginatedResult = await QueryablePaginator.Paginate(source, paging); foreach (DiwanCommentFullViewModel comment in paginatedResult.Items) { comment.AuthorName = comment.AuthorName.ToPersianNumbers().ApplyCorrectYeKe(); var relatedVerses = comment.CoupletIndex == -1 ? new List() : await _context.DiwanVerses.Where(v => v.PoemId == comment.Poem.Id && v.CoupletIndex == comment.CoupletIndex).OrderBy(v => v.VOrder).ToListAsync(); string coupleText = relatedVerses.Count == 0 ? "" : relatedVerses[0].Text; for (int nVerseIndex = 1; nVerseIndex < relatedVerses.Count; nVerseIndex++) { coupleText += $" {relatedVerses[nVerseIndex].Text}"; } comment.CoupletSummary = _CutSummary(coupleText); if (comment.InReplyTo != null) { var replyRelatedVerses = comment.InReplyTo.CoupletIndex == -1 ? new List() : await _context.DiwanVerses.Where(v => v.PoemId == comment.Poem.Id && v.CoupletIndex == comment.InReplyTo.CoupletIndex).OrderBy(v => v.VOrder).ToListAsync(); string replyCoupleText = relatedVerses.Count == 0 ? "" : relatedVerses[0].Text; for (int nVerseIndex = 1; nVerseIndex < replyRelatedVerses.Count; nVerseIndex++) { replyCoupleText += $" {replyRelatedVerses[nVerseIndex].Text}"; } comment.InReplyTo.CoupletSummary = _CutSummary(replyCoupleText); } } return new RServiceResult<(PaginationMetadata PagingMeta, DiwanCommentFullViewModel[] Items)>(paginatedResult); } /// /// report a comment /// /// /// /// id of report record public async Task> ReportComment(Guid userId, DiwanPostReportCommentViewModel report) { DiwanCommentAbuseReport r = new DiwanCommentAbuseReport() { DiwanCommentId = report.CommentId, ReportedById = userId, ReasonCode = report.ReasonCode, ReasonText = report.ReasonText, }; _context.DiwanReportedComments.Add(r); await _context.SaveChangesAsync(); var moderators = await _appUserService.GetUsersHavingPermission(RMuseumSecurableItem.DiwanEntityShortName, RMuseumSecurableItem.ModerateOperationShortName); if (string.IsNullOrEmpty(moderators.ExceptionString)) //if not, do nothing! { foreach (var moderator in moderators.Result) { await _notificationService.PushNotification ( (Guid)moderator.Id, "گزارش حاشیه", $"گزارشی برای یک حاشیه ثبت شده است. لطفاً بخش حاشیه‌های گزارش شده را بررسی فرمایید.{Environment.NewLine}" + $"توجه فرمایید که اگر کاربر دیگری که دارای مجوز بررسی حاشیه‌هاست پیش از شما به آن رسیدگی کرده باشد آن را در صف نخواهید دید.", NotificationType.ActionRequired ); } } return new RServiceResult(r.DiwanCommentId); } /// /// delete a report /// /// /// public async Task> DeleteReport(int id) { DiwanCommentAbuseReport report = await _context.DiwanReportedComments.Where(r => r.Id == id).SingleOrDefaultAsync(); if (report == null) { return new RServiceResult(false); } _context.DiwanReportedComments.Remove(report); await _context.SaveChangesAsync(); return new RServiceResult(true); } /// /// Get list of reported comments /// /// /// public async Task> GetReportedComments(PagingParameterModel paging) { var source = from report in _context.DiwanReportedComments join comment in _context.DiwanComments.Include(c => c.Poem).Include(c => c.User).Include(c => c.InReplyTo).ThenInclude(r => r.User) on report.DiwanCommentId equals comment.Id orderby report.Id descending select new DiwanCommentAbuseReportViewModel() { Id = report.Id, ReasonCode = report.ReasonCode, ReasonText = report.ReasonText, Comment = new DiwanCommentFullViewModel() { Id = comment.Id, AuthorName = comment.User == null ? comment.AuthorName : $"{comment.User.NickName}", AuthorUrl = comment.AuthorUrl, CommentDate = comment.CommentDate, HtmlComment = comment.HtmlComment, PublishStatus = "",//invalid! UserId = comment.UserId, InReplyTo = comment.InReplyTo == null ? null : new DiwanCommentSummaryViewModel() { Id = comment.InReplyTo.Id, AuthorName = comment.InReplyTo.User == null ? comment.InReplyTo.AuthorName : $"{comment.InReplyTo.User.NickName}", AuthorUrl = comment.InReplyTo.AuthorUrl, CommentDate = comment.InReplyTo.CommentDate, HtmlComment = comment.InReplyTo.HtmlComment, PublishStatus = "", UserId = comment.InReplyTo.UserId, LikeCount = comment.LikeCount, DislikeCount = comment.DislikeCount, CurrentUserRatingValue = 0, }, Poem = new DiwanPoemSummaryViewModel() { Id = comment.Poem.Id, Title = comment.Poem.FullTitle, UrlSlug = comment.Poem.FullUrl, Excerpt = "" } } }; (PaginationMetadata PagingMeta, DiwanCommentAbuseReportViewModel[] Items) paginatedResult = await QueryablePaginator.Paginate(source, paging); foreach (DiwanCommentAbuseReportViewModel report in paginatedResult.Items) { report.Comment.AuthorName = report.Comment.AuthorName.ToPersianNumbers().ApplyCorrectYeKe(); } return new RServiceResult<(PaginationMetadata PagingMeta, DiwanCommentAbuseReportViewModel[] Items)>(paginatedResult); } /// /// get poem images by id (some fields are intentionally field with blank or null), /// EntityImageId : the most important data field, image url is {WebServiceUrl.Url}/api/images/thumb/{EntityImageId}.jpg or {WebServiceUrl.Url}/api/images/norm/{EntityImageId}.jpg /// /// /// public async Task> GetPoemImages(int id) { var museumSrc = from link in _context.DiwanLinks.Include(l => l.Artifact).Include(l => l.Item).ThenInclude(i => i.Images) join poem in _context.DiwanPoems on link.DiwanPostId equals poem.Id where link.DisplayOnPage == true && link.ReviewResult == ReviewResult.Approved && poem.Id == id orderby link.IsTextOriginalSource descending, link.ReviewDate select new PoemRelatedImage() { Id = link.Id, PoemRelatedImageType = PoemRelatedImageType.MuseumLink, ThumbnailImageUrl = link.Item.Images.First().ExternalNormalSizeImageUrl.Replace("/norm/", "/thumb/").Replace("/orig/", "/thumb/"), TargetPageUrl = link.LinkToOriginalSource ? link.OriginalSourceUrl : $"https://museum.ganjoor.net/items/{link.Artifact.FriendlyUrl}/{link.Item.FriendlyUrl}", AltText = $"{link.Artifact.Name} » {link.Item.Name}", IsTextOriginalSource = link.IsTextOriginalSource }; List museumImages = await museumSrc.ToListAsync(); var externalSrc = from link in _context.PinterestLinks join poem in _context.DiwanPoems on link.DiwanPostId equals poem.Id where link.ReviewResult == ReviewResult.Approved && poem.Id == id orderby link.ReviewDate select new PoemRelatedImage() { Id = link.Id, PoemRelatedImageType = PoemRelatedImageType.ExternalLink, ThumbnailImageUrl = link.LinkType == LinkType.Naskban ? link.PinterestImageUrl : link.Item.Images.First().ExternalNormalSizeImageUrl.Replace("/norm/", "/thumb/").Replace("/orig/", "/thumb/"), TargetPageUrl = link.PinterestUrl, AltText = link.AltText, IsTextOriginalSource = link.IsTextOriginalSource }; museumImages.AddRange(await externalSrc.AsNoTracking().ToListAsync()); for (int i = 0; i < museumImages.Count; i++) { museumImages[i].ImageOrder = 0; } return new RServiceResult(museumImages.ToArray()); } /// /// Get Poem By Url /// /// /// /// /// /// /// /// /// /// /// /// public async Task> GetPoemByUrl(string url, bool catInfo = true, bool catPoems = false, bool rhymes = true, bool recitations = true, bool images = true, bool songs = true, bool comments = true, bool verseDetails = true, bool navigation = true) { // /hafez/ => /hafez : if (url.LastIndexOf('/') == url.Length - 1) { url = url.Substring(0, url.Length - 1); } var poem = await _context.DiwanPoems.Where(p => p.FullUrl == url).SingleOrDefaultAsync(); if (poem == null) { return new RServiceResult(null); //not found } return await GetPoemById(poem.Id, catInfo, catPoems, rhymes, recitations, images, songs, comments, verseDetails, navigation); } /// /// get poem verses /// /// /// /// public async Task> GetPoemVersesAsync(int id, int coupletIndex) { try { return new RServiceResult(await _context.DiwanVerses .Where(v => v.PoemId == id && (coupletIndex == -1 || v.CoupletIndex == coupletIndex)) .OrderBy(v => v.VOrder) .Select ( v => new DiwanVerseViewModel() { Id = v.Id, VOrder = v.VOrder, CoupletIndex = v.CoupletIndex, VersePosition = v.VersePosition, Text = v.Text, SectionIndex1 = v.SectionIndex1, SectionIndex2 = v.SectionIndex2, SectionIndex3 = v.SectionIndex3, SectionIndex4 = v.SectionIndex4, LanguageId = v.LanguageId, CoupletSummary = v.CoupletSummary, } ).AsNoTracking().ToArrayAsync()); } catch (Exception exp) { return new RServiceResult(null, exp.ToString()); } } /// /// Get Poem By Id /// /// /// /// /// /// /// /// /// /// /// /// /// sections /// /// public async Task> GetPoemById(int id, bool catInfo = true, bool catPoems = false, bool rhymes = true, bool recitations = true, bool images = true, bool songs = true, bool comments = true, bool verseDetails = true, bool navigation = true, bool relatedpoems = true, bool sections = true, DiwanCommentSortOrder commentSortOrder = DiwanCommentSortOrder.TopRated) { var cachKey = $"GetPoemById({id}, {catInfo}, {catPoems}, {rhymes}, {recitations}, {images}, {songs}, {comments}, {verseDetails}, {navigation}, {relatedpoems}, {sections}, {commentSortOrder})"; if (!_memoryCache.TryGetValue(cachKey, out DiwanPoemCompleteViewModel poemViewModel)) { var poem = await _context.DiwanPoems.Where(p => p.Id == id).AsNoTracking().SingleOrDefaultAsync(); if (poem == null) { return new RServiceResult(null); //not found } DiwanPoetCompleteViewModel cat = null; if (catInfo) { var catRes = await GetCatById(poem.CatId, catPoems); if (!string.IsNullOrEmpty(catRes.ExceptionString)) { return new RServiceResult(null, catRes.ExceptionString); } cat = catRes.Result; } DiwanPoemSummaryViewModel next = null; if (navigation) { int nextId = await _context.DiwanPoems .Where(p => p.CatId == poem.CatId && p.Id > poem.Id) .AnyAsync() ? await _context.DiwanPoems .Where(p => p.CatId == poem.CatId && p.Id > poem.Id) .MinAsync(p => p.Id) : 0; if (nextId != 0) { next = await _context.DiwanPoems.Where(p => p.Id == nextId).Select ( p => new DiwanPoemSummaryViewModel() { Id = p.Id, Title = p.Title, UrlSlug = p.UrlSlug, Excerpt = _context.DiwanVerses.Where(v => v.PoemId == p.Id && v.VOrder == 1).FirstOrDefault().Text } ).AsNoTracking().SingleAsync(); } } DiwanPoemSummaryViewModel previous = null; if (navigation) { int preId = await _context.DiwanPoems .Where(p => p.CatId == poem.CatId && p.Id < poem.Id) .AnyAsync() ? await _context.DiwanPoems .Where(p => p.CatId == poem.CatId && p.Id < poem.Id) .MaxAsync(p => p.Id) : 0; if (preId != 0) { previous = await _context.DiwanPoems.Where(p => p.Id == preId).Select ( p => new DiwanPoemSummaryViewModel() { Id = p.Id, Title = p.Title, UrlSlug = p.UrlSlug, Excerpt = _context.DiwanVerses.Where(v => v.PoemId == p.Id && v.VOrder == 1).FirstOrDefault().Text } ).AsNoTracking().SingleAsync(); } } PublicRecitationViewModel[] rc = null; if (recitations) { var rcRes = await GetPoemRecitations(id); if (!string.IsNullOrEmpty(rcRes.ExceptionString)) return new RServiceResult(null, rcRes.ExceptionString); rc = rcRes.Result; } PoemRelatedImage[] imgs = null; if (images) { var imgsRes = await GetPoemImages(id); if (!string.IsNullOrEmpty(imgsRes.ExceptionString)) return new RServiceResult(null, imgsRes.ExceptionString); imgs = imgsRes.Result; } DiwanVerseViewModel[] verses = null; if (verseDetails) { verses = await _context.DiwanVerses .Where(v => v.PoemId == id) .OrderBy(v => v.VOrder) .Select ( v => new DiwanVerseViewModel() { Id = v.Id, VOrder = v.VOrder, CoupletIndex = v.CoupletIndex, VersePosition = v.VersePosition, Text = v.Text, SectionIndex1 = v.SectionIndex1, SectionIndex2 = v.SectionIndex2, SectionIndex3 = v.SectionIndex3, SectionIndex4 = v.SectionIndex4, LanguageId = v.LanguageId, CoupletSummary = v.CoupletSummary, } ).AsNoTracking().ToArrayAsync(); }; PoemMusicTrackViewModel[] tracks = null; // diwan: music features removed (models kept so they can be rebuilt later) DiwanCommentSummaryViewModel[] poemComments = null; if (comments) { var commentsRes = await GetPoemComments(id, Guid.Empty, null, commentSortOrder); if (!string.IsNullOrEmpty(commentsRes.ExceptionString)) return new RServiceResult(null, commentsRes.ExceptionString); poemComments = commentsRes.Result; } DiwanPoemSection[] poemSections = null; if (sections) { var poemSectionsRes = await GetPoemWholeSections(id); if (!string.IsNullOrEmpty(poemSectionsRes.ExceptionString)) return new RServiceResult(null, poemSectionsRes.ExceptionString); poemSections = poemSectionsRes.Result; } var tagsRes = await GetPoemGeoDateTagsAsync(id); if (!string.IsNullOrEmpty(tagsRes.ExceptionString)) return new RServiceResult(null, tagsRes.ExceptionString); PoemGeoDateTag[] geoDateTags = tagsRes.Result; var quotedRes = await GetDiwanQuotedPoemsForPoemAsync(id, 0, 6, null, true, true); if (!string.IsNullOrEmpty(quotedRes.ExceptionString)) return new RServiceResult(null, quotedRes.ExceptionString); DiwanQuotedPoemViewModel[] quoteds = quotedRes.Result; poemViewModel = new DiwanPoemCompleteViewModel() { Id = poem.Id, Title = poem.Title, FullTitle = poem.FullTitle, FullUrl = poem.FullUrl, UrlSlug = poem.UrlSlug, HtmlText = poem.HtmlText, PlainText = poem.PlainText, SourceName = poem.SourceName, SourceUrlSlug = poem.SourceUrlSlug, OldTag = poem.OldTag, OldTagPageUrl = poem.OldTagPageUrl, MixedModeOrder = poem.MixedModeOrder, Published = poem.Published, Language = poem.Language, PoemSummary = poem.PoemSummary, Category = cat, Next = next, Previous = previous, Recitations = rc, Images = imgs, Verses = verses, Songs = tracks, Comments = poemComments, Sections = poemSections, GeoDateTags = geoDateTags, Top6QuotedPoems = quoteds, ClaimedByMultiplePoets = poem.ClaimedByMultiplePoets, }; if (AggressiveCacheEnabled) { _memoryCache.Set(cachKey, poemViewModel, TimeSpan.FromHours(1)); } } return new RServiceResult ( poemViewModel ); } /// /// delete unreviewed user corrections for a poem /// /// /// /// public async Task> DeletePoemCorrections(Guid userId, int poemId) { var preCorrections = await _context.DiwanPoemCorrections.Include(c => c.VerseOrderText).Include(c => c.GeoDateTags) .Where(c => c.UserId == userId && c.PoemId == poemId && c.Reviewed == false) .ToListAsync(); if (preCorrections.Count > 0) { foreach (var preCorrection in preCorrections) { preCorrection.VerseOrderText.Clear(); preCorrection.GeoDateTags?.Clear(); } _context.DiwanPoemCorrections.RemoveRange(preCorrections); await _context.SaveChangesAsync(); } return new RServiceResult(true); } /// /// send poem correction /// /// /// public async Task> SuggestPoemCorrection(DiwanPoemCorrectionViewModel correction) { // wrapped in try/catch (matching ModeratePoemCorrection's own pattern) so an unexpected // exception - e.g. the EF Core "same key value is already being tracked" conflict this // method used to throw on a second save - comes back as a normal RServiceResult.ExceptionString // instead of an unhandled exception. Left uncaught, it would bypass DiwanController's own // "if (!string.IsNullOrEmpty(res.ExceptionString)) return BadRequest(...)" check entirely // (that line is never reached because this method never returns), fall through to the // framework's generic HTML error page, and show up to the user as unreadable HTML source // instead of a readable message - with the real exception visible only in the Windows Event // Log. try { if (!string.IsNullOrEmpty(correction.Rhythm3) || !string.IsNullOrEmpty(correction.Rhythm4)) return new RServiceResult(null, "انتساب وزن سوم و چهارم هنوز پیاده‌سازی نشده است."); var sections = await _context.DiwanPoemSections.AsNoTracking().Include(s => s.DiwanMetre) .Where(s => s.PoemId == correction.PoemId).OrderBy(s => s.SectionType).ThenBy(s => s.Index).ToListAsync(); //beware: items consisting only of paragraphs have no main setion (mainSection in the following line can legitimately become null) var mainSection = sections.FirstOrDefault(s => s.SectionType == PoemSectionType.WholePoem && s.VerseType == VersePoemSectionType.First); var secondSection = sections.FirstOrDefault(s => s.SectionType == PoemSectionType.WholePoem && s.VerseType == VersePoemSectionType.Second); if (correction.Rhythm != null || correction.Rhythm2 != null) { if (correction.Rhythm == "") return new RServiceResult(null, "امکان حذف وزن اول با وجود وزن دوم وجود ندارد."); if (mainSection == null) return new RServiceResult(null, "امکان تعیین وزن برای این مورد وجود ندارد."); var poemVerses = await _context.DiwanVerses.AsNoTracking(). Where(p => p.PoemId == correction.PoemId).OrderBy(v => v.VOrder).ToListAsync(); if (poemVerses.Where(v => v.VersePosition == VersePosition.Paragraph).Any()) { return new RServiceResult(null, "امکان انتساب وزن به متون مخلوط از طریق ویرایشگر کاربر وجود ندارد."); } if (sections.Where(s => s.SectionType == PoemSectionType.WholePoem && s.VerseType == VersePoemSectionType.First).Count() > 1) { return new RServiceResult(null, "امکان انتساب وزن به متون حاوی بیش از یک شعر از طریق ویرایشگر کاربر وجود ندارد."); } } var preCorrections = await _context.DiwanPoemCorrections.Include(c => c.VerseOrderText).Include(c => c.GeoDateTags) .Where(c => c.UserId == correction.UserId && c.PoemId == correction.PoemId && c.Reviewed == false) .ToListAsync(); var poem = (await GetPoemById(correction.PoemId, false, false, true, false, false, false, false, true, false)).Result; if (correction.GeoDateTags != null) { // already-approved tags for this poem, used below to reject suggestions that just // repeat what's already tagged on the same couplet - no extra service call needed, // GetPoemById above always loads this var approvedGeoDateTags = poem.GeoDateTags ?? Array.Empty(); // lazily loaded only if a tag actually suggests a brand new location - most // submissions either pick an existing location or only carry a date, so this // extra query is skipped entirely in the common case DiwanGeoLocation[] allLocations = null; foreach (var geoDateTag in correction.GeoDateTags) { if (!geoDateTag.MarkForDelete) { bool hasLocation = geoDateTag.LocationId != null || (!string.IsNullOrWhiteSpace(geoDateTag.SuggestedLocationName) && geoDateTag.SuggestedLatitude != null && geoDateTag.SuggestedLongitude != null); bool hasDate = geoDateTag.LunarYear != null; bool hasPerson = geoDateTag.PersonId != null || !string.IsNullOrWhiteSpace(geoDateTag.SuggestedPersonGraphJson); if (!hasLocation && !hasDate && !hasPerson) { return new RServiceResult(null, "برچسب جغرافیایی/تاریخی/شخصیتی باید حداقل شامل مکان، تاریخ یا شخصیت باشد."); } if (geoDateTag.LocationId == null && !string.IsNullOrWhiteSpace(geoDateTag.SuggestedLocationName) && geoDateTag.SuggestedLatitude != null && geoDateTag.SuggestedLongitude != null) { // catches "this is already a catalogued place" regardless of which couplet/poem // it was previously tagged on - a hard reject, since coordinates a few kilometers // apart are effectively certainly the same real place, not a judgment call const double nearDuplicateKm = 5.0; if (allLocations == null) { allLocations = await _context.DiwanGeoLocations.AsNoTracking().ToArrayAsync(); } var nearExistingLocation = allLocations.FirstOrDefault(l => _GeoDistanceKm((double)geoDateTag.SuggestedLatitude, (double)geoDateTag.SuggestedLongitude, l.Latitude, l.Longitude) <= nearDuplicateKm); if (nearExistingLocation != null) { return new RServiceResult(null, $"مکانی با نام «{nearExistingLocation.Name}» با مختصات بسیار نزدیک از قبل در فهرست مکان‌ها ثبت شده است. لطفاً به‌جای پیشنهاد مکان جدید، همان را از فهرست «مکان» انتخاب کنید."); } // a same-name-but-far-away match is deliberately NOT rejected here - it can // legitimately be a different real place sharing a name, and the editor's // client-side check already asks the user to confirm that before it gets here } // PoemGeoDateTag.CoupletIndex uses 0 for "whole poem", matching a null CoupletIndex here int effectiveCoupletIndex = geoDateTag.CoupletIndex ?? 0; var approvedForCouplet = approvedGeoDateTags.Where(t => t.CoupletIndex == effectiveCoupletIndex); if (hasLocation) { bool locationAlreadyTagged = geoDateTag.LocationId != null ? approvedForCouplet.Any(t => t.LocationId == geoDateTag.LocationId) : approvedForCouplet.Any(t => t.Location != null && string.Equals(t.Location.Name?.Trim(), geoDateTag.SuggestedLocationName.Trim(), StringComparison.OrdinalIgnoreCase)); if (locationAlreadyTagged) { return new RServiceResult(null, "این مکان از قبل برای همین بیت ثبت شده است."); } } if (hasDate) { bool dateAlreadyTagged = approvedForCouplet.Any(t => t.LunarYear == geoDateTag.LunarYear && t.LunarMonth == geoDateTag.LunarMonth && t.LunarDay == geoDateTag.LunarDay); if (dateAlreadyTagged) { return new RServiceResult(null, "این تاریخ از قبل برای همین بیت ثبت شده است."); } } } } } foreach (var verse in correction.VerseOrderText) { if (!verse.NewVerse) { var v = poem.Verses.Where(poemVerse => poemVerse.VOrder == verse.VORder).First(); verse.OriginalText = v.Text; verse.OriginalVersePosition = v.VersePosition; verse.OriginalLanguageId = v.LanguageId; verse.OriginalCoupletSummary = v.CoupletSummary; } } if (correction.GeoDateTags != null) { // every submission here creates a brand-new DiwanPoemCorrection row (and any older // unreviewed correction for the same user/poem - preCorrections above - is removed // together with its own GeoDateTags rows a few lines down). The client pre-fills the // form from the user's previous, still-unreviewed draft (MyLastEditGeoDateTagsJson in // Editor.cshtml.cs/.cshtml), so when the user only adds one more tag and resubmits, the // posted GeoDateTags array still carries the OLD rows' real database Ids alongside the // new one. Those old rows are already tracked by EF from the preCorrections query above, // so attaching this freshly-deserialized correction.GeoDateTags (Added, because its new // parent dbCorrection is Added) under those same Ids makes EF try to track two different // instances for the same key, throwing "cannot be tracked because another instance with // the same key value ... is already being tracked" - reproducible simply by saving a // correction, then adding another person/tag and saving again. Every tag belongs to a // brand-new correction row here, so none of them should ever reuse an old row's Id. foreach (var geoDateTag in correction.GeoDateTags) { geoDateTag.Id = 0; } } DiwanPoemCorrection dbCorrection = new DiwanPoemCorrection() { PoemId = correction.PoemId, UserId = correction.UserId, VerseOrderText = correction.VerseOrderText, Title = correction.Title, OriginalTitle = poem.Title, Rhythm = correction.Rhythm, OriginalRhythm = (mainSection == null || mainSection.DiwanMetre == null) ? null : mainSection.DiwanMetre.Rhythm, Rhythm2 = correction.Rhythm2, OriginalRhythm2 = (secondSection == null || secondSection.DiwanMetre == null) ? null : secondSection.DiwanMetre.Rhythm, RhymeLetters = correction.RhymeLetters, OriginalRhymeLetters = mainSection == null ? null : mainSection.RhymeLetters, PoemFormat = correction.PoemFormat, OriginalPoemFormat = correction.PoemFormat == null ? null : mainSection.PoemFormat, Note = correction.Note, Date = DateTime.Now, Result = CorrectionReviewResult.NotReviewed, Reviewed = false, AffectedThePoem = false, //Language = correction.Language, not used PoemSummary = correction.PoemSummary, HideMyName = correction.HideMyName, GeoDateTags = correction.GeoDateTags, }; _context.DiwanPoemCorrections.Add(dbCorrection); await _context.SaveChangesAsync(); correction.Id = dbCorrection.Id; if (preCorrections.Count > 0) { foreach (var preCorrection in preCorrections) { preCorrection.VerseOrderText.Clear(); preCorrection.GeoDateTags?.Clear(); } _context.DiwanPoemCorrections.RemoveRange(preCorrections); await _context.SaveChangesAsync(); } return new RServiceResult(correction); } catch (Exception exp) { return new RServiceResult(null, exp.ToString()); } } /// /// last unreviewed user correction for a poem /// /// /// /// public async Task> GetLastUnreviewedUserCorrectionForPoem(Guid userId, int poemId) { var dbCorrection = await _context.DiwanPoemCorrections.AsNoTracking().Include(c => c.VerseOrderText).Include(c => c.GeoDateTags).ThenInclude(g => g.Location).Include(c => c.User) .Where(c => c.UserId == userId && c.PoemId == poemId && c.Reviewed == false) .OrderByDescending(c => c.Id) .FirstOrDefaultAsync(); if (dbCorrection == null) return new RServiceResult(null); return new RServiceResult ( new DiwanPoemCorrectionViewModel() { Id = dbCorrection.Id, PoemId = dbCorrection.PoemId, UserId = dbCorrection.UserId, VerseOrderText = dbCorrection.VerseOrderText == null ? null : dbCorrection.VerseOrderText.ToArray(), Title = dbCorrection.Title, OriginalTitle = dbCorrection.OriginalTitle, Rhythm = dbCorrection.Rhythm, OriginalRhythm = dbCorrection.OriginalRhythm, RhythmResult = dbCorrection.RhythmResult, Rhythm2 = dbCorrection.Rhythm2, OriginalRhythm2 = dbCorrection.OriginalRhythm2, RhymeLetters = dbCorrection.RhymeLetters, OriginalRhymeLetters = dbCorrection.OriginalRhymeLetters, RhymeLettersReviewResult = dbCorrection.RhymeLettersReviewResult, PoemSummary = dbCorrection.PoemSummary, OriginalPoemSummary = dbCorrection.OriginalPoemSummary, SummaryReviewResult = dbCorrection.SummaryReviewResult, Note = dbCorrection.Note, Date = dbCorrection.Date, Reviewed = dbCorrection.Reviewed, Result = dbCorrection.Result, ReviewNote = dbCorrection.ReviewNote, ReviewDate = dbCorrection.ReviewDate, UserNickname = dbCorrection.HideMyName && dbCorrection.Reviewed ? "" : string.IsNullOrEmpty(dbCorrection.User.NickName) ? dbCorrection.User.Id.ToString() : dbCorrection.User.NickName, PoemFormat = dbCorrection.PoemFormat, OriginalPoemFormat = dbCorrection.OriginalPoemFormat, PoemFormatReviewResult = dbCorrection.PoemFormatReviewResult, HideMyName = dbCorrection.HideMyName, GeoDateTags = dbCorrection.GeoDateTags == null ? null : dbCorrection.GeoDateTags.ToArray(), } ); } /// /// get user or all corrections /// /// if sent empty returns all corrections /// /// public async Task> GetUserCorrections(Guid userId, PagingParameterModel paging) { var source = from dbCorrection in _context.DiwanPoemCorrections.AsNoTracking().Include(c => c.VerseOrderText).Include(c => c.GeoDateTags).ThenInclude(g => g.Location).Include(c => c.User) where userId == Guid.Empty || dbCorrection.UserId == userId orderby dbCorrection.Id descending select dbCorrection; (PaginationMetadata PagingMeta, DiwanPoemCorrection[] Items) dbPaginatedResult = await QueryablePaginator.Paginate(source, paging); List list = new List(); foreach (var dbCorrection in dbPaginatedResult.Items) { list.Add ( new DiwanPoemCorrectionViewModel() { Id = dbCorrection.Id, PoemId = dbCorrection.PoemId, UserId = dbCorrection.UserId, VerseOrderText = dbCorrection.VerseOrderText == null ? null : dbCorrection.VerseOrderText.ToArray(), Title = dbCorrection.Title, OriginalTitle = dbCorrection.OriginalTitle, Rhythm = dbCorrection.Rhythm, OriginalRhythm = dbCorrection.OriginalRhythm, RhythmResult = dbCorrection.RhythmResult, Rhythm2 = dbCorrection.Rhythm2, OriginalRhythm2 = dbCorrection.OriginalRhythm2, Rhythm2Result = dbCorrection.Rhythm2Result, RhymeLetters = dbCorrection.RhymeLetters, OriginalRhymeLetters = dbCorrection.OriginalRhymeLetters, RhymeLettersReviewResult = dbCorrection.RhymeLettersReviewResult, PoemSummary = dbCorrection.PoemSummary, OriginalPoemSummary = dbCorrection.OriginalPoemSummary, SummaryReviewResult = dbCorrection.SummaryReviewResult, Note = dbCorrection.Note, Date = dbCorrection.Date, Reviewed = dbCorrection.Reviewed, Result = dbCorrection.Result, ReviewNote = dbCorrection.ReviewNote, ReviewDate = dbCorrection.ReviewDate, UserNickname = dbCorrection.HideMyName && dbCorrection.Reviewed ? "" : string.IsNullOrEmpty(dbCorrection.User.NickName) ? dbCorrection.User.Id.ToString() : dbCorrection.User.NickName, PoemFormat = dbCorrection.PoemFormat, OriginalPoemFormat = dbCorrection.OriginalPoemFormat, PoemFormatReviewResult = dbCorrection.PoemFormatReviewResult, HideMyName = dbCorrection.HideMyName, GeoDateTags = dbCorrection.GeoDateTags == null ? null : dbCorrection.GeoDateTags.ToArray(), } ); } return new RServiceResult<(PaginationMetadata, DiwanPoemCorrectionViewModel[])> ((dbPaginatedResult.PagingMeta, list.ToArray())); } /// /// effective corrections for poem /// /// /// /// public async Task> GetPoemEffectiveCorrections(int poemId, PagingParameterModel paging) { var source = from dbCorrection in _context.DiwanPoemCorrections.AsNoTracking().Include(c => c.VerseOrderText).Include(c => c.GeoDateTags).ThenInclude(g => g.Location) where dbCorrection.PoemId == poemId && dbCorrection.Reviewed == true && ( dbCorrection.Result == CorrectionReviewResult.Approved || dbCorrection.RhythmResult == CorrectionReviewResult.Approved || dbCorrection.Rhythm2Result == CorrectionReviewResult.Approved || dbCorrection.RhymeLettersReviewResult == CorrectionReviewResult.Approved || dbCorrection.PoemFormatReviewResult == CorrectionReviewResult.Approved || dbCorrection.SummaryReviewResult == CorrectionReviewResult.Approved || dbCorrection.VerseOrderText .Any(v => v.Result == CorrectionReviewResult.Approved || v.VersePositionResult == CorrectionReviewResult.Approved || v.MarkForDeleteResult == CorrectionReviewResult.Approved || v.NewVerseResult == CorrectionReviewResult.Approved || v.SummaryReviewResult == CorrectionReviewResult.Approved || v.LanguageReviewResult == CorrectionReviewResult.Approved ) || dbCorrection.GeoDateTags.Any(g => g.Result == CorrectionReviewResult.Approved) ) orderby dbCorrection.Id descending select dbCorrection; (PaginationMetadata PagingMeta, DiwanPoemCorrection[] Items) dbPaginatedResult = await QueryablePaginator.Paginate(source, paging); List list = new List(); foreach (var dbCorrection in dbPaginatedResult.Items) { list.Add ( new DiwanPoemCorrectionViewModel() { Id = dbCorrection.Id, PoemId = dbCorrection.PoemId, UserId = dbCorrection.UserId, VerseOrderText = dbCorrection.VerseOrderText == null ? null : dbCorrection.VerseOrderText.ToArray(), Title = dbCorrection.Title, OriginalTitle = dbCorrection.OriginalTitle, Rhythm = dbCorrection.Rhythm, OriginalRhythm = dbCorrection.OriginalRhythm, RhythmResult = dbCorrection.RhythmResult, Rhythm2 = dbCorrection.Rhythm2, OriginalRhythm2 = dbCorrection.OriginalRhythm2, Rhythm2Result = dbCorrection.Rhythm2Result, RhymeLetters = dbCorrection.RhymeLetters, OriginalRhymeLetters = dbCorrection.OriginalRhymeLetters, RhymeLettersReviewResult = dbCorrection.RhymeLettersReviewResult, PoemSummary = dbCorrection.PoemSummary, OriginalPoemSummary = dbCorrection.OriginalPoemSummary, SummaryReviewResult = dbCorrection.SummaryReviewResult, Note = dbCorrection.Note, Date = dbCorrection.Date, Reviewed = dbCorrection.Reviewed, Result = dbCorrection.Result, ReviewNote = dbCorrection.ReviewNote, ReviewDate = dbCorrection.ReviewDate, PoemFormat = dbCorrection.PoemFormat, OriginalPoemFormat = dbCorrection.OriginalPoemFormat, PoemFormatReviewResult = dbCorrection.PoemFormatReviewResult, GeoDateTags = dbCorrection.GeoDateTags == null ? null : dbCorrection.GeoDateTags.ToArray(), } ); } return new RServiceResult<(PaginationMetadata, DiwanPoemCorrectionViewModel[])> ((dbPaginatedResult.PagingMeta, list.ToArray())); } /// /// get correction by id /// /// /// public async Task> GetCorrectionById(int id) { var dbCorrection = await _context.DiwanPoemCorrections.AsNoTracking().Include(c => c.VerseOrderText).Include(c => c.GeoDateTags).ThenInclude(g => g.Location).Include(c => c.User) .Where(c => c.Id == id) .FirstOrDefaultAsync(); if (dbCorrection == null) return new RServiceResult(null); return new RServiceResult ( new DiwanPoemCorrectionViewModel() { Id = dbCorrection.Id, PoemId = dbCorrection.PoemId, UserId = dbCorrection.UserId, VerseOrderText = dbCorrection.VerseOrderText == null ? null : dbCorrection.VerseOrderText.OrderBy(v => v.VORder).ToArray(), Title = dbCorrection.Title, OriginalTitle = dbCorrection.OriginalTitle, Rhythm = dbCorrection.Rhythm, OriginalRhythm = dbCorrection.OriginalRhythm, RhythmResult = dbCorrection.RhythmResult, Rhythm2 = dbCorrection.Rhythm2, OriginalRhythm2 = dbCorrection.OriginalRhythm2, Rhythm2Result = dbCorrection.Rhythm2Result, RhymeLetters = dbCorrection.RhymeLetters, OriginalRhymeLetters = dbCorrection.OriginalRhymeLetters, RhymeLettersReviewResult = dbCorrection.RhymeLettersReviewResult, PoemSummary = dbCorrection.PoemSummary, OriginalPoemSummary = dbCorrection.OriginalPoemSummary, SummaryReviewResult = dbCorrection.SummaryReviewResult, Note = dbCorrection.Note, Date = dbCorrection.Date, Reviewed = dbCorrection.Reviewed, Result = dbCorrection.Result, ReviewNote = dbCorrection.ReviewNote, ReviewDate = dbCorrection.ReviewDate, UserNickname = dbCorrection.HideMyName && dbCorrection.Reviewed ? "" : string.IsNullOrEmpty(dbCorrection.User.NickName) ? dbCorrection.User.Id.ToString() : dbCorrection.User.NickName, PoemFormat = dbCorrection.PoemFormat, OriginalPoemFormat = dbCorrection.OriginalPoemFormat, PoemFormatReviewResult = dbCorrection.PoemFormatReviewResult, HideMyName = dbCorrection.HideMyName, GeoDateTags = dbCorrection.GeoDateTags == null ? null : dbCorrection.GeoDateTags.ToArray(), } ); } /// /// get next unreviewed correction /// /// /// /// public async Task> GetNextUnreviewedCorrection(int skip, bool onlyUserCorrections) { string systemEmail = $"{Configuration.GetSection("Diwan")["SystemEmail"]}"; var systemUser = await _appUserService.FindUserByEmail(systemEmail); var systemUserId = systemUser.Result == null ? Guid.Empty : (Guid)systemUser.Result.Id; var dbCorrection = await _context.DiwanPoemCorrections.AsNoTracking().Include(c => c.VerseOrderText).Include(c => c.GeoDateTags).ThenInclude(g => g.Location).Include(c => c.User) .Where(c => c.Reviewed == false && (onlyUserCorrections == false || c.UserId != systemUserId)) .OrderBy(c => c.Id) .Skip(skip) .FirstOrDefaultAsync(); if (dbCorrection == null) return new RServiceResult(null); return new RServiceResult ( new DiwanPoemCorrectionViewModel() { Id = dbCorrection.Id, PoemId = dbCorrection.PoemId, UserId = dbCorrection.UserId, VerseOrderText = dbCorrection.VerseOrderText == null ? null : dbCorrection.VerseOrderText.ToArray(), Title = dbCorrection.Title, OriginalTitle = dbCorrection.OriginalTitle, Rhythm = dbCorrection.Rhythm, OriginalRhythm = dbCorrection.OriginalRhythm, RhythmResult = dbCorrection.RhythmResult, RhymeLetters = dbCorrection.RhymeLetters, OriginalRhymeLetters = dbCorrection.OriginalRhymeLetters, RhymeLettersReviewResult = dbCorrection.RhymeLettersReviewResult, PoemSummary = dbCorrection.PoemSummary, OriginalPoemSummary = dbCorrection.OriginalPoemSummary, SummaryReviewResult = dbCorrection.SummaryReviewResult, Note = dbCorrection.Note, Date = dbCorrection.Date, Reviewed = dbCorrection.Reviewed, Result = dbCorrection.Result, Rhythm2 = dbCorrection.Rhythm2, OriginalRhythm2 = dbCorrection.OriginalRhythm2, Rhythm2Result = dbCorrection.Rhythm2Result, ReviewNote = dbCorrection.ReviewNote, ReviewDate = dbCorrection.ReviewDate, UserNickname = dbCorrection.HideMyName && dbCorrection.Reviewed ? "" : string.IsNullOrEmpty(dbCorrection.User.NickName) ? dbCorrection.User.Id.ToString() : dbCorrection.User.NickName, PoemFormat = dbCorrection.PoemFormat, OriginalPoemFormat = dbCorrection.OriginalPoemFormat, PoemFormatReviewResult = dbCorrection.PoemFormatReviewResult, HideMyName = dbCorrection.HideMyName, GeoDateTags = dbCorrection.GeoDateTags == null ? null : dbCorrection.GeoDateTags.ToArray(), } ); } /// /// unreviewed corrections count /// /// /// public async Task> GetUnreviewedCorrectionCount(bool onlyUserCorrections) { string systemEmail = $"{Configuration.GetSection("Diwan")["SystemEmail"]}"; var systemUser = await _appUserService.FindUserByEmail(systemEmail); var systemUserId = systemUser.Result == null ? Guid.Empty : (Guid)systemUser.Result.Id; return new RServiceResult(await _context.DiwanPoemCorrections.AsNoTracking().Include(c => c.VerseOrderText) .Where(c => c.Reviewed == false && (onlyUserCorrections == false || c.UserId != systemUserId)) .CountAsync()); } /// /// random poem id from hafez sonnets and old c.ganjoor.net service /// /// private int _GetRandomPoemId(int poetId, int loopBreaker = 0) { // diwan: data-driven (upstream used hard-coded id ranges of Persian poets); 0 = any poet var poems = _context.DiwanPoems.AsNoTracking().Where(p => p.Published); if (poetId != 0) poems = poems.Where(p => p.Cat.PoetId == poetId); return poems.OrderBy(p => Guid.NewGuid()).Select(p => p.Id).FirstOrDefault(); } /// /// get a random poem from hafez /// /// /// /// public async Task> Faal(int poetId = 0, bool recitation = true) { int poemId = _GetRandomPoemId(poetId); var poem = await _context.DiwanPoems.Where(p => p.Id == poemId).AsNoTracking().SingleOrDefaultAsync(); PublicRecitationViewModel[] recitations = poem == null || !recitation ? new PublicRecitationViewModel[] { } : (await GetPoemRecitations(poemId)).Result; int loopPreventer = 0; while (poem == null || (recitation && recitations.Length == 0)) { poem = await _context.DiwanPoems.Where(p => p.Id == poemId).AsNoTracking().SingleOrDefaultAsync(); recitations = poem == null ? new PublicRecitationViewModel[] { } : (await GetPoemRecitations(poemId)).Result; loopPreventer++; if (loopPreventer > 5) { return new RServiceResult(null); } } return await GetPoemById(poemId, false, false, false, recitation, false, false, false, true /*verse details*/, false); } /// /// Get Similar Poems accroding to prosody and rhyme informations /// /// /// /// /// /// /// /// /// /// /// /// /// public async Task> GetSimilarPoemsAsync(PagingParameterModel paging, string metre, string rhyme, int? poetId, int? catId, string language, DiwanPoemFormat format , string term , int coupletCountsFrom, int coupletCountsTo, int[] exceptPoetId) { if (poetId == null) { catId = null; } List catIdList = new List(); if (catId != null) { catIdList.Add((int)catId); await _populateCategoryChildren(_context, (int)catId, catIdList); } string[] searchPatterns = LanguageUtils.SearchLikePatterns(term); // diwan: LIKE instead of full-text; empty term -> no filter var sections = _context.DiwanPoemSections.AsQueryable(); foreach (var pattern in searchPatterns) sections = sections.Where(s => EF.Functions.Like(s.Poem.PlainText, pattern)); var source = sections.Include(s => s.Poem).Include(s => s.Poet).Include(s => s.DiwanMetre) .Where(s => (poetId == null || s.PoetId == poetId) && (exceptPoetId.Length == 0 || !exceptPoetId.Contains(s.PoetId ?? 0)) && ((language == "ur-PK" && string.IsNullOrEmpty(s.Language)) || s.Language == language) && (string.IsNullOrEmpty(metre) || (metre == "null" && s.DiwanMetreId == null) || (!string.IsNullOrEmpty(metre) && s.DiwanMetre.Rhythm == metre)) && ((string.IsNullOrEmpty(rhyme) && s.SectionType == PoemSectionType.WholePoem) || (!string.IsNullOrEmpty(rhyme) && s.RhymeLetters == rhyme)) && (format == DiwanPoemFormat.Unknown || s.PoemFormat == format) && (catId == null || catIdList.Contains(s.Poem.CatId)) && (s.CoupletsCount >= coupletCountsFrom) && (coupletCountsTo == 0 || s.CoupletsCount <= coupletCountsTo) ) .OrderBy(p => p.Poet.BirthYearInLHijri).ThenBy(p => p.Poet.Nickname).ThenBy(p => p.SectionType).ThenBy(p => p.Poem.Id) .Select ( section => new DiwanPoemCompleteViewModel() { Id = section.Poem.Id, Title = section.Poem.Title, FullTitle = section.Poem.FullTitle, FullUrl = section.CachedFirstCoupletIndex == 0 ? section.Poem.FullUrl : section.Poem.FullUrl + "#bn" + (section.CachedFirstCoupletIndex + 1).ToString(), UrlSlug = section.Poem.UrlSlug, HtmlText = section.HtmlText, PlainText = section.PlainText, MixedModeOrder = section.Poem.MixedModeOrder, Published = section.Poem.Published, Language = section.Poem.Language, PoemSummary = section.Poem.PoemSummary, Category = new DiwanPoetCompleteViewModel() { Poet = new DiwanPoetViewModel() { Id = section.Poet.Id, } }, SectionIndex = section.Index, ClaimedByMultiplePoets = section.Poem.ClaimedByMultiplePoets, CoupletsCount = section.CoupletsCount, } ).AsNoTracking(); (PaginationMetadata PagingMeta, DiwanPoemCompleteViewModel[] Items) paginatedResult = await QueryablePaginator.Paginate(source, paging); Dictionary cachedPoets = new Dictionary(); foreach (var item in paginatedResult.Items) { if (cachedPoets.TryGetValue(item.Category.Poet.Id, out DiwanPoetCompleteViewModel poet)) { item.Category = poet; } else { poet = (await GetPoetById(item.Category.Poet.Id)).Result; cachedPoets.Add(item.Category.Poet.Id, poet); item.Category = poet; } } return new RServiceResult<(PaginationMetadata PagingMeta, DiwanPoemCompleteViewModel[] Items)>(paginatedResult); } private async Task _populateCategoryChildren(RMuseumDbContext context, int catId, List catListId) { var catRes = await _GetCatById(context, catId, false); foreach (var c in catRes.Result.Cat.Children) { catListId.Add(c.Id); await _populateCategoryChildren(context, c.Id, catListId); } } /// /// language tagged poem sections /// /// /// /// /// public async Task> GetLanguageTaggedPoemSections(PagingParameterModel paging, string language, int? poetId) { if (string.IsNullOrEmpty(language)) { language = "ur-PK"; } var source = _context.DiwanPoemSections.Include(s => s.Poem).Include(s => s.Poet).Include(s => s.DiwanMetre) .Where(s => (poetId == null || s.PoetId == poetId) && ((language == "ur-PK" && string.IsNullOrEmpty(s.Language)) || s.Language == language) && s.SectionType == PoemSectionType.WholePoem ) .OrderBy(p => p.Poet.BirthYearInLHijri).ThenBy(p => p.Poet.Nickname).ThenBy(p => p.Poem.Id) .Select ( section => new DiwanPoemCompleteViewModel() { Id = section.Poem.Id, Title = section.Poem.Title, FullTitle = section.Poem.FullTitle, FullUrl = section.CachedFirstCoupletIndex == 0 ? section.Poem.FullUrl : section.Poem.FullUrl + "#bn" + (section.CachedFirstCoupletIndex + 1).ToString(), UrlSlug = section.Poem.UrlSlug, HtmlText = section.HtmlText, PlainText = section.PlainText, MixedModeOrder = section.Poem.MixedModeOrder, Published = section.Poem.Published, Language = section.Poem.Language, PoemSummary = section.Poem.PoemSummary, Category = new DiwanPoetCompleteViewModel() { Poet = new DiwanPoetViewModel() { Id = section.Poet.Id, } }, SectionIndex = section.Index, ClaimedByMultiplePoets = section.Poem.ClaimedByMultiplePoets, } ).AsNoTracking(); (PaginationMetadata PagingMeta, DiwanPoemCompleteViewModel[] Items) paginatedResult = await QueryablePaginator.Paginate(source, paging); Dictionary cachedPoets = new Dictionary(); foreach (var item in paginatedResult.Items) { if (cachedPoets.TryGetValue(item.Category.Poet.Id, out DiwanPoetCompleteViewModel poet)) { item.Category = poet; } else { poet = (await GetPoetById(item.Category.Poet.Id)).Result; cachedPoets.Add(item.Category.Poet.Id, poet); item.Category = poet; } } return new RServiceResult<(PaginationMetadata PagingMeta, DiwanPoemCompleteViewModel[] Items)>(paginatedResult); } /// /// Search /// You need to run this scripts manually on the database before using this method: /// /// CREATE FULLTEXT CATALOG [DiwanPoemPlainTextCatalog] WITH ACCENT_SENSITIVITY = OFF AS DEFAULT /// /// CREATE FULLTEXT INDEX ON [dbo].[DiwanPoems]( /// [PlainText] LANGUAGE 'English') /// KEY INDEX [PK_DiwanPoems]ON ([DiwanPoemPlainTextCatalog], FILEGROUP [PRIMARY]) /// WITH (CHANGE_TRACKING = AUTO, STOPLIST = SYSTEM) /// /// /// /// /// /// /// public async Task> Search(PagingParameterModel paging, string term, int? poetId, int? catId, int[] exceptPoetId) { term = term.Trim().ApplyCorrectYeKe(); if (string.IsNullOrEmpty(term)) { return new RServiceResult<(PaginationMetadata PagingMeta, DiwanPoemCompleteViewModel[] Items)>((null, null), "خطای جستجوی عبارت خالی"); } term = term.Replace("‌", " ");//replace zwnj with space string[] searchPatterns = LanguageUtils.SearchLikePatterns(term); // diwan: LIKE instead of full-text if (poetId == null) { catId = null; } if (poetId != null && catId != null) { var cat = await _context.DiwanCategories.AsNoTracking().Where(c => c.Id == catId).SingleAsync(); if(cat.PoetId != poetId) { catId = null; } } if (poetId != null && catId == null) { var poetRes = await GetPoetById((int)poetId); if (!string.IsNullOrEmpty(poetRes.ExceptionString)) return new RServiceResult<(PaginationMetadata PagingMeta, DiwanPoemCompleteViewModel[] Items)>((null, null), poetRes.ExceptionString); catId = poetRes.Result.Cat.Id; } List catIdList = new List(); if (catId != null) { catIdList.Add((int)catId); await _populateCategoryChildren(_context, (int)catId, catIdList); } var poems = _context.DiwanPoems.AsQueryable(); foreach (var pattern in searchPatterns) poems = poems.Where(p => EF.Functions.Like(p.PlainText, pattern)); var source = poems .Where(p => (catId == null || catIdList.Contains(p.CatId)) && (exceptPoetId.Length == 0 || !exceptPoetId.Contains(p.Cat.PoetId)) ) .Include(p => p.Cat).ThenInclude(c => c.Poet) .OrderBy(p => p.Cat.Poet.BirthYearInLHijri).ThenBy(p => p.Cat.Poet.Nickname).ThenBy(p => p.Id) .Select ( poem => new DiwanPoemCompleteViewModel() { Id = poem.Id, Title = poem.Title, FullTitle = poem.FullTitle, FullUrl = poem.FullUrl, UrlSlug = poem.UrlSlug, HtmlText = poem.HtmlText, PlainText = poem.PlainText, MixedModeOrder = poem.MixedModeOrder, Published = poem.Published, Language = poem.Language, PoemSummary = poem.PoemSummary, Category = new DiwanPoetCompleteViewModel() { Poet = new DiwanPoetViewModel() { Id = poem.Cat.Poet.Id, } }, ClaimedByMultiplePoets = poem.ClaimedByMultiplePoets, } ).AsNoTracking(); (PaginationMetadata PagingMeta, DiwanPoemCompleteViewModel[] Items) paginatedResult = await QueryablePaginator.Paginate(source, paging); Dictionary cachedPoets = new Dictionary(); foreach (var item in paginatedResult.Items) { if (cachedPoets.TryGetValue(item.Category.Poet.Id, out DiwanPoetCompleteViewModel poet)) { item.Category = poet; } else { poet = (await GetPoetById(item.Category.Poet.Id)).Result; cachedPoets.Add(item.Category.Poet.Id, poet); item.Category = poet; } } return new RServiceResult<(PaginationMetadata PagingMeta, DiwanPoemCompleteViewModel[] Items)>(paginatedResult); } private async Task _UpdatePageChildrenTitleAndUrl(RMuseumDbContext context, DiwanPage dbPage, bool messWithTitles, bool messWithUrls) { var children = await context.DiwanPages.Where(p => p.ParentId == dbPage.Id).ToListAsync(); foreach (var child in children) { child.FullUrl = dbPage.FullUrl + "/" + child.UrlSlug; child.FullTitle = dbPage.FullTitle + " » " + child.Title; switch (child.DiwanPageType) { case DiwanPageType.PoemPage: { DiwanPoem poem = await context.DiwanPoems.Where(p => p.Id == child.Id).SingleAsync(); if (messWithTitles) poem.FullTitle = child.FullTitle; if (messWithUrls) poem.FullUrl = child.FullUrl; context.DiwanPoems.Update(poem); } break; case DiwanPageType.CatPage: { if (messWithUrls) { DiwanCat cat = await context.DiwanCategories.Where(c => c.Id == child.CatId).SingleAsync(); cat.FullUrl = child.FullUrl; context.DiwanCategories.Update(cat); } } break; } await _UpdatePageChildrenTitleAndUrl(context, child, messWithTitles, messWithUrls); CacheCleanForPageByUrl(child.FullUrl); } context.DiwanPages.UpdateRange(children); await context.SaveChangesAsync(); } /// /// modify page /// /// /// /// /// public async Task> UpdatePageAsync(int id, Guid editingUserId, DiwanModifyPageViewModel pageData) { return await _UpdatePageAsync(_context, id, editingUserId, pageData, true); } /// /// modify poem => only these fields: NoIndex, RedirectFromFullUrl, MixedModeOrder /// /// /// /// /// public async Task> UpdatePoemAsync(int id, Guid editingUserId, DiwanModifyPageViewModel pageData) { return await _UpdatePoemAsync(_context, id, editingUserId, pageData, true); } /// /// break a poem from a verse forward /// /// /// /// /// public async Task> BreakPoemAsync(int poemId, int vOrder, Guid userId) { var poem = (await GetPoemById(poemId, true, false, true, false, false, false, false, true, true)).Result; var parentPage = await _context.DiwanPages.AsNoTracking().Where(p => p.DiwanPageType == DiwanPageType.CatPage && p.CatId == poem.Category.Cat.Id).SingleOrDefaultAsync(); if(parentPage == null) { parentPage = await _context.DiwanPages.AsNoTracking().Where(p => p.DiwanPageType == DiwanPageType.PoetPage && p.CatId == poem.Category.Cat.Id).SingleAsync(); } var poemTitleStaticPart = "شمارهٔ"; if(!poem.Title.Contains(poemTitleStaticPart)) { poemTitleStaticPart = "بخش"; } if (poem.Next == null) { return await _BreakLastPoemInItsCategoryAsync(_context, poemId, vOrder, userId, poem, parentPage, poemTitleStaticPart); } _backgroundTaskQueue.QueueBackgroundWorkItem ( async token => { using (RMuseumDbContext context = new RMuseumDbContext(new DbContextOptions())) //this is long running job, so _context might be already been freed/collected by GC { LongRunningJobProgressServiceEF jobProgressServiceEF = new LongRunningJobProgressServiceEF(context); var job = (await jobProgressServiceEF.NewJob($"Breaking poem {poem.FullTitle}", "Query data")).Result; try { var res = await _BreakPoemAsync(context, poemId, vOrder, userId, poem, parentPage, poemTitleStaticPart); if (!string.IsNullOrEmpty(res.ExceptionString)) { await jobProgressServiceEF.UpdateJob(job.Id, 100, "", false, res.ExceptionString); return; } await jobProgressServiceEF.UpdateJob(job.Id, 100, "", true); } catch (Exception exp) { await jobProgressServiceEF.UpdateJob(job.Id, 100, "", false, exp.ToString()); } } }); return new RServiceResult(-1); } /// /// update related sections /// /// /// public void UpdateRelatedSections(int metreId, string rhyme) { if (string.IsNullOrEmpty(rhyme)) return; if (metreId <= 0) return; _backgroundTaskQueue.QueueBackgroundWorkItem ( async token => { using (RMuseumDbContext inlineContext = new RMuseumDbContext(new DbContextOptions())) //this is long running job, so context might be already been freed/collected by GC { LongRunningJobProgressServiceEF jobProgressServiceEF = new LongRunningJobProgressServiceEF(inlineContext); var job = (await jobProgressServiceEF.NewJob($"بازسازی فهرست بخش‌های مرتبط", $"M: {metreId}, G: {rhyme}")).Result; try { await _UpdateRelatedSections(inlineContext, metreId, rhyme); await inlineContext.SaveChangesAsync(); await jobProgressServiceEF.UpdateJob(job.Id, 100, "", true); } catch (Exception exp) { await jobProgressServiceEF.UpdateJob(job.Id, 100, "", false, exp.ToString()); } } }); } /// /// return page modifications history /// /// /// public async Task> GetOlderVersionsOfPage(int pageId) { return new RServiceResult ( await _context.DiwanPageSnapshots.AsNoTracking() .Where(s => s.DiwanPageId == pageId) .OrderByDescending(s => s.RecordDate) .Select ( s => new DiwanPageSnapshotSummaryViewModel() { Id = s.Id, RecordDate = s.RecordDate, Note = s.Note } ) .ToArrayAsync() ); } /// /// get old version /// /// /// public async Task> GetOldVersionOfPage(int id) { return new RServiceResult ( await _context.DiwanPageSnapshots.AsNoTracking() .Where(s => s.Id == id) .Select ( s => new DiwanModifyPageViewModel() { HtmlText = s.HtmlText, Note = s.Note, OldTag = s.OldTag, OldTagPageUrl = s.OldTagPageUrl, RhymeLetters = s.RhymeLetters, Rhythm = s.Rhythm, SourceName = s.SourceName, SourceUrlSlug = s.SourceUrlSlug, Title = s.Title, UrlSlug = s.UrlSlug } ) .SingleOrDefaultAsync() ); } /// /// returns metre list (ordered by Rhythm) /// /// /// public async Task> GetDiwanMetres(bool sortOnVerseCount = false) { return new RServiceResult( sortOnVerseCount ? await _context.DiwanMetres.OrderByDescending(m => m.VerseCount).AsNoTracking().ToArrayAsync() : await _context.DiwanMetres.OrderBy(m => m.Rhythm).AsNoTracking().ToArrayAsync() ); } private void CleanPoetCache(int poetId) { //cache clean: _memoryCache.Remove($"/api/diwan/poets?published={true}&includeBio={false}"); _memoryCache.Remove($"/api/diwan/poets?published={false}&includeBio={true}"); _memoryCache.Remove("diwan/poets"); _memoryCache.Remove($"/api/diwan/poet/{poetId}"); _memoryCache.Remove($"poet/byid/{poetId}"); } /// /// modify poet /// /// /// /// public async Task> UpdatePoetAsync(DiwanPoetViewModel poet, Guid editingUserId) { var dbPoet = await _context.DiwanPoets.Where(p => p.Id == poet.Id).SingleAsync(); var dbPoetPage = await _context.DiwanPages.Where(page => page.PoetId == poet.Id && page.DiwanPageType == DiwanPageType.PoetPage).SingleAsync(); if (string.IsNullOrEmpty(poet.Nickname)) { poet.Nickname = dbPoet.Nickname; poet.Published = dbPoet.Published; } if (string.IsNullOrEmpty(poet.FullUrl)) poet.FullUrl = dbPoetPage.FullUrl; if (string.IsNullOrEmpty(poet.Name)) poet.Name = dbPoet.Name; if (string.IsNullOrEmpty(poet.Description)) poet.Description = dbPoet.Description; if (dbPoet.Nickname != poet.Nickname || dbPoetPage.FullUrl != poet.FullUrl) { var resPageEdit = await UpdatePageAsync ( dbPoetPage.Id, editingUserId, new DiwanModifyPageViewModel() { Title = poet.Nickname, HtmlText = dbPoetPage.HtmlText, Note = "ویرایش مستقیم مشخصات سخنور", UrlSlug = poet.FullUrl.Substring(1), } ); if (!string.IsNullOrEmpty(resPageEdit.ExceptionString)) new RServiceResult(false, resPageEdit.ExceptionString); dbPoet.Nickname = poet.Nickname; } dbPoet.Name = poet.Name; dbPoet.Description = poet.Description; bool publishedChange = dbPoet.Published != poet.Published; dbPoet.Published = poet.Published; dbPoet.BirthYearInLHijri = poet.BirthYearInLHijri; dbPoet.ValidBirthDate = poet.ValidBirthDate; dbPoet.DeathYearInLHijri = poet.DeathYearInLHijri; dbPoet.ValidDeathDate = poet.ValidDeathDate; dbPoet.PinOrder = poet.PinOrder; dbPoet.BirthLocationId = string.IsNullOrEmpty(poet.BirthPlace) ? null : (await _context.DiwanGeoLocations.Where(l => l.Name == poet.BirthPlace).SingleAsync()).Id; dbPoet.DeathLocationId = string.IsNullOrEmpty(poet.DeathPlace) ? null : (await _context.DiwanGeoLocations.Where(l => l.Name == poet.DeathPlace).SingleAsync()).Id; _context.DiwanPoets.Update(dbPoet); await _context.SaveChangesAsync(); if (publishedChange) { var pages = await _context.DiwanPages.Where(p => p.PoemId == poet.Id).ToListAsync(); foreach (var page in pages) { page.Published = poet.Published; } _context.DiwanPages.UpdateRange(pages); await _context.SaveChangesAsync(); } CleanPoetCache(poet.Id); return new RServiceResult(true); } /// /// create new poet /// /// /// /// public async Task> AddPoetAsync(DiwanPoetViewModel poet, Guid editingUserId) { if (await _context.DiwanPoets.Where(p => p.Nickname == poet.Nickname || p.Name == poet.Name).AnyAsync()) { return new RServiceResult(null, "conflicting poet"); } if (await _context.DiwanCategories.Where(c => c.FullUrl == poet.FullUrl).AnyAsync()) { return new RServiceResult(null, "conflicting cat"); } if (await _context.DiwanPages.Where(p => p.FullUrl == poet.FullUrl).AnyAsync()) { return new RServiceResult(null, "conflicting page"); } if (poet.FullUrl.IndexOf('/') != 0) { return new RServiceResult(null, "Invalid FullUrl, it must start with /"); } if (poet.FullUrl.Substring(1).IndexOf('/') >= 0) { return new RServiceResult(null, "Invalid FullUrl, it must contain only one /"); } var id = 1 + await _context.DiwanPoets.MaxAsync(p => p.Id); for (int i = 2; i < id; i++) { if (!(await _context.DiwanPoets.Where(p => p.Id == i).AnyAsync())) { id = i; break; } } if (string.IsNullOrEmpty(poet.Description)) poet.Description = ""; DiwanPoet dbPoet = new DiwanPoet() { Id = id, Name = poet.Name, Nickname = poet.Nickname, Description = poet.Description, Published = poet.Published, BirthYearInLHijri = poet.BirthYearInLHijri, ValidBirthDate = poet.ValidBirthDate, DeathYearInLHijri = poet.DeathYearInLHijri, ValidDeathDate = poet.ValidDeathDate, PinOrder = poet.PinOrder, BirthLocationId = string.IsNullOrEmpty(poet.BirthPlace) ? null : (await _context.DiwanGeoLocations.Where(l => l.Name == poet.BirthPlace).SingleAsync()).Id, DeathLocationId = string.IsNullOrEmpty(poet.DeathPlace) ? null : (await _context.DiwanGeoLocations.Where(l => l.Name == poet.DeathPlace).SingleAsync()).Id }; _context.DiwanPoets.Add(dbPoet); var poetCatId = 1 + await _context.DiwanCategories.MaxAsync(c => c.Id); DiwanCat dbCat = new DiwanCat() { Id = poetCatId, PoetId = id, Title = poet.Nickname, UrlSlug = poet.FullUrl.Substring(1), FullUrl = poet.FullUrl, TableOfContentsStyle = DiwanTOC.Analyse, Published = true, }; _context.DiwanCategories.Add(dbCat); var poetPageId = 1 + await _context.DiwanPages.MaxAsync(p => p.Id); while (await _context.DiwanPoems.Where(p => p.Id == poetPageId).AnyAsync()) poetPageId++; var pageText = ""; foreach (var line in poet.Description.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries)) { pageText += $"

{line}

{Environment.NewLine}"; } DiwanPage dbPage = new DiwanPage() { Id = poetPageId, DiwanPageType = DiwanPageType.PoetPage, Published = poet.Published, PageOrder = -1, Title = poet.Nickname, FullTitle = poet.Nickname, UrlSlug = poet.FullUrl.Substring(1), FullUrl = poet.FullUrl, HtmlText = pageText, PoetId = id, CatId = poetCatId, PostDate = DateTime.Now }; _context.DiwanPages.Add(dbPage); DiwanPageSnapshot snapshot = new DiwanPageSnapshot() { DiwanPageId = poetPageId, MadeObsoleteByUserId = editingUserId, RecordDate = DateTime.Now, Note = "ایجاد سخنور", Title = dbPage.Title, UrlSlug = dbPage.UrlSlug, HtmlText = dbPage.HtmlText, }; _context.DiwanPageSnapshots.Add(snapshot); await _context.SaveChangesAsync(); CleanPoetCache(0); return await GetPoetById(id); } /// /// delete poet /// /// /// public RServiceResult StartDeletePoet(int id) { _backgroundTaskQueue.QueueBackgroundWorkItem ( async token => { using (RMuseumDbContext context = new RMuseumDbContext(new DbContextOptions())) //this is long running job, so _context might be already been freed/collected by GC { LongRunningJobProgressServiceEF jobProgressServiceEF = new LongRunningJobProgressServiceEF(context); var job = (await jobProgressServiceEF.NewJob($"Deleting Poet {id}", "Query data")).Result; try { var pages = await context.DiwanPages.Where(p => p.PoetId == id).ToListAsync(); context.DiwanPages.RemoveRange(pages); await jobProgressServiceEF.UpdateJob(job.Id, 50, "Deleting page and Querying the poet - if no progress unplublish and regen group by centuries"); var poet = await context.DiwanPoets.Where(p => p.Id == id).SingleAsync(); context.DiwanPoets.Remove(poet); await jobProgressServiceEF.UpdateJob(job.Id, 99, "Deleting poet"); await context.SaveChangesAsync(); await jobProgressServiceEF.UpdateJob(job.Id, 100, "", true); } catch (Exception exp) { await jobProgressServiceEF.UpdateJob(job.Id, 100, "", false, exp.ToString()); } } }); return new RServiceResult(true); } /// /// delete a page /// /// /// public async Task> DeletePageAsync(int id) { var firstChild = await _context.DiwanPages.Where(p => p.ParentId == id).FirstOrDefaultAsync(); if (firstChild != null) { return new RServiceResult(false, "Please delete children of the page first."); } var page = await _context.DiwanPages.Where(p => p.Id == id).SingleAsync(); if (page.PoemId != null) { return new RServiceResult(false, "Poem related pages can not be deleted."); } var cat = await _context.DiwanCategories.Where(c => c.FullUrl == page.FullUrl).FirstOrDefaultAsync(); if (cat != null) { return new RServiceResult(false, "Category related pages can not be deleted."); } _context.DiwanPages.Remove(page); await _context.SaveChangesAsync(); return new RServiceResult(true); } /// /// delete a poem /// /// /// public async Task> DeletePoemAsync(int id) { try { var poem = await _context.DiwanPoems.Where(p => p.Id == id).SingleAsync(); var comments = await _context.DiwanComments.Where(c => c.PoemId == id).ToListAsync(); _context.RemoveRange(comments); var music = await _context.DiwanPoemMusicTracks.Where(m => m.PoemId == id).ToListAsync(); _context.RemoveRange(music); //these lines cause timeout, so I commented them: /* var similars = await _context.DiwanCachedRelatedSections.Where(s => s.FullUrl.Contains(poem.FullUrl)).ToListAsync(); _context.RemoveRange(similars); */ var corrections = await _context.DiwanPoemCorrections.Include(c => c.VerseOrderText).Where(c => c.PoemId == id).ToListAsync(); _context.RemoveRange(corrections); var page = await _context.DiwanPages.Where(p => p.Id == id && p.DiwanPageType == DiwanPageType.PoemPage).SingleOrDefaultAsync(); if(page != null) { _context.Remove(page); } _context.Remove(poem); await _context.SaveChangesAsync(); return new RServiceResult(true); } catch (Exception exp) { return new RServiceResult(false, exp.ToString()); } } /// /// delete a category /// /// /// public async Task> DeleteCategoryAsync(int id) { try { var poems = await _context.DiwanPoems.Where(p => p.CatId == id).ToListAsync(); foreach (var poem in poems) { var res = await DeletePoemAsync(poem.Id); if (!string.IsNullOrEmpty(res.ExceptionString)) return res; } var subCats = await _context.DiwanCategories.Where(c => c.ParentId == id).ToListAsync(); foreach (var subCat in subCats) { var res = await DeleteCategoryAsync(subCat.Id); if (!string.IsNullOrEmpty(res.ExceptionString)) return res; } var page = await _context.DiwanPages.Where(p => p.DiwanPageType == DiwanPageType.CatPage && p.CatId == id).SingleAsync(); _context.Remove(page); await _context.SaveChangesAsync(); var cat = await _context.DiwanCategories.Where(c => c.Id == id).SingleAsync(); _context.Remove(cat); await _context.SaveChangesAsync(); return new RServiceResult(true); } catch (Exception exp) { return new RServiceResult(false, exp.ToString()); } } /// /// chaneg poet image /// /// /// /// public async Task> ChangePoetImageAsync(int poetId, Guid imageId) { try { RServiceResult img = await _imageFileService.GetImage(imageId); if (!string.IsNullOrEmpty(img.ExceptionString)) { return new RServiceResult(false, img.ExceptionString); } if (bool.Parse(Configuration.GetSection("ExternalFTPServer")["UploadEnabled"])) { var ftpClient = new AsyncFtpClient ( Configuration.GetSection("ExternalFTPServer")["Host"], Configuration.GetSection("ExternalFTPServer")["Username"], Configuration.GetSection("ExternalFTPServer")["Password"] ); ftpClient.ValidateCertificate += FtpClient_ValidateCertificate; await ftpClient.AutoConnect(); ftpClient.Config.RetryAttempts = 3; RServiceResult imgPath = _imageFileService.GetImagePath(img.Result); if (!string.IsNullOrEmpty(imgPath.ExceptionString)) return new RServiceResult(false, imgPath.ExceptionString); var localFilePath = imgPath.Result; var remoteFilePath = $"{Configuration.GetSection("ExternalFTPServer")["RootPath"]}/images/PoetImages/{Path.GetFileName(localFilePath)}"; await ftpClient.UploadFile(localFilePath, remoteFilePath); await ftpClient.Disconnect(); } if (bool.Parse(Configuration.GetSection("BackupFTPServer")["UploadEnabled"])) { var ftpClient = new AsyncFtpClient ( Configuration.GetSection("BackupFTPServer")["Host"], Configuration.GetSection("BackupFTPServer")["Username"], Configuration.GetSection("BackupFTPServer")["Password"] ); ftpClient.ValidateCertificate += FtpClient_ValidateCertificate; await ftpClient.AutoConnect(); ftpClient.Config.RetryAttempts = 3; RServiceResult imgPath = _imageFileService.GetImagePath(img.Result); if (!string.IsNullOrEmpty(imgPath.ExceptionString)) return new RServiceResult(false, imgPath.ExceptionString); var localFilePath = imgPath.Result; var remoteFilePath = $"{Configuration.GetSection("BackupFTPServer")["RootPath"]}/images/PoetImages/{Path.GetFileName(localFilePath)}"; await ftpClient.UploadFile(localFilePath, remoteFilePath); await ftpClient.Disconnect(); } var dbPoet = await _context.DiwanPoets.Where(p => p.Id == poetId).SingleAsync(); dbPoet.RImageId = imageId; _context.DiwanPoets.Update(dbPoet); await _context.SaveChangesAsync(); return new RServiceResult(true); } catch (Exception exp) { return new RServiceResult(false, exp.ToString()); } } private void FtpClient_ValidateCertificate(FluentFTP.Client.BaseClient.BaseFtpClient control, FtpSslValidationEventArgs e) { e.Accept = true; } /// /// find poem rhyme /// /// /// public async Task> FindPoemMainSectionRhyme(int id) { var section = await _context.DiwanPoemSections.AsNoTracking().Where(s => s.PoemId == id && s.SectionType == PoemSectionType.WholePoem && s.VerseType == VersePoemSectionType.First).OrderBy(s => s.Index).FirstOrDefaultAsync(); if (section == null) return new RServiceResult(null, "no sections"); return await FindSectionRhyme(section.Id); } /// /// find poem section rhyme /// /// /// public async Task> FindSectionRhyme(int id) { return await _FindSectionRhyme(_context, id); } private async Task> _FindSectionRhyme(RMuseumDbContext context, int id) { var section = await context.DiwanPoemSections.Include(s => s.DiwanMetre).AsNoTracking().Where(s => s.Id == id).FirstOrDefaultAsync(); if (section == null) return new RServiceResult(null, "no sections"); var verses = await context.DiwanVerses.AsNoTracking().Where(v => v.PoemId == section.PoemId).OrderBy(v => v.VOrder).ToListAsync(); var rhymeAnalysisResult = LanguageUtils.FindRhyme(FilterSectionVerses(section, verses)); if (rhymeAnalysisResult.Rhyme.Length > 30 && verses.Count == 2 && section.DiwanMetre != null)//single verse { var rhymingSection = await context.DiwanPoemSections.AsNoTracking() .Where(s => s.DiwanMetreId == section.DiwanMetreId && section.RhymeLetters != null && s.RhymeLetters.Length < 15 && rhymeAnalysisResult.Rhyme.Contains(s.RhymeLetters)) .OrderByDescending(s => s.RhymeLetters.Length) .FirstOrDefaultAsync(); if (rhymingSection != null) { rhymeAnalysisResult.Rhyme = rhymingSection.RhymeLetters; } } return new RServiceResult(rhymeAnalysisResult); } /// /// find poem rhythm /// /// /// public async Task> FindPoemMainSectionRhythm(int id) { var metres = (await GetDiwanMetres()).Result.Select(m => m.Rhythm).ToArray(); return await _FindPoemMainSectionRhythm(id, _context, _httpClient, metres); } private async Task> _FindPoemMainSectionRhythm(int id, RMuseumDbContext context, HttpClient httpClient, string[] metres, bool alwaysReturnaAResult = false) { var section = await context.DiwanPoemSections.AsNoTracking().Where(s => s.PoemId == id && s.SectionType == PoemSectionType.WholePoem && s.VerseType == VersePoemSectionType.First).OrderBy(s => s.Index).FirstOrDefaultAsync(); if (section == null) return new RServiceResult(null, "no main sections"); return await _FindSectionRhythm(section, context, httpClient, metres, alwaysReturnaAResult); } private async Task> _FindSectionRhythm(DiwanPoemSection section, RMuseumDbContext context, HttpClient httpClient, string[] metres, bool alwaysReturnaAResult = false) { try { var poemVerses = await context.DiwanVerses.AsNoTracking().Where(v => v.PoemId == section.PoemId).OrderBy(v => v.VOrder).ToListAsync(); var verses = FilterSectionVerses(section, poemVerses); if (verses.Any(v => v.VersePosition == VersePosition.Paragraph)) { return new RServiceResult("paragraph"); } Dictionary rhytmCounter = new Dictionary(); for (int i = 0; i < verses.Count; i++) { var verse = verses[i]; try { var response = await httpClient.GetAsync($"http://sorud.info/?Text={HttpUtility.UrlEncode(LanguageUtils.MakeTextSearchable(verse.Text))}"); response.EnsureSuccessStatusCode(); string result = await response.Content.ReadAsStringAsync(); if (result.IndexOf("آهنگِ همه‌ی بندها شناسایی نشد.") != -1) { continue; } int nVaznIndex = result.IndexOf("ctl00_MainContent_lblMetricBottom"); if (nVaznIndex == -1) { continue; } nVaznIndex += 2; int nQuote1Index = result.IndexOf('\'', nVaznIndex); if (nQuote1Index == -1) { continue; } int nQuote2Index = result.IndexOf('\'', nQuote1Index + 1); if (nQuote2Index == -1) { continue; } string strRokn = result.Substring(nQuote1Index + 1, nQuote2Index - nQuote1Index - 1); int nSpanClose = result.IndexOf("", nQuote2Index + 1); if (nSpanClose == -1) { continue; } int nFQ1 = result.IndexOf('«', nQuote2Index + 1); if (nFQ1 == -1) { continue; } string rhythm = metres.Where(m => m.IndexOf(strRokn) == 0).SingleOrDefault(); if (string.IsNullOrEmpty(rhythm)) continue; if (rhythm == "فاعلاتن فاعلن فاعلاتن فاعلن") rhythm = "فاعلاتن فاعلاتن فاعلاتن فاعلن (رمل مثمن محذوف)"; if (rhythm == "مفاعلتن مفاعلتن مفاعلتن مفاعلتن") rhythm = "مفاعیلن مفاعیلن مفاعیلن مفاعیلن (هزج مثمن سالم)"; if (rhythm == "فاعلات مفعولن فاعلات مفعولن") rhythm = "فاعلن مفاعیلن فاعلن مفاعیلن (مقتضب مثمن مطوی مقطوع)"; if (rhytmCounter.TryGetValue(rhythm, out int count)) { count++; if (count > 10 || (count * 100.0 / verses.Count > 60)) { return new RServiceResult(rhythm); } } else { count = 1; } rhytmCounter[rhythm] = count; } catch { //continue } } if (alwaysReturnaAResult) { int maxCount = -1; string rhytm = ""; foreach (var r in rhytmCounter) { if (r.Value > maxCount) { maxCount = r.Value; rhytm = r.Key; } } if (!string.IsNullOrEmpty(rhytm)) return new RServiceResult(rhytm); if (verses.Count < 9) return new RServiceResult("paragraph"); } return new RServiceResult(""); } catch (Exception exp) { return new RServiceResult(null, exp.ToString()); } } /// /// manually add a duplicate for a poems /// /// /// /// /// public async Task> AdDuplicateAsync(int srcCatId, int srcPoemId, int destPoemId) { try { var alreadyDup = await _context.DiwanDuplicates.AsNoTracking().Where(p => p.SrcPoemId == srcPoemId).FirstOrDefaultAsync(); if (alreadyDup != null) { return new RServiceResult(false, $"already dupped : {alreadyDup.DestPoemId}"); } var dup = new DiwanDuplicate() { SrcCatId = srcCatId, SrcPoemId = srcPoemId, DestPoemId = destPoemId }; _context.DiwanDuplicates.Add(dup); await _context.SaveChangesAsync(); return new RServiceResult(true); } catch (Exception exp) { return new RServiceResult(false, exp.ToString()); } } /// /// delete duplicate /// /// /// public async Task> DeleteDuplicateAsync(int id) { try { var dup = await _context.DiwanDuplicates.Where(d => d.Id == id).SingleAsync(); _context.Remove(dup); await _context.SaveChangesAsync(); return new RServiceResult(false); } catch (Exception exp) { return new RServiceResult(false, exp.ToString()); } } /// /// get category poem related images /// /// /// public async Task> GetCatPoemImagesAsync(int catId) { try { var museumSrc = from link in _context.DiwanLinks.Include(l => l.Artifact).Include(l => l.Item).ThenInclude(i => i.Images) join poem in _context.DiwanPoems on link.DiwanPostId equals poem.Id where link.DisplayOnPage == true && link.ReviewResult == ReviewResult.Approved && poem.CatId == catId orderby poem.Id select new PoemRelatedImageEx() { PoemRelatedImageType = PoemRelatedImageType.MuseumLink, ThumbnailImageUrl = link.Item.Images.First().ExternalNormalSizeImageUrl.Replace("/norm/", "/thumb/").Replace("/orig/", "/thumb/"), TargetPageUrl = link.LinkToOriginalSource ? link.OriginalSourceUrl : $"https://museum.ganjoor.net/items/{link.Artifact.FriendlyUrl}/{link.Item.FriendlyUrl}", AltText = $"{link.Artifact.Name} » {link.Item.Name}", IsTextOriginalSource = link.IsTextOriginalSource, PoemId = poem.Id, PoemFullUrl = poem.FullUrl, PoemFullTitle = poem.FullTitle, }; List museumImages = await museumSrc.ToListAsync(); var externalSrc = from link in _context.PinterestLinks join poem in _context.DiwanPoems on link.DiwanPostId equals poem.Id where link.ReviewResult == ReviewResult.Approved && poem.CatId == catId orderby poem.Id select new PoemRelatedImageEx() { PoemRelatedImageType = PoemRelatedImageType.ExternalLink, ThumbnailImageUrl = link.LinkType == LinkType.Naskban ? link.PinterestImageUrl : link.Item.Images.First().ExternalNormalSizeImageUrl.Replace("/norm/", "/thumb/").Replace("/orig/", "/thumb/"), TargetPageUrl = link.PinterestUrl, AltText = link.AltText, IsTextOriginalSource = link.IsTextOriginalSource, PoemId = poem.Id, PoemFullUrl = poem.FullUrl, PoemFullTitle = poem.FullTitle, }; museumImages.AddRange(await externalSrc.AsNoTracking().ToListAsync()); return new RServiceResult(museumImages.ToArray()); } catch (Exception exp) { return new RServiceResult(null, exp.ToString()); } } /// /// add page /// /// /// public async Task> AddPageAsync(DiwanPage page) { try { if (page.UrlSlug.Contains("/")) return new RServiceResult(null, "slug could not contain /"); page.UrlSlug = page.UrlSlug.Trim(); page.Title = page.Title.Trim(); if (string.IsNullOrEmpty(page.UrlSlug)) return new RServiceResult(null, "empty url"); if (string.IsNullOrEmpty(page.Title)) return new RServiceResult(null, "empty title"); var newPageId = 1 + await _context.DiwanPages.MaxAsync(p => p.Id); while (await _context.DiwanPoems.Where(p => p.Id == newPageId).AnyAsync()) newPageId++; page.Id = newPageId; if(page.ParentId != null) { var parentPage = await _context.DiwanPages.AsNoTracking().Where(p => p.Id == page.ParentId).SingleAsync(); page.FullUrl = parentPage.FullUrl + "/" + page.UrlSlug; page.FullTitle = parentPage.FullTitle + " » " + page.Title; } if (true == await _context.DiwanPages.Where(p => p.FullUrl == page.FullUrl).AnyAsync()) return new RServiceResult(null, "duplicated full url."); if (true == await _context.DiwanPages.Where(p => p.FullTitle == page.FullTitle).AnyAsync()) return new RServiceResult(null, "duplicated full title"); _context.Add(page); await _context.SaveChangesAsync(); return new RServiceResult(page); } catch (Exception exp) { return new RServiceResult(null, exp.ToString()); } } /// /// send cat correction /// /// /// public async Task> SuggestCatCorrectionAsync(DiwanCatCorrectionViewModel correction) { var preCorrections = await _context.DiwanCatCorrections .Where(c => c.UserId == correction.UserId && c.CatId == correction.CatId && c.Reviewed == false) .ToListAsync(); var cat = await _context.DiwanCategories.AsNoTracking().Where(c => c.Id == correction.CatId).SingleAsync(); var page = await _context.DiwanPages.AsNoTracking().Where(p => p.FullUrl == cat.FullUrl).SingleAsync(); DiwanCatCorrection dbCorrection = new DiwanCatCorrection() { CatId = correction.CatId, UserId = correction.UserId, DescriptionHtml = correction.DescriptionHtml, Description = correction.Description, OriginalDescription = cat.Description, OriginalDescriptionHtml = cat.DescriptionHtml, Note = correction.Note, Date = DateTime.Now, Result = CorrectionReviewResult.NotReviewed, Reviewed = false, AffectedTheCat = false, HideMyName = correction.HideMyName, PageId = page.Id, }; _context.DiwanCatCorrections.Add(dbCorrection); await _context.SaveChangesAsync(); correction.Id = dbCorrection.Id; if (preCorrections.Count > 0) { _context.DiwanCatCorrections.RemoveRange(preCorrections); await _context.SaveChangesAsync(); } var moderators = await _appUserService.GetUsersHavingPermission(RMuseumSecurableItem.DiwanEntityShortName, RMuseumSecurableItem.ModerateOperationShortName); if (string.IsNullOrEmpty(moderators.ExceptionString)) //if not, do nothing! { foreach (var moderator in moderators.Result) { await _notificationService.PushNotification ( (Guid)moderator.Id, "پیشنهاد متن بخش یا زندگینامه", $"کاربری ویرایشی را برای یک بخش یا زندگینامهٔ یک شاعر پیشنهاد داده است. لطفاً بخش ویرایش‌های بخش‌ها را بررسی فرمایید.{Environment.NewLine}" + $"توجه فرمایید که اگر کاربر دیگری که دارای مجوز بررسی ویرایش‌های بخش‌های پیشنهادی است پیش از شما به آن رسیدگی کرده باشد آن را در صف نخواهید دید.", NotificationType.ActionRequired ); } } return new RServiceResult(correction); } /// /// delete unreviewed user corrections for a cat /// /// /// /// public async Task> DeleteCatCorrectionsAsync(Guid userId, int catId) { var preCorrections = await _context.DiwanCatCorrections .Where(c => c.UserId == userId && c.CatId == catId && c.Reviewed == false) .ToListAsync(); if (preCorrections.Count > 0) { _context.DiwanCatCorrections.RemoveRange(preCorrections); await _context.SaveChangesAsync(); } return new RServiceResult(true); } /// /// last unreviewed user correction for a cat /// /// /// /// public async Task> GetLastUnreviewedUserCorrectionForCatAsync(Guid userId, int catId) { var dbCorrection = await _context.DiwanCatCorrections.AsNoTracking().Include(c => c.User) .Where(c => c.UserId == userId && c.CatId == catId && c.Reviewed == false) .OrderByDescending(c => c.Id) .FirstOrDefaultAsync(); if (dbCorrection == null) return new RServiceResult(null); return new RServiceResult ( new DiwanCatCorrectionViewModel() { Id = dbCorrection.Id, CatId = dbCorrection.CatId, UserId = dbCorrection.UserId, Description = dbCorrection.Description, DescriptionHtml = dbCorrection.DescriptionHtml, OriginalDescription = dbCorrection.OriginalDescription, OriginalDescriptionHtml = dbCorrection.OriginalDescriptionHtml, Note = dbCorrection.Note, Date = dbCorrection.Date, Reviewed = dbCorrection.Reviewed, Result = dbCorrection.Result, ReviewNote = dbCorrection.ReviewNote, ReviewDate = dbCorrection.ReviewDate, UserNickname = dbCorrection.HideMyName && dbCorrection.Reviewed ? "" : string.IsNullOrEmpty(dbCorrection.User.NickName) ? dbCorrection.User.Id.ToString() : dbCorrection.User.NickName, HideMyName = dbCorrection.HideMyName, PageId = dbCorrection.PageId, } ); } /// /// get user or all corrections for categories /// /// if sent empty returns all corrections /// /// public async Task> GetUserCatCorrectionsAsync(Guid userId, PagingParameterModel paging) { var source = from dbCorrection in _context.DiwanCatCorrections.AsNoTracking().Include(c => c.User) where userId == Guid.Empty || dbCorrection.UserId == userId orderby dbCorrection.Id descending select dbCorrection; (PaginationMetadata PagingMeta, DiwanCatCorrection[] Items) dbPaginatedResult = await QueryablePaginator.Paginate(source, paging); List list = new List(); foreach (var dbCorrection in dbPaginatedResult.Items) { list.Add ( new DiwanCatCorrectionViewModel() { Id = dbCorrection.Id, CatId = dbCorrection.CatId, UserId = dbCorrection.UserId, Description = dbCorrection.Description, DescriptionHtml = dbCorrection.DescriptionHtml, OriginalDescription = dbCorrection.OriginalDescription, OriginalDescriptionHtml = dbCorrection.OriginalDescriptionHtml, Note = dbCorrection.Note, Date = dbCorrection.Date, Reviewed = dbCorrection.Reviewed, Result = dbCorrection.Result, ReviewNote = dbCorrection.ReviewNote, ReviewDate = dbCorrection.ReviewDate, UserNickname = dbCorrection.HideMyName && dbCorrection.Reviewed ? "" : string.IsNullOrEmpty(dbCorrection.User.NickName) ? dbCorrection.User.Id.ToString() : dbCorrection.User.NickName, HideMyName = dbCorrection.HideMyName, PageId = dbCorrection.PageId, } ); } return new RServiceResult<(PaginationMetadata, DiwanCatCorrectionViewModel[])> ((dbPaginatedResult.PagingMeta, list.ToArray())); } /// /// cat effectinve corrections /// /// /// /// public async Task> GetCatEffectiveCorrectionsAsync(int catId, PagingParameterModel paging) { var source = from dbCorrection in _context.DiwanCatCorrections.AsNoTracking() where dbCorrection.CatId == catId && dbCorrection.Reviewed == true && dbCorrection.Result == CorrectionReviewResult.Approved orderby dbCorrection.Id descending select dbCorrection; (PaginationMetadata PagingMeta, DiwanCatCorrection[] Items) dbPaginatedResult = await QueryablePaginator.Paginate(source, paging); List list = new List(); foreach (var dbCorrection in dbPaginatedResult.Items) { list.Add ( new DiwanCatCorrectionViewModel() { Id = dbCorrection.Id, CatId = dbCorrection.CatId, UserId = dbCorrection.UserId, Description = dbCorrection.Description, DescriptionHtml = dbCorrection.DescriptionHtml, OriginalDescription = dbCorrection.OriginalDescription, OriginalDescriptionHtml = dbCorrection.OriginalDescriptionHtml, Note = dbCorrection.Note, Date = dbCorrection.Date, Reviewed = dbCorrection.Reviewed, Result = dbCorrection.Result, ReviewNote = dbCorrection.ReviewNote, ReviewDate = dbCorrection.ReviewDate, UserNickname = dbCorrection.HideMyName && dbCorrection.Reviewed ? "" : string.IsNullOrEmpty(dbCorrection.User.NickName) ? dbCorrection.User.Id.ToString() : dbCorrection.User.NickName, HideMyName = dbCorrection.HideMyName, PageId = dbCorrection.PageId, } ); } return new RServiceResult<(PaginationMetadata, DiwanCatCorrectionViewModel[])> ((dbPaginatedResult.PagingMeta, list.ToArray())); } /// /// get cat correction by id /// /// /// public async Task> GetCatCorrectionByIdAsync(int id) { var dbCorrection = await _context.DiwanCatCorrections.AsNoTracking().Include(c => c.User) .Where(c => c.Id == id) .FirstOrDefaultAsync(); if (dbCorrection == null) return new RServiceResult(null); return new RServiceResult ( new DiwanCatCorrectionViewModel() { Id = dbCorrection.Id, CatId = dbCorrection.CatId, UserId = dbCorrection.UserId, Description = dbCorrection.Description, DescriptionHtml = dbCorrection.DescriptionHtml, OriginalDescription = dbCorrection.OriginalDescription, OriginalDescriptionHtml = dbCorrection.OriginalDescriptionHtml, Note = dbCorrection.Note, Date = dbCorrection.Date, Reviewed = dbCorrection.Reviewed, Result = dbCorrection.Result, ReviewNote = dbCorrection.ReviewNote, ReviewDate = dbCorrection.ReviewDate, UserNickname = dbCorrection.HideMyName && dbCorrection.Reviewed ? "" : string.IsNullOrEmpty(dbCorrection.User.NickName) ? dbCorrection.User.Id.ToString() : dbCorrection.User.NickName, HideMyName = dbCorrection.HideMyName, PageId = dbCorrection.PageId, } ); } /// /// get next unreviewed cat correction /// /// /// /// public async Task> GetNextUnreviewedCatCorrectionAsync(int skip, bool onlyUserCorrections) { string systemEmail = $"{Configuration.GetSection("Diwan")["SystemEmail"]}"; var systemUser = await _appUserService.FindUserByEmail(systemEmail); var systemUserId = systemUser.Result == null ? Guid.Empty : (Guid)systemUser.Result.Id; var dbCorrection = await _context.DiwanCatCorrections.AsNoTracking().Include(c => c.User) .Where(c => c.Reviewed == false && (onlyUserCorrections == false || c.UserId != systemUserId)) .OrderBy(c => c.Id) .Skip(skip) .FirstOrDefaultAsync(); if (dbCorrection == null) return new RServiceResult(null); return new RServiceResult ( new DiwanCatCorrectionViewModel() { Id = dbCorrection.Id, CatId = dbCorrection.CatId, UserId = dbCorrection.UserId, Description = dbCorrection.Description, DescriptionHtml = dbCorrection.DescriptionHtml, OriginalDescription = dbCorrection.OriginalDescription, OriginalDescriptionHtml = dbCorrection.OriginalDescriptionHtml, Note = dbCorrection.Note, Date = dbCorrection.Date, Reviewed = dbCorrection.Reviewed, Result = dbCorrection.Result, ReviewNote = dbCorrection.ReviewNote, ReviewDate = dbCorrection.ReviewDate, UserNickname = dbCorrection.HideMyName && dbCorrection.Reviewed ? "" : string.IsNullOrEmpty(dbCorrection.User.NickName) ? dbCorrection.User.Id.ToString() : dbCorrection.User.NickName, HideMyName = dbCorrection.HideMyName, PageId = dbCorrection.PageId, } ); } /// /// unreviewed cat corrections count /// /// /// public async Task> GetUnreviewedCatCorrectionCountAsync(bool onlyUserCorrections) { string systemEmail = $"{Configuration.GetSection("Diwan")["SystemEmail"]}"; var systemUser = await _appUserService.FindUserByEmail(systemEmail); var systemUserId = systemUser.Result == null ? Guid.Empty : (Guid)systemUser.Result.Id; return new RServiceResult(await _context.DiwanCatCorrections.AsNoTracking() .Where(c => c.Reviewed == false && (onlyUserCorrections == false || c.UserId != systemUserId)) .CountAsync()); } /// /// aggressive cache /// public bool AggressiveCacheEnabled { get { try { return bool.Parse(Configuration["AggressiveCacheEnabled"]); } catch { return false; } } } /// /// Database Context /// protected readonly RMuseumDbContext _context; /// /// Configuration /// protected IConfiguration Configuration { get; } /// /// Background Task Queue Instance /// protected readonly IBackgroundTaskQueue _backgroundTaskQueue; /// /// IAppUserService instance /// protected IAppUserService _appUserService; /// /// Messaging service /// protected readonly IRNotificationService _notificationService; /// /// Image File Service /// protected readonly IImageFileService _imageFileService; /// /// IMemoryCache /// protected readonly IMemoryCache _memoryCache; /// /// http client /// protected readonly HttpClient _httpClient; /// /// options service /// protected readonly IRGenericOptionsService _optionsService; /// /// constructor /// /// /// /// /// /// /// /// /// /// public DiwanService(RMuseumDbContext context, IConfiguration configuration, IBackgroundTaskQueue backgroundTaskQueue, IAppUserService appUserService, IRNotificationService notificationService, IImageFileService imageFileService, IMemoryCache memoryCache, HttpClient httpClient, IRGenericOptionsService optionsService) { _context = context; _backgroundTaskQueue = backgroundTaskQueue; _appUserService = appUserService; _notificationService = notificationService; _imageFileService = imageFileService; _memoryCache = memoryCache; Configuration = configuration; _httpClient = httpClient; _optionsService = optionsService; } } }