divan/RMuseum/Services/Implementation/DivanAppUserService.cs
Anas Rashid 532d005cdb Rename Ganjoor -> Divan throughout the code
Projects (GanjooRazor -> DivanRazor, GanjoorService.sln -> DivanService.sln), files,
folders, classes, namespaces, settings, API routes (/api/divan) and DB tables (Divan*).
External addresses (ganjoor.net, github.com/ganjoor) unchanged. Migrations renamed
consistently (no pending model changes); requires a fresh database.
Upstream RUNNING_LOCALLY/SEMANTIC_SEARCH docs archived unmodified under docs/.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 23:49:01 +02:00

576 lines
27 KiB
C#

using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Configuration;
using RMuseum.DbContext;
using RMuseum.Models.Divan;
using RMuseum.Models.Divan.ViewModels;
using RMuseum.Models.DivanAudio;
using RMuseum.Models.DivanAudio.ViewModels;
using RMuseum.Models.Note;
using RSecurityBackend.Models.Auth.Db;
using RSecurityBackend.Models.Auth.ViewModels;
using RSecurityBackend.Models.Generic;
using RSecurityBackend.Services;
using RSecurityBackend.Services.Implementation;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace RMuseum.Services.Implementation
{
/// <summary>
/// Divan User Service
/// </summary>
public class DivanAppUserService : AppUserService
{
public DivanAppUserService(
RMuseumDbContext context,
UserManager<RAppUser> userManager,
SignInManager<RAppUser> signInManager,
RoleManager<RAppRole> roleManager,
ISecretGenerator secretGenerator,
IImageFileService imageFileService,
IUserRoleService userRoleService,
IConfiguration configuration,
IMemoryCache memoryCache,
IEmailSender emailSender
)
: base(context, userManager, signInManager, roleManager, secretGenerator, imageFileService, userRoleService, configuration, emailSender)
{
_memoryCache = memoryCache;
}
/// <summary>
/// IMemoryCache
/// </summary>
protected readonly IMemoryCache _memoryCache;
/// <summary>
/// Divan Service
/// </summary>
protected IDivanService _divanService { get; set; }
/// <summary>
/// Sign Up Email Subject
/// </summary>
/// <returns>
/// subject
/// </returns>
/// <param name="op"></param>
/// <param name="secretCode"></param>
public override string GetEmailSubject(RVerifyQueueType op, string secretCode)
{
string opString =
op == RVerifyQueueType.SignUp ? "نام‌نویسی"
:
op == RVerifyQueueType.ForgotPassword
?
"بازیابی کلمهٔ عبور"
:
"حذف حساب کاربری";
return $"{secretCode} کد {opString} شما در گنجور";
}
/// <summary>
/// Sign Up Email Html Content
/// </summary>
/// <param name="op"></param>
/// <param name="secretCode"></param>
/// <param name="signupCallbackUrl"></param>
/// <returns>html content</returns>
public override string GetEmailHtmlContent(RVerifyQueueType op, string secretCode, string signupCallbackUrl)
{
string opString = op == RVerifyQueueType.SignUp ? "نام‌نویسی" : op == RVerifyQueueType.ForgotPassword ? "بازیابی کلمهٔ عبور" : "حذف حساب کاربری";
string ifNot = op == RVerifyQueueType.SignUp ? "اگر در گنجور نام‌نویسی نکرده‌اید لطفاً این نامه را نادیده بگیرید."
: op == RVerifyQueueType.ForgotPassword ?
"اگر در گنجور فراموشی گذرواژه را نزده‌اید یا گذرواژه‌تان را به خاطر آوردید لطفاً این نامه را نادیده بگیرید."
:
op == RVerifyQueueType.UserSelfDelete ?
"اگر در گنجور حذف حساب کاربری را نزده‌اید یا از حذف حساب کاربریتان منصرف شده‌اید لطفاً این نامه را نادیده بگیرید."
:
"";
string content =
"<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">"
+
"<html xmlns=\"http://www.w3.org/1999/xhtml\" xml:lang=\"en\" lang=\"fa\">"
+
"<head>"
+
"<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />"
+
(op == RVerifyQueueType.KickOutUser ? $"<title>حذف حساب کاربری شما در گنجور</title>" : $" <title>کد {opString} شما در گنجور: {secretCode}</title>")
+
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>"
+
"</head>"
+
"<body style=\"font:normal 12px tahoma;direction:rtl\">"
+
"<table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"600\">"
+
"<tr>"
+
"<td align=\"center\" style=\"padding: 40px 0 30px 0;\">"
+
"<img src=\"https://i.ganjoor.net/gm.gif\" alt=\"گنجور\" width=\"150\" height=\"150\" style=\"display: block;\" />"
+
"</td>"
+
"</tr>"
+
"<tr><td>"
+
(
op == RVerifyQueueType.KickOutUser ?
$"<p style=\"font:normal 12px tahoma;direction:rtl\">کاربر گرامی، متأسفیم که به اطلاع برسانیم که به دلیل نقض قوانین استفاده از گنجور و به طور مشخص {secretCode} حساب کاربری شما به همراه حاشیه‌ها، خوانش‌ها و سایر اطلاعات خصوصیتان از گنجور حذف شده است. امیدواریم در آینده در صورت تمایل به استفاده از گنجور در چارچوب‌های قابل پذیرش برای ما با حساب کاربری جدیدی پذیرای شما باشیم. با این ایمیل امکان نام‌نویسی مجدد نخواهید داشت.</p>"
:
string.IsNullOrEmpty(signupCallbackUrl) ?
$"<p style=\"font:normal 12px tahoma;direction:rtl\">لطفاً جهت تکمیل {opString} در گنجور کد <strong>{secretCode}</strong> را به عنوان رمز دریافتی در صفحهٔ {opString} وارد کنید.</p>"
:
$"<p style=\"font:normal 12px tahoma;direction:rtl\">لطفاً جهت تکمیل {opString} در گنجور <a href=\"{signupCallbackUrl}?secret={secretCode}\">اینجا</a> کلیک کنید یا اگر صفحهٔ {opString} هنوز باز است کد <strong>{secretCode}</strong> را در آن وارد کنید.</p>"
)
+
"</td></tr>"
+
"<tr><td>"
+
$"<p style=\"font:normal 12px tahoma;direction:rtl\">{ifNot} به این نامه پاسخ ندهید، کسی پاسخگوی شما نخواهد بود.</p>"
+
"</td></tr>"
+
"</table>"
+
"</body>"
+
"</html>"
;
return content;
}
/// <summary>
/// Start signup process using email
/// </summary>
/// <param name="email"></param>
/// <param name="clientIPAddress"></param>
/// <param name="clientAppName"></param>
/// <param name="langauge"></param>
/// <returns></returns>
public override async Task<RServiceResult<RVerifyQueueItem>> SignUp(string email, string clientIPAddress, string clientAppName, string langauge)
{
RServiceResult<RVerifyQueueItem> res = await base.SignUp(email, clientIPAddress, clientAppName, langauge);
if(res.Result == null)
{
if(res.ExceptionString == "شما قبلا نام‌نویسی کرده‌اید.")
{
return new RServiceResult<RVerifyQueueItem>(null, "شما قبلا نام‌نویسی کرده‌اید. توجه بفرمایید که کاربران گنجینهٔ گنجور و پیشخان خوانشگران یکسانند و می‌توانید با همان نام کاربری اینجا وارد شوید.");
}
}
return res;
}
/// <summary>
/// finalize signup and assign his or him comments to him or her
/// </summary>
/// <param name="email"></param>
/// <param name="secret"></param>
/// <param name="password"></param>
/// <param name="firstName"></param>
/// <param name="surName"></param>
/// <param name="phoneNumber"></param>
/// <returns></returns>
public override async Task<RServiceResult<bool>> FinalizeSignUp(string email, string secret, string password, string firstName, string surName, string phoneNumber)
{
RServiceResult<bool> res = await base.FinalizeSignUp(email, secret, password, firstName, surName, phoneNumber);
if(res.Result)
{
try
{
RMuseumDbContext context = _context as RMuseumDbContext;
var user = (await FindUserByEmail(email)).Result;
if(user.EmailConfirmed)
{
var comments = await context.DivanComments.Where(c => c.AuthorEmail == email.ToLower()).ToListAsync();
if (comments.Count > 0)
{
foreach (var comment in comments)
{
comment.UserId = user.Id;
}
_context.UpdateRange(comments);
await _context.SaveChangesAsync();
}
}
}
catch
{
return new RServiceResult<bool>(true); //ignore this error! because signup was succesfull
}
}
return res;
}
/// <summary>
/// modify existing user /*update related entities cache*/
/// </summary>
/// <param name="userId"></param>
/// <param name="updateUserInfo"></param>
/// <returns></returns>
public override async Task<RServiceResult<bool>> ModifyUser(Guid userId, RegisterRAppUser updateUserInfo)
{
try
{
RAppUser unmodifiedUserInfo = await _userManager.FindByIdAsync(userId.ToString());
if (unmodifiedUserInfo == null)
{
return new RServiceResult<bool>(false, "کاربر مورد نظر یافت نشد");
}
string nickName = updateUserInfo.NickName;
if (string.IsNullOrEmpty(nickName))
{
return new RServiceResult<bool>(false, "نام مستعار نمی‌تواند خالی باشد.");
}
nickName = nickName.Trim();
RServiceResult<bool> res = await base.ModifyUser(userId, updateUserInfo);
if (res.Result)
{
try
{
if (nickName != updateUserInfo.NickName)
{
RMuseumDbContext context = _context as RMuseumDbContext;
var poemIdSet = await context.DivanComments.AsNoTracking().Where(c => c.UserId == userId).Select(c => c.PoemId).ToListAsync();
foreach (var poemId in poemIdSet)
{
//await _divanService.CacheCleanForPageById(poemId); /*had error in service initializtion, so done it in the dirty way*/
var dbPage = await context.DivanPages.Where(p => p.Id == poemId).AsNoTracking().SingleOrDefaultAsync();
if (dbPage != null)
{
//CacheCleanForPageByUrl(dbPage.FullUrl);
var url = dbPage.FullUrl;
var cachKey = $"DivanService::GetPageByUrl::{url}";
if (_memoryCache.TryGetValue(cachKey, out DivanPageCompleteViewModel page))
{
_memoryCache.Remove(cachKey);
var poemCachKey = $"GetPoemById({page.Id}, {true}, {false}, {true}, {true}, {true}, {true}, {true}, {true}, {true})";
if (_memoryCache.TryGetValue(poemCachKey, out DivanPoemCompleteViewModel p))
{
_memoryCache.Remove(poemCachKey);
}
}
}
}
}
}
catch
{
return new RServiceResult<bool>(true); //ignore this error! because main operation was successfull!
}
}
return res;
}
catch(Exception exp)
{
return new RServiceResult<bool>(false, exp.ToString());
}
}
/// <summary>
/// remove user data
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>
public override async Task<RServiceResult<bool>> RemoveUserData(Guid userId)
{
RMuseumDbContext context = _context as RMuseumDbContext;
string systemEmail = $"{Configuration.GetSection("Divan")["SystemEmail"]}";
var systemUserId = (Guid)(await FindUserByEmail(systemEmail)).Result.Id;
if(systemUserId == userId)
{
return new RServiceResult<bool>(false, "تلاش برای حذف کاربر سیستمی");
}
string deletedUserEmail = $"{Configuration.GetSection("Divan")["DeleteUserEmail"]}";
var deletedUserId = (Guid)(await FindUserByEmail(deletedUserEmail)).Result.Id;
if (deletedUserId == userId)
{
return new RServiceResult<bool>(false, "تلاش برای حذف کاربر سیستمی کاربر حذف شده");
}
var reviewedRecitations = await context.Recitations.Where(r => r.ReviewerId == userId).ToListAsync();
foreach(var reviewedRecitation in reviewedRecitations)
reviewedRecitation.ReviewerId = deletedUserId;
context.UpdateRange(reviewedRecitations);
await context.SaveChangesAsync(); //some tracking data related bugs makes it necessary to call this especially for same table data processing
var suggestedCorrections = await context.DivanPoemCorrections.Where(c => c.UserId == userId).ToListAsync();
foreach (var suggestedCorrection in suggestedCorrections)
suggestedCorrection.UserId = deletedUserId;
context.UpdateRange(suggestedCorrections);
await context.SaveChangesAsync();
var reviewedCorrections = await context.DivanPoemCorrections.Where(c => c.ReviewerUserId == userId).ToListAsync();
foreach (var reviewedCorrection in reviewedCorrections)
reviewedCorrection.ReviewerUserId = deletedUserId;
context.UpdateRange(reviewedCorrections);
await context.SaveChangesAsync();
var suggestedSectionCorrections = await context.DivanPoemSectionCorrections.Where(c => c.UserId == userId).ToListAsync();
foreach (var suggestedCorrection in suggestedSectionCorrections)
suggestedCorrection.UserId = deletedUserId;
context.UpdateRange(suggestedSectionCorrections);
await context.SaveChangesAsync();
var reviewedSectionCorrections = await context.DivanPoemSectionCorrections.Where(c => c.ReviewerUserId == userId).ToListAsync();
foreach (var reviewedCorrection in reviewedSectionCorrections)
reviewedCorrection.ReviewerUserId = deletedUserId;
context.UpdateRange(reviewedSectionCorrections);
await context.SaveChangesAsync();
var suggestedCatCorrections = await context.DivanCatCorrections.Where(c => c.UserId == userId).ToListAsync();
foreach (var suggestedCatCorrection in suggestedCatCorrections)
suggestedCatCorrection.UserId = deletedUserId;
context.UpdateRange(suggestedCatCorrections);
await context.SaveChangesAsync();
var reviewedCatCorrections = await context.DivanCatCorrections.Where(c => c.ReviewerUserId == userId).ToListAsync();
foreach (var reviewedCatCorrection in reviewedCatCorrections)
reviewedCatCorrection.ReviewerUserId = deletedUserId;
context.UpdateRange(reviewedCatCorrections);
await context.SaveChangesAsync();
var reportedComments = await context.DivanReportedComments.Where(r => r.ReportedById == userId).ToListAsync();
foreach (var reportedComment in reportedComments)
reportedComment.ReportedById = deletedUserId;
context.UpdateRange(reportedComments);
await context.SaveChangesAsync();
var divanLinks = await context.DivanLinks.Where(l => l.SuggestedById == userId).ToListAsync();
foreach (var divanLink in divanLinks)
divanLink.SuggestedById = deletedUserId;
context.UpdateRange(divanLinks);
await context.SaveChangesAsync();
var reviewedDivanLinks = await context.DivanLinks.Where(l => l.ReviewerId == userId).ToListAsync();
foreach (var reviewedDivanLink in reviewedDivanLinks)
reviewedDivanLink.ReviewerId = deletedUserId;
context.UpdateRange(reviewedDivanLinks);
await context.SaveChangesAsync();
var pinLinks = await context.PinterestLinks.Where(l => l.SuggestedById == userId).ToListAsync();
foreach (var pinLink in pinLinks)
pinLink.SuggestedById = deletedUserId;
context.UpdateRange(pinLinks);
await context.SaveChangesAsync();
var reviewedPinLinks = await context.PinterestLinks.Where(l => l.ReviewerId == userId).ToListAsync();
foreach (var reviewedPinLink in reviewedPinLinks)
reviewedPinLink.ReviewerId = deletedUserId;
context.UpdateRange(reviewedPinLinks);
await context.SaveChangesAsync();
var poemMusicTracks = await context.DivanPoemMusicTracks.Where(m => m.SuggestedById == userId).ToListAsync();
foreach (var poemMusicTrack in poemMusicTracks)
poemMusicTrack.SuggestedById = deletedUserId;
context.UpdateRange(poemMusicTracks);
await context.SaveChangesAsync();
var snapshots = await context.DivanPageSnapshots.Where(s => s.MadeObsoleteByUserId == userId).ToListAsync();
foreach (var snapshot in snapshots)
snapshot.MadeObsoleteByUserId = deletedUserId;
context.UpdateRange(snapshots);
await context.SaveChangesAsync();
var suggestedPoetNotes = await context.DivanPoetSuggestedSpecLines.Where(s => s.SuggestedById == userId).ToListAsync();
foreach (var suggestedPoetNote in suggestedPoetNotes)
suggestedPoetNote.SuggestedById = deletedUserId;
context.UpdateRange(suggestedPoetNotes);
await context.SaveChangesAsync();
var suggestedPoetPhotos = await context.DivanPoetSuggestedPictures.Where(s => s.SuggestedById == userId).ToListAsync();
foreach (var suggestedPoetPhoto in suggestedPoetPhotos)
suggestedPoetPhoto.SuggestedById = deletedUserId;
context.UpdateRange(suggestedPoetPhotos);
await context.SaveChangesAsync();
var suggestQuotes = await context.DivanQuotedPoems.Where(s => s.SuggestedById == userId).ToListAsync();
foreach(var suggestedQuote in suggestedPoetPhotos)
suggestedQuote.SuggestedById = deletedUserId;
context.UpdateRange(suggestedPoetPhotos);
await context.SaveChangesAsync();
var visits = await context.DivanUserPoemVisits.Where(v => v.UserId == userId).ToListAsync();
context.RemoveRange(visits);
await context.SaveChangesAsync();
var bookmarks = await context.UserBookmarks.Where(b => b.RAppUserId == userId).ToListAsync();
context.RemoveRange(bookmarks);
await context.SaveChangesAsync();
var uploadSessions = await context.UploadSessions.Where(s => s.UseId == userId).ToListAsync();
context.RemoveRange(uploadSessions);
await context.SaveChangesAsync();
var recitations = await context.Recitations.Where(r => r.OwnerId == userId).ToListAsync();
context.RemoveRange(recitations);
await context.SaveChangesAsync();
var divanBookmarks = await context.DivanUserBookmarks.Where(b => b.UserId == userId).ToListAsync();
context.RemoveRange(divanBookmarks);
await context.SaveChangesAsync();
var reportedRecitaions = await context.RecitationErrorReports.Where(r => r.ReporterId == userId).ToListAsync();
context.RemoveRange(reportedRecitaions);
await context.SaveChangesAsync();
var comments = await context.DivanComments.Where(c => c.UserId == userId).ToListAsync();
foreach (var comment in comments)
{
//await _divanService.DeleteMyComment(userId, comment.Id);/*had error in service initializtion, so done it in the dirty way*/
await _DeleteComment(context, comment.Id);
}
var userNotes = await context.UserNotes.Where(c => c.RAppUserId == userId).ToListAsync();
foreach (var note in userNotes)
{
await _DeleteUserNotesAsync(context, note.Id);
}
var recitationsVotes = await context.RecitationUserUpVotes.Where(c => c.UserId == userId).ToListAsync();
foreach (var vote in recitationsVotes)
{
int poemId = await context.Recitations.AsNoTracking().Where(r => r.Id == vote.RecitationId).Select(r => r.DivanPostId).SingleAsync();
context.Remove(vote);
await context.SaveChangesAsync();
await _ReOrderPoemRecitationsAsync(context, poemId);
}
return await base.RemoveUserData(userId);//notifications are deleted here, some of these operations might produce new notifications
}
private async Task<RServiceResult<bool>> _DeleteComment(RMuseumDbContext context, int commentId)
{
DivanComment comment = await context.DivanComments.Where(c => c.Id == commentId).SingleOrDefaultAsync();
if (comment == null)
{
return new RServiceResult<bool>(false); //not found
}
foreach(var reply in await _FindReplies(context, comment))
{
await _DeleteComment(context, reply.Id);
}
context.DivanComments.Remove(comment);
await _context.SaveChangesAsync();
return new RServiceResult<bool>(true);
}
private async Task<List<DivanComment>> _FindReplies(RMuseumDbContext context, DivanComment comment)
{
return await context.DivanComments.Where(c => c.InReplyToId == comment.Id).AsNoTracking().ToListAsync();
}
private async Task<RServiceResult<bool>> _DeleteUserNotesAsync(RMuseumDbContext context, Guid commentId)
{
RUserNote comment = await context.UserNotes.Where(c => c.Id == commentId).SingleOrDefaultAsync();
if (comment == null)
{
return new RServiceResult<bool>(false); //not found
}
foreach (var reply in await _FindNoteRepliesAsync(context, comment))
{
await _DeleteUserNotesAsync(context, reply.Id);
}
context.Remove(comment);
await _context.SaveChangesAsync();
return new RServiceResult<bool>(true);
}
private async Task<List<RUserNote>> _FindNoteRepliesAsync(RMuseumDbContext context, RUserNote comment)
{
return await context.UserNotes.Where(c => c.ReferenceNoteId == comment.Id).AsNoTracking().ToListAsync();
}
private async Task _ReOrderPoemRecitationsAsync(RMuseumDbContext context, int poemId, bool update = true)
{
var recitations =
await context.Recitations
.Where(r => r.ReviewStatus == AudioReviewStatus.Approved && r.DivanPostId == poemId)
.OrderBy(r => r.Id) //this causes the oldest recirations to become the first one
.ToListAsync();
List<RecitationOrderingViewModel> scores = new List<RecitationOrderingViewModel>();
for (var i = 0; i < recitations.Count; i++)
{
var recitation = recitations[i];
RecitationOrderingViewModel score = new RecitationOrderingViewModel()
{
RecitationId = recitation.Id,
EarlynessAdvantage = recitations.Count - 1 - i,
InitialScore = recitations[i].InitialScore,
UpVotes = await context.RecitationUserUpVotes.AsNoTracking().Where(r => r.RecitationId == recitation.Id && r.UserId != recitation.OwnerId)
.CountAsync(),
Mistakes = await context.RecitationApprovedMistakes.AsNoTracking().Where(m => m.RecitationId == recitation.Id).SumAsync(m => m.NumberOfLinesAffected)
};
score.TotalScores = score.EarlynessAdvantage
+
score.InitialScore
+ score.UpVotes
- (5 * score.Mistakes);
//audio order is used as a temporary variable in the following line and soon is getting replaced by computed value
recitation.AudioOrder = score.TotalScores;
scores.Add(score);
}
recitations.Sort((a, b) => b.AudioOrder.CompareTo(a.AudioOrder));
for (var i = 0; i < recitations.Count; i++)
{
recitations[i].AudioOrder = i + 1;
scores.Where(s => s.RecitationId == recitations[i].Id).Single().ComputedOrder = i + 1;
if (update)
{
context.Update(recitations[i]);
}
}
if (update)
await context.SaveChangesAsync();
}
}
}