using Microsoft.EntityFrameworkCore; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Timeline.Entities; using Timeline.Models; using Timeline.Models.Http; using Timeline.Models.Validation; namespace Timeline.Services { /// /// This define the common interface of both personal timeline /// and normal timeline. /// /// /// The "name" parameter in method means name of timeline in /// while username of the owner /// of the personal timeline in . /// public interface IBaseTimelineService { /// /// Get all the posts in the timeline. /// /// Username or the timeline name. See remarks of . /// A list of all posts. /// Thrown when is null. /// /// Thrown when timeline name is of bad format. /// For normal timeline, it means name is an empty string. /// For personal timeline, it means the username is of bad format, /// the inner exception should be a . /// /// /// Thrown when timeline does not exist. /// For normal timeline, it means the name does not exist. /// For personal timeline, it means the user of that username does not exist /// and the inner exception should be a . /// Task> GetPosts(string name); /// /// Create a new post in timeline. /// /// Username or the timeline name. See remarks of . /// The author's username. /// The content. /// The time of the post. If null, then use current time. /// The info of the created post. /// Thrown when or or is null. /// /// Thrown when timeline name is of bad format. /// For normal timeline, it means name is an empty string. /// For personal timeline, it means the username is of bad format, /// the inner exception should be a . /// /// /// Thrown when timeline does not exist. /// For normal timeline, it means the name does not exist. /// For personal timeline, it means the user of that username does not exist /// and the inner exception should be a . /// /// Thrown if is of bad format. /// Thrown if does not exist. Task CreatePost(string name, string author, string content, DateTime? time); /// /// Delete a post /// /// Username or the timeline name. See remarks of . /// The id of the post to delete. /// Thrown when or is null. /// /// Thrown when timeline name is of bad format. /// For normal timeline, it means name is an empty string. /// For personal timeline, it means the username is of bad format, /// the inner exception should be a . /// /// /// Thrown when timeline does not exist. /// For normal timeline, it means the name does not exist. /// For personal timeline, it means the user of that username does not exist /// and the inner exception should be a . /// /// /// Thrown when the post with given id does not exist or is deleted already. /// /// /// First use /// to check the permission. /// Task DeletePost(string name, long id); /// /// Set the properties of a timeline. /// /// Username or the timeline name. See remarks of . /// The new properties. Null member means not to change. /// Thrown when or is null. /// /// Thrown when timeline name is of bad format. /// For normal timeline, it means name is an empty string. /// For personal timeline, it means the username is of bad format, /// the inner exception should be a . /// /// /// Thrown when timeline does not exist. /// For normal timeline, it means the name does not exist. /// For personal timeline, it means the user of that username does not exist /// and the inner exception should be a . /// Task ChangeProperty(string name, TimelinePropertyChangeRequest newProperties); /// /// Remove members to a timeline. /// /// Username or the timeline name. See remarks of . /// 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. /// /// Thrown when timeline name is of bad format. /// For normal timeline, it means name is an empty string. /// For personal timeline, it means the username is of bad format, /// the inner exception should be a . /// /// /// Thrown when timeline does not exist. /// For normal timeline, it means the name does not exist. /// For personal timeline, it means the user of that username does not exist /// and the inner exception should be a . /// /// /// Thrown when an exception occurs on the user list. /// The inner exception is /// when one of the username is invalid. /// The inner exception is /// 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 name, IList? add, IList? remove); /// /// Verify whether a visitor has the permission to read a timeline. /// /// Username or the timeline name. See remarks of . /// The user to check on. Null means visitor without account. /// True if can read, false if can't read. /// Thrown when is null. /// /// Thrown when timeline name is of bad format. /// For normal timeline, it means name is an empty string. /// For personal timeline, it means the username is of bad format, /// the inner exception should be a . /// /// /// Thrown when timeline does not exist. /// For normal timeline, it means the name does not exist. /// For personal timeline, it means the user of that username does not exist /// and the inner exception should be a . /// /// /// Thrown when is of bad format. /// /// /// Thrown when does not exist. /// Task HasReadPermission(string name, string? username); /// /// Verify whether a user has the permission to modify a post. /// /// Username or the timeline name. See remarks of . /// The user to check on. /// True if can modify, false if can't modify. /// Thrown when or is null. /// /// Thrown when timeline name is of bad format. /// For normal timeline, it means name is an empty string. /// For personal timeline, it means the username is of bad format, /// the inner exception should be a . /// /// /// Thrown when timeline does not exist. /// For normal timeline, it means the name does not exist. /// For personal timeline, it means the user of that username does not exist /// and the inner exception should be a . /// /// /// Thrown when the post with given id does not exist or is deleted already. /// /// /// Thrown when is of bad format. /// /// /// Thrown when does not exist. /// /// /// 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. /// Task HasPostModifyPermission(string name, long id, string username); /// /// Verify whether a user is member of a timeline. /// /// Username or the timeline name. See remarks of . /// The user to check on. /// Thrown when or is null. /// /// Thrown when timeline name is of bad format. /// For normal timeline, it means name is an empty string. /// For personal timeline, it means the username is of bad format, /// the inner exception should be a . /// /// /// Thrown when timeline does not exist. /// For normal timeline, it means the name does not exist. /// For personal timeline, it means the user of that username does not exist /// and the inner exception should be a . /// /// /// Thrown when is not a valid username. /// /// /// Thrown when user does not exist. /// True if it is a member, false if not. Task IsMemberOf(string name, string username); } /// /// Service for normal timeline. /// public interface ITimelineService : IBaseTimelineService { /// /// Get the timeline info. /// /// The name of the timeline. /// The timeline info. /// Thrown when is null. /// /// Thrown when timeline name is invalid. Currently it means it is an empty string. /// /// /// Thrown when timeline with the name does not exist. /// Task GetTimeline(string name); /// /// Create a timeline. /// /// The name of the timeline. /// The owner of the timeline. /// Thrown when or is null. /// /// Thrown when timeline name is invalid. Currently it means it is an empty string. /// /// /// Thrown when the timeline already exists. /// /// /// Thrown when the username of the owner is not valid. /// /// /// Thrown when the owner user does not exist. Task CreateTimeline(string name, string owner); } public interface IPersonalTimelineService : IBaseTimelineService { /// /// Get the timeline info. /// /// The username of the owner of the personal timeline. /// The timeline info. /// /// Thrown when is null. /// /// /// Thrown when is of bad format. Inner exception MUST be . /// /// /// Thrown when the user does not exist. Inner exception MUST be . /// Task GetTimeline(string username); } public abstract class BaseTimelineService : IBaseTimelineService { protected BaseTimelineService(DatabaseContext database, IClock clock) { Clock = clock; Database = database; } protected IClock Clock { get; } protected UsernameValidator UsernameValidator { get; } = new UsernameValidator(); protected DatabaseContext Database { get; } /// /// Find the timeline id by the name. /// For details, see remarks. /// /// The username or the timeline name. See remarks. /// The id of the timeline entity. /// Thrown when is null. /// /// Thrown when timeline name is of bad format. /// For normal timeline, it means name is an empty string. /// For personal timeline, it means the username is of bad format, /// the inner exception should be a . /// /// /// Thrown when timeline does not exist. /// For normal timeline, it means the name does not exist. /// For personal timeline, it means the user of that username does not exist /// and the inner exception should be a . /// /// /// This is the common but different part for both types of timeline service. /// For class that implements , this method should /// find the timeline entity id by the given as the username of the owner. /// For class that implements , this method should /// find the timeline entity id by the given as the timeline name. /// This method should be called by many other method that follows the contract. /// protected abstract Task FindTimelineId(string name); public async Task> GetPosts(string name) { if (name == null) throw new ArgumentNullException(nameof(name)); var timelineId = await FindTimelineId(name); var postEntities = await Database.TimelinePosts.Where(p => p.TimelineId == timelineId).ToListAsync(); var posts = new List(await Task.WhenAll(postEntities.Select(async p => new TimelinePostInfo { Id = p.Id, Content = p.Content, Author = (await Database.Users.Where(u => u.Id == p.AuthorId).Select(u => new { u.Name }).SingleAsync()).Name, Time = p.Time }))); return posts; } public async Task CreatePost(string name, string author, string content, DateTime? time) { if (name == null) throw new ArgumentNullException(nameof(name)); if (author == null) throw new ArgumentNullException(nameof(author)); if (content == null) throw new ArgumentNullException(nameof(content)); { var (result, message) = UsernameValidator.Validate(author); if (!result) { throw new UsernameBadFormatException(author, message); } } var timelineId = await FindTimelineId(name); var authorEntity = Database.Users.Where(u => u.Name == author).Select(u => new { u.Id }).SingleOrDefault(); if (authorEntity == null) { throw new UserNotExistException(author); } var authorId = authorEntity.Id; var currentTime = Clock.GetCurrentTime(); var postEntity = new TimelinePostEntity { Content = content, AuthorId = authorId, TimelineId = timelineId, Time = time ?? currentTime, LastUpdated = currentTime }; Database.TimelinePosts.Add(postEntity); await Database.SaveChangesAsync(); return new TimelinePostCreateResponse { Id = postEntity.Id, Time = postEntity.Time }; } public async Task DeletePost(string name, long id) { if (name == null) throw new ArgumentNullException(nameof(name)); var timelineId = FindTimelineId(name); var post = await Database.TimelinePosts.Where(p => p.Id == id).SingleOrDefaultAsync(); if (post == null) throw new TimelinePostNotExistException(id); Database.TimelinePosts.Remove(post); await Database.SaveChangesAsync(); } public async Task ChangeProperty(string name, TimelinePropertyChangeRequest newProperties) { if (name == null) throw new ArgumentNullException(nameof(name)); if (newProperties == null) throw new ArgumentNullException(nameof(newProperties)); var timelineId = await FindTimelineId(name); var timelineEntity = await Database.Timelines.Where(t => t.Id == timelineId).SingleAsync(); if (newProperties.Description != null) { timelineEntity.Description = newProperties.Description; } if (newProperties.Visibility.HasValue) { timelineEntity.Visibility = newProperties.Visibility.Value; } await Database.SaveChangesAsync(); } public async Task ChangeMember(string name, IList? add, IList? remove) { if (name == null) throw new ArgumentNullException(nameof(name)); // remove duplication and check the format of each username. // Return a username->index map. Dictionary? RemoveDuplicateAndCheckFormat(IList? list, TimelineMemberOperationUserException.MemberOperation operation) { if (list != null) { Dictionary result = new Dictionary(); var count = 0; for (var index = 0; index < count; index++) { var username = list[index]; if (result.ContainsKey(username)) { continue; } var (validationResult, message) = UsernameValidator.Validate(username); if (!validationResult) throw new TimelineMemberOperationUserException( index, operation, username, new UsernameBadFormatException(username, message)); result.Add(username, index); } return result; } else { return null; } } var simplifiedAdd = RemoveDuplicateAndCheckFormat(add, TimelineMemberOperationUserException.MemberOperation.Add); var simplifiedRemove = RemoveDuplicateAndCheckFormat(remove, TimelineMemberOperationUserException.MemberOperation.Remove); // remove those both in add and remove if (simplifiedAdd != null && simplifiedRemove != null) { var usersToClean = simplifiedRemove.Keys.Where(u => simplifiedAdd.ContainsKey(u)); foreach (var u in usersToClean) { simplifiedAdd.Remove(u); simplifiedRemove.Remove(u); } } var timelineId = await FindTimelineId(name); async Task?> CheckExistenceAndGetId(Dictionary? map, TimelineMemberOperationUserException.MemberOperation operation) { if (map == null) return null; List result = new List(); foreach (var (username, index) in map) { var user = await Database.Users.Where(u => u.Name == username).Select(u => new { u.Id }).SingleOrDefaultAsync(); if (user == null) { throw new TimelineMemberOperationUserException(index, operation, username, new UserNotExistException(username)); } result.Add(user.Id); } return result; } var userIdsAdd = await CheckExistenceAndGetId(simplifiedAdd, TimelineMemberOperationUserException.MemberOperation.Add); var userIdsRemove = await CheckExistenceAndGetId(simplifiedRemove, TimelineMemberOperationUserException.MemberOperation.Remove); 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); } await Database.SaveChangesAsync(); } public async Task HasReadPermission(string name, string? username) { if (name == null) throw new ArgumentNullException(nameof(name)); long? userId = null; if (username != null) { var (result, message) = UsernameValidator.Validate(username); if (!result) { throw new UsernameBadFormatException(username); } var user = await Database.Users.Where(u => u.Name == username).Select(u => new { u.Id }).SingleOrDefaultAsync(); if (user == null) { throw new UserNotExistException(username); } userId = user.Id; } var timelineId = await FindTimelineId(name); 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 && username != null) return true; if (userId == null) { return false; } else { var memberEntity = await Database.TimelineMembers.Where(m => m.UserId == userId && m.TimelineId == timelineId).SingleOrDefaultAsync(); return memberEntity != null; } } public async Task HasPostModifyPermission(string name, long id, string username) { if (name == null) throw new ArgumentNullException(nameof(name)); if (username == null) throw new ArgumentNullException(nameof(username)); { var (result, message) = UsernameValidator.Validate(username); if (!result) { throw new UsernameBadFormatException(username); } } var user = await Database.Users.Where(u => u.Name == username).Select(u => new { u.Id }).SingleOrDefaultAsync(); if (user == null) { throw new UserNotExistException(username); } var userId = user.Id; var timelineId = await FindTimelineId(name); var timelineEntity = await Database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.OwnerId }).SingleAsync(); var postEntitu = await Database.Timelines. // TODO! if (timelineEntity.OwnerId == userId) { return true; } } } }