From 43ac8b704e47e05d259f35d0a9cdb4de6c787ee5 Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 26 Nov 2020 21:04:42 +0800 Subject: refactor: ... --- BackEnd/Timeline/Services/TimelinePostService.cs | 493 +++++++++++++++++++++++ 1 file changed, 493 insertions(+) create mode 100644 BackEnd/Timeline/Services/TimelinePostService.cs (limited to 'BackEnd/Timeline/Services/TimelinePostService.cs') diff --git a/BackEnd/Timeline/Services/TimelinePostService.cs b/BackEnd/Timeline/Services/TimelinePostService.cs new file mode 100644 index 00000000..36fcdbca --- /dev/null +++ b/BackEnd/Timeline/Services/TimelinePostService.cs @@ -0,0 +1,493 @@ +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; +using Timeline.Entities; +using Timeline.Helpers; +using Timeline.Models; +using Timeline.Services.Exceptions; +using SixLabors.ImageSharp; +using static Timeline.Resources.Services.TimelineService; +using Microsoft.Extensions.Logging; + +namespace Timeline.Services +{ + public class PostData : ICacheableData + { +#pragma warning disable CA1819 // Properties should not return arrays + public byte[] Data { get; set; } = default!; +#pragma warning restore CA1819 // Properties should not return arrays + public string Type { get; set; } = default!; + public string ETag { get; set; } = default!; + public DateTime? LastModified { get; set; } // TODO: Why nullable? + } + + public interface ITimelinePostService + { + /// + /// Get all the posts in the timeline. + /// + /// The name of the timeline. + /// The time that posts have been modified since. + /// Whether include deleted posts. + /// A list of all posts. + /// Thrown when is null. + /// Throw when is of bad format. + /// + /// Thrown when timeline with name does not exist. + /// If it is a personal timeline, then inner exception is . + /// + Task> GetPosts(string timelineName, DateTime? modifiedSince = null, bool includeDeleted = false); + + /// + /// Get the etag of data of a post. + /// + /// The name of the timeline of the post. + /// The id of the post. + /// The etag of the data. + /// Thrown when is null. + /// Throw when is of bad format. + /// + /// Thrown when timeline with name does not exist. + /// If it is a personal timeline, then inner exception is . + /// + /// Thrown when post of does not exist or has been deleted. + /// Thrown when post has no data. + /// + Task GetPostDataETag(string timelineName, long postId); + + /// + /// Get the data of a post. + /// + /// The name of the timeline of the post. + /// The id of the post. + /// The etag of the data. + /// Thrown when is null. + /// Throw when is of bad format. + /// + /// Thrown when timeline with name does not exist. + /// If it is a personal timeline, then inner exception is . + /// + /// Thrown when post of does not exist or has been deleted. + /// Thrown when post has no data. + /// + Task GetPostData(string timelineName, long postId); + + /// + /// Create a new text post in timeline. + /// + /// The name of the timeline to create post against. + /// The author's user id. + /// The content text. + /// The time of the post. If null, then current time is used. + /// The info of the created post. + /// Thrown when or is null. + /// Throw when is of bad format. + /// + /// Thrown when timeline with name does not exist. + /// If it is a personal timeline, then inner exception is . + /// + /// Thrown if user of does not exist. + Task CreateTextPost(string timelineName, long authorId, string text, DateTime? time); + + /// + /// Create a new image post in timeline. + /// + /// The name of the timeline to create post against. + /// The author's user id. + /// The image data. + /// The time of the post. If null, then use current time. + /// The info of the created post. + /// Thrown when or is null. + /// Throw when is of bad format. + /// + /// Thrown when timeline with name does not exist. + /// If it is a personal timeline, then inner exception is . + /// + /// Thrown if user of does not exist. + /// Thrown if data is not a image. Validated by . + Task CreateImagePost(string timelineName, long authorId, byte[] imageData, DateTime? time); + + /// + /// Delete a post. + /// + /// The name of the timeline to delete post against. + /// The id of the post to delete. + /// Thrown when is null. + /// Throw when is of bad format. + /// + /// Thrown when timeline with name does not exist. + /// If it is a personal timeline, then inner exception is . + /// + /// Thrown when the post with given id does not exist or is deleted already. + /// + /// First use to check the permission. + /// + Task DeletePost(string timelineName, long postId); + + /// + /// Delete all posts of the given user. Used when delete a user. + /// + /// The id of the user. + Task DeleteAllPostsOfUser(long userId); + + /// + /// Verify whether a user has the permission to modify a post. + /// + /// The name of the timeline. + /// The id of the post. + /// The id of the user to check on. + /// True if you want it to throw . Default false. + /// True if can modify, false if can't modify. + /// Thrown when is null. + /// Throw when is of bad format. + /// + /// Thrown when timeline with name does not exist. + /// If it is a personal timeline, then inner exception is . + /// + /// Thrown when the post with given id does not exist or is deleted already and is true. + /// + /// Unless is true, this method should return true if the post does not exist. + /// If the post is deleted, its author info still exists, so it is checked as the post is not deleted unless is true. + /// This method does not check whether the user is administrator. + /// It only checks whether he is the author of the post or the owner of the timeline. + /// Return false when user with modifier id does not exist. + /// + Task HasPostModifyPermission(string timelineName, long postId, long modifierId, bool throwOnPostNotExist = false); + } + + public class TimelinePostService : ITimelinePostService + { + private readonly ILogger _logger; + private readonly DatabaseContext _database; + private readonly IBasicTimelineService _basicTimelineService; + private readonly IUserService _userService; + private readonly IDataManager _dataManager; + private readonly IImageValidator _imageValidator; + private readonly IClock _clock; + + public TimelinePostService(ILogger logger, DatabaseContext database, IBasicTimelineService basicTimelineService, IUserService userService, IDataManager dataManager, IImageValidator imageValidator, IClock clock) + { + _logger = logger; + _database = database; + _basicTimelineService = basicTimelineService; + _userService = userService; + _dataManager = dataManager; + _imageValidator = imageValidator; + _clock = clock; + } + + private async Task MapTimelinePostFromEntity(TimelinePostEntity entity, string timelineName) + { + User? author = entity.AuthorId.HasValue ? await _userService.GetUser(entity.AuthorId.Value) : null; + + ITimelinePostContent? content = null; + + if (entity.Content != null) + { + var type = entity.ContentType; + + content = type switch + { + TimelinePostContentTypes.Text => new TextTimelinePostContent(entity.Content), + TimelinePostContentTypes.Image => new ImageTimelinePostContent(entity.Content), + _ => throw new DatabaseCorruptedException(string.Format(CultureInfo.InvariantCulture, ExceptionDatabaseUnknownContentType, type)) + }; + } + + return new TimelinePost( + id: entity.LocalId, + author: author, + content: content, + time: entity.Time, + lastUpdated: entity.LastUpdated, + timelineName: timelineName + ); + } + + public async Task> GetPosts(string timelineName, DateTime? modifiedSince = null, bool includeDeleted = false) + { + modifiedSince = modifiedSince?.MyToUtc(); + + if (timelineName == null) + throw new ArgumentNullException(nameof(timelineName)); + + var timelineId = await _basicTimelineService.GetTimelineIdByName(timelineName); + IQueryable query = _database.TimelinePosts.Where(p => p.TimelineId == timelineId); + + if (!includeDeleted) + { + query = query.Where(p => p.Content != null); + } + + if (modifiedSince.HasValue) + { + query = query.Include(p => p.Author).Where(p => p.LastUpdated >= modifiedSince || (p.Author != null && p.Author.UsernameChangeTime >= modifiedSince)); + } + + query = query.OrderBy(p => p.Time); + + var postEntities = await query.ToListAsync(); + + var posts = new List(); + foreach (var entity in postEntities) + { + posts.Add(await MapTimelinePostFromEntity(entity, timelineName)); + } + return posts; + } + + public async Task GetPostDataETag(string timelineName, long postId) + { + if (timelineName == null) + throw new ArgumentNullException(nameof(timelineName)); + + var timelineId = await _basicTimelineService.GetTimelineIdByName(timelineName); + + var postEntity = await _database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == postId).SingleOrDefaultAsync(); + + if (postEntity == null) + throw new TimelinePostNotExistException(timelineName, postId, false); + + if (postEntity.Content == null) + throw new TimelinePostNotExistException(timelineName, postId, true); + + if (postEntity.ContentType != TimelinePostContentTypes.Image) + throw new TimelinePostNoDataException(ExceptionGetDataNonImagePost); + + var tag = postEntity.Content; + + return tag; + } + + public async Task GetPostData(string timelineName, long postId) + { + if (timelineName == null) + throw new ArgumentNullException(nameof(timelineName)); + + var timelineId = await _basicTimelineService.GetTimelineIdByName(timelineName); + var postEntity = await _database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == postId).SingleOrDefaultAsync(); + + if (postEntity == null) + throw new TimelinePostNotExistException(timelineName, postId, false); + + if (postEntity.Content == null) + throw new TimelinePostNotExistException(timelineName, postId, true); + + if (postEntity.ContentType != TimelinePostContentTypes.Image) + throw new TimelinePostNoDataException(ExceptionGetDataNonImagePost); + + var tag = postEntity.Content; + + byte[] data; + + try + { + data = await _dataManager.GetEntry(tag); + } + catch (InvalidOperationException e) + { + throw new DatabaseCorruptedException(ExceptionGetDataDataEntryNotExist, e); + } + + if (postEntity.ExtraContent == null) + { + _logger.LogWarning(LogGetDataNoFormat); + var format = Image.DetectFormat(data); + postEntity.ExtraContent = format.DefaultMimeType; + await _database.SaveChangesAsync(); + } + + return new PostData + { + Data = data, + Type = postEntity.ExtraContent, + ETag = tag, + LastModified = postEntity.LastUpdated + }; + } + + public async Task CreateTextPost(string timelineName, long authorId, string text, DateTime? time) + { + time = time?.MyToUtc(); + + if (timelineName == null) + throw new ArgumentNullException(nameof(timelineName)); + if (text == null) + throw new ArgumentNullException(nameof(text)); + + var timelineId = await _basicTimelineService.GetTimelineIdByName(timelineName); + var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync(); + + var author = await _userService.GetUser(authorId); + + var currentTime = _clock.GetCurrentTime(); + var finalTime = time ?? currentTime; + + timelineEntity.CurrentPostLocalId += 1; + + var postEntity = new TimelinePostEntity + { + LocalId = timelineEntity.CurrentPostLocalId, + ContentType = TimelinePostContentTypes.Text, + Content = text, + AuthorId = authorId, + TimelineId = timelineId, + Time = finalTime, + LastUpdated = currentTime + }; + _database.TimelinePosts.Add(postEntity); + await _database.SaveChangesAsync(); + + + return new TimelinePost( + id: postEntity.LocalId, + content: new TextTimelinePostContent(text), + time: finalTime, + author: author, + lastUpdated: currentTime, + timelineName: timelineName + ); + } + + public async Task CreateImagePost(string timelineName, long authorId, byte[] data, DateTime? time) + { + time = time?.MyToUtc(); + + if (timelineName == null) + throw new ArgumentNullException(nameof(timelineName)); + if (data == null) + throw new ArgumentNullException(nameof(data)); + + var timelineId = await _basicTimelineService.GetTimelineIdByName(timelineName); + var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync(); + + var author = await _userService.GetUser(authorId); + + var imageFormat = await _imageValidator.Validate(data); + + var imageFormatText = imageFormat.DefaultMimeType; + + var tag = await _dataManager.RetainEntry(data); + + var currentTime = _clock.GetCurrentTime(); + var finalTime = time ?? currentTime; + + timelineEntity.CurrentPostLocalId += 1; + + var postEntity = new TimelinePostEntity + { + LocalId = timelineEntity.CurrentPostLocalId, + ContentType = TimelinePostContentTypes.Image, + Content = tag, + ExtraContent = imageFormatText, + AuthorId = authorId, + TimelineId = timelineId, + Time = finalTime, + LastUpdated = currentTime + }; + _database.TimelinePosts.Add(postEntity); + await _database.SaveChangesAsync(); + + return new TimelinePost( + id: postEntity.LocalId, + content: new ImageTimelinePostContent(tag), + time: finalTime, + author: author, + lastUpdated: currentTime, + timelineName: timelineName + ); + } + + public async Task DeletePost(string timelineName, long id) + { + if (timelineName == null) + throw new ArgumentNullException(nameof(timelineName)); + + var timelineId = await _basicTimelineService.GetTimelineIdByName(timelineName); + + var post = await _database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == id).SingleOrDefaultAsync(); + + if (post == null) + throw new TimelinePostNotExistException(timelineName, id, false); + + if (post.Content == null) + throw new TimelinePostNotExistException(timelineName, id, true); + + string? dataTag = null; + + if (post.ContentType == TimelinePostContentTypes.Image) + { + dataTag = post.Content; + } + + post.Content = null; + post.LastUpdated = _clock.GetCurrentTime(); + + await _database.SaveChangesAsync(); + + if (dataTag != null) + { + await _dataManager.FreeEntry(dataTag); + } + } + + public async Task DeleteAllPostsOfUser(long userId) + { + var posts = await _database.TimelinePosts.Where(p => p.AuthorId == userId).ToListAsync(); + + var now = _clock.GetCurrentTime(); + + var dataTags = new List(); + + foreach (var post in posts) + { + if (post.Content != null) + { + if (post.ContentType == TimelinePostContentTypes.Image) + { + dataTags.Add(post.Content); + } + post.Content = null; + } + post.LastUpdated = now; + } + + await _database.SaveChangesAsync(); + + foreach (var dataTag in dataTags) + { + await _dataManager.FreeEntry(dataTag); + } + } + + public async Task HasPostModifyPermission(string timelineName, long postId, long modifierId, bool throwOnPostNotExist = false) + { + if (timelineName == null) + throw new ArgumentNullException(nameof(timelineName)); + + var timelineId = await _basicTimelineService.GetTimelineIdByName(timelineName); + + var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.OwnerId }).SingleAsync(); + + var postEntity = await _database.TimelinePosts.Where(p => p.Id == postId).Select(p => new { p.Content, p.AuthorId }).SingleOrDefaultAsync(); + + if (postEntity == null) + { + if (throwOnPostNotExist) + throw new TimelinePostNotExistException(timelineName, postId, false); + else + return true; + } + + if (postEntity.Content == null && throwOnPostNotExist) + { + throw new TimelinePostNotExistException(timelineName, postId, true); + } + + return timelineEntity.OwnerId == modifierId || postEntity.AuthorId == modifierId; + } + } +} -- cgit v1.2.3