using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using SixLabors.ImageSharp;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Timeline.Entities;
using Timeline.Helpers;
using Timeline.Helpers.Cache;
using Timeline.Models;
using Timeline.Models.Validation;
using Timeline.Services.Exceptions;
using static Timeline.Resources.Services.TimelineService;
namespace Timeline.Services
{
public class TimelinePostCreateRequestData
{
public TimelinePostCreateRequestData(string contentType, byte[] data)
{
ContentType = contentType;
Data = data;
}
public string ContentType { get; set; }
#pragma warning disable CA1819 // Properties should not return arrays
public byte[] Data { get; set; }
#pragma warning restore CA1819 // Properties should not return arrays
}
public class TimelinePostCreateRequest
{
public string? Color { get; set; }
/// If not set, current time is used.
public DateTime? Time { get; set; }
public List DataList { get; set; } = new List();
}
public class TimelinePostPatchRequest
{
public string? Color { get; set; }
public DateTime? Time { get; set; }
}
public interface ITimelinePostService
{
///
/// Get all the posts in the timeline.
///
/// The id of the timeline.
/// The time that posts have been modified since.
/// Whether include deleted posts.
/// A list of all posts.
/// Thrown when timeline does not exist.
Task> GetPosts(long timelineId, DateTime? modifiedSince = null, bool includeDeleted = false);
///
/// Get a post of a timeline.
///
/// The id of the timeline of the post.
/// The id of the post.
/// If true, return the entity even if it is deleted.
/// The post.
/// Thrown when timeline does not exist.
/// Thrown when post of does not exist or has been deleted.
Task GetPost(long timelineId, long postId, bool includeDelete = false);
///
/// Get the data digest of a post.
///
/// The timeline id.
/// The post id.
/// The index of the data.
/// The data digest.
/// Thrown when timeline does not exist.
/// Thrown when post of does not exist or has been deleted.
/// Thrown when data of that index does not exist.
Task GetPostDataDigest(long timelineId, long postId, long dataIndex);
///
/// Get the data of a post.
///
/// The timeline id.
/// The post id.
/// The index of the data.
/// The data.
/// Thrown when timeline does not exist.
/// Thrown when post of does not exist or has been deleted.
/// Thrown when data of that index does not exist.
Task GetPostData(long timelineId, long postId, long dataIndex);
///
/// Create a new post in timeline.
///
/// The id of the timeline to create post against.
/// The author's user id.
/// Info about the post.
/// The entity of the created post.
/// Thrown when is null.
/// Thrown when is of invalid format.
/// Thrown when timeline does not exist.
/// Thrown if user of does not exist.
/// Thrown if data is not a image. Validated by .
Task CreatePost(long timelineId, long authorId, TimelinePostCreateRequest request);
///
/// Modify a post. Change its properties or replace its content.
///
/// The timeline id.
/// The post id.
/// The request.
/// The entity of the patched post.
/// Thrown when is null.
/// Thrown when is of invalid format.
/// Thrown when timeline does not exist.
/// Thrown when post does not exist.
Task PatchPost(long timelineId, long postId, TimelinePostPatchRequest request);
///
/// Delete a post.
///
/// The id of the timeline to delete post against.
/// The id of the post to delete.
/// Thrown when timeline does not exist.
/// Thrown when the post with given id does not exist or is deleted already.
///
/// First use to check the permission.
///
Task DeletePost(long timelineId, 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 id 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 timeline does not exist.
/// 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(long timelineId, 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 IBasicUserService _basicUserService;
private readonly IDataManager _dataManager;
private readonly IImageValidator _imageValidator;
private readonly IClock _clock;
private readonly ColorValidator _colorValidator = new ColorValidator();
public TimelinePostService(ILogger logger, DatabaseContext database, IBasicTimelineService basicTimelineService, IBasicUserService basicUserService, IDataManager dataManager, IImageValidator imageValidator, IClock clock)
{
_logger = logger;
_database = database;
_basicTimelineService = basicTimelineService;
_basicUserService = basicUserService;
_dataManager = dataManager;
_imageValidator = imageValidator;
_clock = clock;
}
private async Task CheckTimelineExistence(long timelineId)
{
if (!await _basicTimelineService.CheckExistence(timelineId))
throw new TimelineNotExistException(timelineId);
}
private async Task CheckUserExistence(long userId)
{
if (!await _basicUserService.CheckUserExistence(userId))
throw new UserNotExistException(userId);
}
public async Task> GetPosts(long timelineId, DateTime? modifiedSince = null, bool includeDeleted = false)
{
await CheckTimelineExistence(timelineId);
modifiedSince = modifiedSince?.MyToUtc();
IQueryable query = _database.TimelinePosts.Where(p => p.TimelineId == timelineId);
if (!includeDeleted)
{
query = query.Where(p => p.Content != null);
}
if (modifiedSince.HasValue)
{
query = query.Where(p => p.LastUpdated >= modifiedSince || (p.Author != null && p.Author.UsernameChangeTime >= modifiedSince));
}
query = query.OrderBy(p => p.Time);
return await query.ToListAsync();
}
public async Task GetPost(long timelineId, long postId, bool includeDelete = false)
{
await CheckTimelineExistence(timelineId);
var post = await _database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == postId).SingleOrDefaultAsync();
if (post is null)
{
throw new TimelinePostNotExistException(timelineId, postId, false);
}
if (!includeDelete && post.Content is null)
{
throw new TimelinePostNotExistException(timelineId, postId, true);
}
return post;
}
public async Task GetPostDataETag(long timelineId, long postId)
{
await CheckTimelineExistence(timelineId);
var postEntity = await _database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == postId).SingleOrDefaultAsync();
if (postEntity == null)
throw new TimelinePostNotExistException(timelineId, postId, false);
if (postEntity.Content == null)
throw new TimelinePostNotExistException(timelineId, postId, true);
if (postEntity.ContentType != TimelinePostDataKind.Image)
throw new TimelinePostNoDataException(ExceptionGetDataNonImagePost);
var tag = postEntity.Content;
return tag;
}
public async Task GetPostData(long timelineId, long postId)
{
await CheckTimelineExistence(timelineId);
var postEntity = await _database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == postId).SingleOrDefaultAsync();
if (postEntity == null)
throw new TimelinePostNotExistException(timelineId, postId, false);
if (postEntity.Content == null)
throw new TimelinePostNotExistException(timelineId, postId, true);
if (postEntity.ContentType != TimelinePostDataKind.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 TimelinePostData
{
Data = data,
Type = postEntity.ExtraContent,
ETag = tag,
LastModified = postEntity.LastUpdated
};
}
private async Task SaveContent(TimelinePostEntity entity, TimelinePostCreateRequestData content)
{
switch (content)
{
case TimelinePostCreateRequestTextData c:
entity.ContentType = c.Kind;
entity.Content = c.Data;
break;
case TimelinePostCreateRequestImageData c:
var imageFormat = await _imageValidator.Validate(c.Data);
var imageFormatText = imageFormat.DefaultMimeType;
var tag = await _dataManager.RetainEntry(c.Data);
entity.ContentType = content.Kind;
entity.Content = tag;
entity.ExtraContent = imageFormatText;
break;
default:
throw new ArgumentException("Unknown content type.", nameof(content));
};
}
private async Task CleanContent(TimelinePostEntity entity)
{
if (entity.Content is not null && entity.ContentType == TimelinePostDataKind.Image)
await _dataManager.FreeEntry(entity.Content);
entity.Content = null;
}
public async Task CreatePost(long timelineId, long authorId, TimelinePostCreateRequest request)
{
if (request is null)
throw new ArgumentNullException(nameof(request));
if (request.Content is null)
throw new ArgumentException("Content is null.", nameof(request));
{
if (!_colorValidator.Validate(request.Color, out var message))
throw new ArgumentException("Color is not valid.", nameof(request));
}
request.Time = request.Time?.MyToUtc();
await CheckTimelineExistence(timelineId);
await CheckUserExistence(authorId);
var currentTime = _clock.GetCurrentTime();
var finalTime = request.Time ?? currentTime;
await using var transaction = await _database.Database.BeginTransactionAsync();
var postEntity = new TimelinePostEntity
{
AuthorId = authorId,
TimelineId = timelineId,
Time = finalTime,
LastUpdated = currentTime,
Color = request.Color
};
await SaveContent(postEntity, request.Content);
var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync();
timelineEntity.CurrentPostLocalId += 1;
postEntity.LocalId = timelineEntity.CurrentPostLocalId;
_database.TimelinePosts.Add(postEntity);
await _database.SaveChangesAsync();
await transaction.CommitAsync();
return postEntity;
}
public async Task PatchPost(long timelineId, long postId, TimelinePostPatchRequest request)
{
if (request is null)
throw new ArgumentNullException(nameof(request));
{
if (!_colorValidator.Validate(request.Color, out var message))
throw new ArgumentException("Color is not valid.", nameof(request));
}
request.Time = request.Time?.MyToUtc();
await CheckTimelineExistence(timelineId);
var entity = await _database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == postId).SingleOrDefaultAsync();
await using var transaction = await _database.Database.BeginTransactionAsync();
if (entity is null)
throw new TimelinePostNotExistException(timelineId, postId, false);
if (entity.Content is null)
throw new TimelinePostNotExistException(timelineId, postId, true);
if (request.Time.HasValue)
entity.Time = request.Time.Value;
if (request.Color is not null)
entity.Color = request.Color;
if (request.Content is not null)
{
await CleanContent(entity);
await SaveContent(entity, request.Content);
}
entity.LastUpdated = _clock.GetCurrentTime();
await _database.SaveChangesAsync();
await transaction.CommitAsync();
return entity;
}
public async Task DeletePost(long timelineId, long postId)
{
await CheckTimelineExistence(timelineId);
var entity = await _database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == postId).SingleOrDefaultAsync();
if (entity == null)
throw new TimelinePostNotExistException(timelineId, postId, false);
if (entity.Content == null)
throw new TimelinePostNotExistException(timelineId, postId, true);
await using var transaction = await _database.Database.BeginTransactionAsync();
await CleanContent(entity);
entity.LastUpdated = _clock.GetCurrentTime();
await _database.SaveChangesAsync();
await transaction.CommitAsync();
}
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 == TimelinePostDataKind.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(long timelineId, long postId, long modifierId, bool throwOnPostNotExist = false)
{
await CheckTimelineExistence(timelineId);
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(timelineId, postId, false);
else
return true;
}
if (postEntity.Content == null && throwOnPostNotExist)
{
throw new TimelinePostNotExistException(timelineId, postId, true);
}
return timelineEntity.OwnerId == modifierId || postEntity.AuthorId == modifierId;
}
}
}