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.Models;
using Timeline.Models.Validation;
using Timeline.Services.Exceptions;
using static Timeline.Resources.Services.TimelineService;
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 class TimelinePostCommonProperties
{
public string? Color { get; set; }
/// If not set, current time is used.
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 the etag of data of a post.
///
/// The id of the timeline of the post.
/// The id of the post.
/// The etag of the data.
/// Thrown when timeline does not exist.
/// Thrown when post of does not exist or has been deleted.
/// Thrown when post has no data.
Task GetPostDataETag(long timelineId, long postId);
///
/// Get the data of a post.
///
/// The id of the timeline of the post.
/// The id of the post.
/// The etag of the data.
/// Thrown when timeline does not exist.
/// Thrown when post of does not exist or has been deleted.
/// Thrown when post has no data.
///
Task GetPostData(long timelineId, long postId);
///
/// Create a new text post in timeline.
///
/// The id of the timeline to create post against.
/// The author's user id.
/// The content text.
/// Some properties.
/// The info of the created post.
/// Thrown when is null.
/// Thrown when timeline does not exist.
/// Thrown if user of does not exist.
Task CreateTextPost(long timelineId, long authorId, string text, TimelinePostCommonProperties? properties = null);
///
/// Create a new image post in timeline.
///
/// The id of the timeline to create post against.
/// The author's user id.
/// The image data.
/// Some properties.
/// The info of the created post.
/// Thrown when is null.
/// Thrown when timeline does not exist.
/// Thrown if user of does not exist.
/// Thrown if data is not a image. Validated by .
Task CreateImagePost(long timelineId, long authorId, byte[] imageData, TimelinePostCommonProperties? properties = null);
///
/// 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 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 != TimelinePostContentTypes.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 != 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
};
}
private async Task GeneralCreatePost(long timelineId, long authorId, TimelinePostCommonProperties? properties, Func saveContent)
{
if (properties is not null)
{
if (!_colorValidator.Validate(properties.Color, out var message))
{
throw new ArgumentException(message, nameof(properties));
}
properties.Time = properties.Time?.MyToUtc();
}
await CheckTimelineExistence(timelineId);
await CheckUserExistence(authorId);
var currentTime = _clock.GetCurrentTime();
var finalTime = properties?.Time ?? currentTime;
var postEntity = new TimelinePostEntity
{
AuthorId = authorId,
TimelineId = timelineId,
Time = finalTime,
LastUpdated = currentTime,
Color = properties?.Color
};
await saveContent(postEntity);
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();
return postEntity;
}
public async Task CreateTextPost(long timelineId, long authorId, string text, TimelinePostCommonProperties? properties = null)
{
if (text is null)
throw new ArgumentNullException(nameof(text));
return await GeneralCreatePost(timelineId, authorId, properties, (entity) =>
{
entity.ContentType = TimelinePostContentTypes.Text;
entity.Content = text;
return Task.CompletedTask;
});
}
public async Task CreateImagePost(long timelineId, long authorId, byte[] data, TimelinePostCommonProperties? properties = null)
{
if (data is null)
throw new ArgumentNullException(nameof(data));
await CheckTimelineExistence(timelineId);
return await GeneralCreatePost(timelineId, authorId, properties, async (entity) =>
{
var imageFormat = await _imageValidator.Validate(data);
var imageFormatText = imageFormat.DefaultMimeType;
var tag = await _dataManager.RetainEntry(data);
entity.ContentType = TimelinePostContentTypes.Image;
entity.Content = tag;
entity.ExtraContent = imageFormatText;
});
}
public async Task DeletePost(long timelineId, long postId)
{
await CheckTimelineExistence(timelineId);
var post = await _database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == postId).SingleOrDefaultAsync();
if (post == null)
throw new TimelinePostNotExistException(timelineId, postId, false);
if (post.Content == null)
throw new TimelinePostNotExistException(timelineId, postId, 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(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;
}
}
}