using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using SixLabors.ImageSharp; 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.Models.Validation; using Timeline.Services.Exceptions; using static Timeline.Resources.Services.TimelineService; namespace Timeline.Services { public static class TimelineHelper { public static string ExtractTimelineName(string name, out bool isPersonal) { if (name.StartsWith("@", StringComparison.OrdinalIgnoreCase)) { isPersonal = true; return name.Substring(1); } else { isPersonal = false; return name; } } } public enum TimelineUserRelationshipType { Own = 0b1, Join = 0b10, Default = Own | Join } public class TimelineUserRelationship { public TimelineUserRelationship(TimelineUserRelationshipType type, long userId) { Type = type; UserId = userId; } public TimelineUserRelationshipType Type { get; set; } public long UserId { get; set; } } 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? } /// /// This define the interface of both personal timeline and ordinary timeline. /// public interface ITimelineService { /// /// Get the timeline last modified time (not include name change). /// /// The name of the timeline. /// The timeline info. /// 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 GetTimelineLastModifiedTime(string timelineName); /// /// Get the timeline unique id. /// /// The name of the timeline. /// The timeline info. /// 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 GetTimelineUniqueId(string timelineName); /// /// Get the timeline info. /// /// The name of the timeline. /// The timeline info. /// 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 GetTimeline(string timelineName); /// /// Set the properties of a timeline. /// /// The name of the timeline. /// The new properties. Null member means not to change. /// 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 . /// Task ChangeProperty(string timelineName, TimelineChangePropertyRequest newProperties); /// /// 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); /// /// Change member of timeline. /// /// The name of the timeline. /// A list of usernames of members to add. May be null. /// A list of usernames of members to remove. May be null. /// 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 names in or is not a valid username. /// Thrown when one of the user to change does not exist. /// /// Operating on a username that is of bad format or does not exist always throws. /// Add a user that already is a member has no effects. /// Remove a user that is not a member also has not effects. /// Add and remove an identical user results in no effects. /// More than one same usernames are regarded as one. /// Task ChangeMember(string timelineName, IList? membersToAdd, IList? membersToRemove); /// /// Check whether a user can manage(change timeline info, member, ...) a timeline. /// /// The name of the timeline. /// The id of the user to check on. /// True if the user can manage the timeline, otherwise false. /// 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 . /// /// /// This method does not check whether visitor is administrator. /// Return false if user with user id does not exist. /// Task HasManagePermission(string timelineName, long userId); /// /// Verify whether a visitor has the permission to read a timeline. /// /// The name of the timeline. /// The id of the user to check on. Null means visitor without account. /// True if can read, false if can't read. /// 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 . /// /// /// This method does not check whether visitor is administrator. /// Return false if user with visitor id does not exist. /// Task HasReadPermission(string timelineName, long? visitorId); /// /// 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); /// /// Verify whether a user is member of a timeline. /// /// The name of the timeline. /// The id of user to check on. /// True if it is a member, false if not. /// 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 . /// /// /// Timeline owner is also considered as a member. /// Return false when user with user id does not exist. /// Task IsMemberOf(string timelineName, long userId); /// /// Get all timelines including personal and ordinary timelines. /// /// Filter timelines related (own or is a member) to specific user. /// Filter timelines with given visibility. If null or empty, all visibilities are returned. Duplicate value are ignored. /// The list of timelines. /// /// If user with related user id does not exist, empty list will be returned. /// Task> GetTimelines(TimelineUserRelationship? relate = null, List? visibility = null); /// /// Create a timeline. /// /// The name of the timeline. /// The id of owner of the timeline. /// The info of the new timeline. /// Thrown when is null. /// Thrown when timeline name is invalid. /// Thrown when the timeline already exists. /// Thrown when the owner user does not exist. Task CreateTimeline(string timelineName, long ownerId); /// /// Delete a timeline. /// /// The name of the timeline to delete. /// Thrown when is null. /// Thrown when timeline name is invalid. /// Thrown when the timeline does not exist. Task DeleteTimeline(string timelineName); } public class TimelineService : ITimelineService { public TimelineService(ILogger logger, DatabaseContext database, IDataManager dataManager, IUserService userService, IImageValidator imageValidator, IClock clock) { _logger = logger; _database = database; _dataManager = dataManager; _userService = userService; _imageValidator = imageValidator; _clock = clock; } private readonly ILogger _logger; private readonly DatabaseContext _database; private readonly IDataManager _dataManager; private readonly IUserService _userService; private readonly IImageValidator _imageValidator; private readonly IClock _clock; private readonly UsernameValidator _usernameValidator = new UsernameValidator(); private readonly TimelineNameValidator _timelineNameValidator = new TimelineNameValidator(); private void ValidateTimelineName(string name, string paramName) { if (!_timelineNameValidator.Validate(name, out var message)) { throw new ArgumentException(ExceptionTimelineNameBadFormat.AppendAdditionalMessage(message), paramName); } } private async Task MapTimelineFromEntity(TimelineEntity entity) { var owner = await _userService.GetUserById(entity.OwnerId); var members = new List(); foreach (var memberEntity in entity.Members) { members.Add(await _userService.GetUserById(memberEntity.UserId)); } return new Models.Timeline { UniqueID = entity.UniqueId, Name = entity.Name ?? ("@" + owner.Username), NameLastModified = entity.NameLastModified, Description = entity.Description ?? "", Owner = owner, Visibility = entity.Visibility, Members = members, CreateTime = entity.CreateTime, LastModified = entity.LastModified }; } private async Task MapTimelinePostFromEntity(TimelinePostEntity entity, string timelineName) { User? author = entity.AuthorId.HasValue ? await _userService.GetUserById(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 ); } private TimelineEntity CreateNewTimelineEntity(string? name, long ownerId) { var currentTime = _clock.GetCurrentTime(); return new TimelineEntity { Name = name, NameLastModified = currentTime, OwnerId = ownerId, Visibility = TimelineVisibility.Register, CreateTime = currentTime, LastModified = currentTime, CurrentPostLocalId = 0, Members = new List() }; } // Get timeline id by name. If it is a personal timeline and it does not exist, it will be created. // // This method will check the name format and if it is invalid, ArgumentException is thrown. // // For personal timeline, if the user does not exist, TimelineNotExistException will be thrown with UserNotExistException as inner exception. // For ordinary timeline, if the timeline does not exist, TimelineNotExistException will be thrown. // // It follows all timeline-related function common interface contracts. private async Task FindTimelineId(string timelineName) { timelineName = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal); if (isPersonal) { long userId; try { userId = await _userService.GetUserIdByUsername(timelineName); } catch (ArgumentException e) { throw new ArgumentException(ExceptionFindTimelineUsernameBadFormat, nameof(timelineName), e); } catch (UserNotExistException e) { throw new TimelineNotExistException(timelineName, e); } var timelineEntity = await _database.Timelines.Where(t => t.OwnerId == userId && t.Name == null).Select(t => new { t.Id }).SingleOrDefaultAsync(); if (timelineEntity != null) { return timelineEntity.Id; } else { var newTimelineEntity = CreateNewTimelineEntity(null, userId); _database.Timelines.Add(newTimelineEntity); await _database.SaveChangesAsync(); return newTimelineEntity.Id; } } else { if (timelineName == null) throw new ArgumentNullException(nameof(timelineName)); ValidateTimelineName(timelineName, nameof(timelineName)); var timelineEntity = await _database.Timelines.Where(t => t.Name == timelineName).Select(t => new { t.Id }).SingleOrDefaultAsync(); if (timelineEntity == null) { throw new TimelineNotExistException(timelineName); } else { return timelineEntity.Id; } } } public async Task GetTimelineLastModifiedTime(string timelineName) { if (timelineName == null) throw new ArgumentNullException(nameof(timelineName)); var timelineId = await FindTimelineId(timelineName); var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.LastModified }).SingleAsync(); return timelineEntity.LastModified; } public async Task GetTimelineUniqueId(string timelineName) { if (timelineName == null) throw new ArgumentNullException(nameof(timelineName)); var timelineId = await FindTimelineId(timelineName); var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.UniqueId }).SingleAsync(); return timelineEntity.UniqueId; } public async Task GetTimeline(string timelineName) { if (timelineName == null) throw new ArgumentNullException(nameof(timelineName)); var timelineId = await FindTimelineId(timelineName); var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Include(t => t.Members).SingleAsync(); return await MapTimelineFromEntity(timelineEntity); } 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 FindTimelineId(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 FindTimelineId(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 FindTimelineId(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 FindTimelineId(timelineName); var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync(); var author = await _userService.GetUserById(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 FindTimelineId(timelineName); var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync(); var author = await _userService.GetUserById(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 FindTimelineId(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 ChangeProperty(string timelineName, TimelineChangePropertyRequest newProperties) { if (timelineName == null) throw new ArgumentNullException(nameof(timelineName)); if (newProperties == null) throw new ArgumentNullException(nameof(newProperties)); var timelineId = await FindTimelineId(timelineName); var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync(); var changed = false; if (newProperties.Description != null) { changed = true; timelineEntity.Description = newProperties.Description; } if (newProperties.Visibility.HasValue) { changed = true; timelineEntity.Visibility = newProperties.Visibility.Value; } if (changed) { var currentTime = _clock.GetCurrentTime(); timelineEntity.LastModified = currentTime; } await _database.SaveChangesAsync(); } public async Task ChangeMember(string timelineName, IList? add, IList? remove) { if (timelineName == null) throw new ArgumentNullException(nameof(timelineName)); List? RemoveDuplicateAndCheckFormat(IList? list, string paramName) { if (list != null) { List result = new List(); var count = list.Count; for (var index = 0; index < count; index++) { var username = list[index]; if (result.Contains(username)) { continue; } var (validationResult, message) = _usernameValidator.Validate(username); if (!validationResult) throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, ExceptionChangeMemberUsernameBadFormat, index), nameof(paramName)); result.Add(username); } return result; } else { return null; } } var simplifiedAdd = RemoveDuplicateAndCheckFormat(add, nameof(add)); var simplifiedRemove = RemoveDuplicateAndCheckFormat(remove, nameof(remove)); // remove those both in add and remove if (simplifiedAdd != null && simplifiedRemove != null) { var usersToClean = simplifiedRemove.Where(u => simplifiedAdd.Contains(u)).ToList(); foreach (var u in usersToClean) { simplifiedAdd.Remove(u); simplifiedRemove.Remove(u); } if (simplifiedAdd.Count == 0) simplifiedAdd = null; if (simplifiedRemove.Count == 0) simplifiedRemove = null; } if (simplifiedAdd == null && simplifiedRemove == null) return; var timelineId = await FindTimelineId(timelineName); async Task?> CheckExistenceAndGetId(List? list) { if (list == null) return null; List result = new List(); foreach (var username in list) { result.Add(await _userService.GetUserIdByUsername(username)); } return result; } var userIdsAdd = await CheckExistenceAndGetId(simplifiedAdd); var userIdsRemove = await CheckExistenceAndGetId(simplifiedRemove); if (userIdsAdd != null) { var membersToAdd = userIdsAdd.Select(id => new TimelineMemberEntity { UserId = id, TimelineId = timelineId }).ToList(); _database.TimelineMembers.AddRange(membersToAdd); } if (userIdsRemove != null) { var membersToRemove = await _database.TimelineMembers.Where(m => m.TimelineId == timelineId && userIdsRemove.Contains(m.UserId)).ToListAsync(); _database.TimelineMembers.RemoveRange(membersToRemove); } var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync(); timelineEntity.LastModified = _clock.GetCurrentTime(); await _database.SaveChangesAsync(); } public async Task HasManagePermission(string timelineName, long userId) { if (timelineName == null) throw new ArgumentNullException(nameof(timelineName)); var timelineId = await FindTimelineId(timelineName); var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.OwnerId }).SingleAsync(); return userId == timelineEntity.OwnerId; } public async Task HasReadPermission(string timelineName, long? visitorId) { if (timelineName == null) throw new ArgumentNullException(nameof(timelineName)); var timelineId = await FindTimelineId(timelineName); var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.Visibility }).SingleAsync(); if (timelineEntity.Visibility == TimelineVisibility.Public) return true; if (timelineEntity.Visibility == TimelineVisibility.Register && visitorId != null) return true; if (visitorId == null) { return false; } else { var memberEntity = await _database.TimelineMembers.Where(m => m.UserId == visitorId && m.TimelineId == timelineId).SingleOrDefaultAsync(); return memberEntity != null; } } public async Task HasPostModifyPermission(string timelineName, long postId, long modifierId, bool throwOnPostNotExist = false) { if (timelineName == null) throw new ArgumentNullException(nameof(timelineName)); var timelineId = await FindTimelineId(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; } public async Task IsMemberOf(string timelineName, long userId) { if (timelineName == null) throw new ArgumentNullException(nameof(timelineName)); var timelineId = await FindTimelineId(timelineName); var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.OwnerId }).SingleAsync(); if (userId == timelineEntity.OwnerId) return true; return await _database.TimelineMembers.AnyAsync(m => m.TimelineId == timelineId && m.UserId == userId); } public async Task> GetTimelines(TimelineUserRelationship? relate = null, List? visibility = null) { List entities; IQueryable ApplyTimelineVisibilityFilter(IQueryable query) { if (visibility != null && visibility.Count != 0) { return query.Where(t => visibility.Contains(t.Visibility)); } return query; } bool allVisibilities = visibility == null || visibility.Count == 0; if (relate == null) { entities = await ApplyTimelineVisibilityFilter(_database.Timelines).Include(t => t.Members).ToListAsync(); } else { entities = new List(); if ((relate.Type & TimelineUserRelationshipType.Own) != 0) { entities.AddRange(await ApplyTimelineVisibilityFilter(_database.Timelines.Where(t => t.OwnerId == relate.UserId)).Include(t => t.Members).ToListAsync()); } if ((relate.Type & TimelineUserRelationshipType.Join) != 0) { entities.AddRange(await ApplyTimelineVisibilityFilter(_database.TimelineMembers.Where(m => m.UserId == relate.UserId).Include(m => m.Timeline).ThenInclude(t => t.Members).Select(m => m.Timeline)).ToListAsync()); } } var result = new List(); foreach (var entity in entities) { result.Add(await MapTimelineFromEntity(entity)); } return result; } public async Task CreateTimeline(string name, long owner) { if (name == null) throw new ArgumentNullException(nameof(name)); ValidateTimelineName(name, nameof(name)); var user = await _userService.GetUserById(owner); var conflict = await _database.Timelines.AnyAsync(t => t.Name == name); if (conflict) throw new EntityAlreadyExistException(EntityNames.Timeline, null, ExceptionTimelineNameConflict); var newEntity = CreateNewTimelineEntity(name, user.Id!.Value); _database.Timelines.Add(newEntity); await _database.SaveChangesAsync(); return await MapTimelineFromEntity(newEntity); } public async Task DeleteTimeline(string name) { if (name == null) throw new ArgumentNullException(nameof(name)); ValidateTimelineName(name, nameof(name)); var entity = await _database.Timelines.Where(t => t.Name == name).SingleOrDefaultAsync(); if (entity == null) throw new TimelineNotExistException(name); _database.Timelines.Remove(entity); await _database.SaveChangesAsync(); } } }