From a1e6182c205f726b33c47438a8334449ca92d411 Mon Sep 17 00:00:00 2001 From: 杨宇千 Date: Wed, 13 Nov 2019 22:55:31 +0800 Subject: WIP : Write timeline service. --- Timeline/Services/TimelineService.cs | 336 ++++++++++++++++++++++++++++++++++- 1 file changed, 330 insertions(+), 6 deletions(-) (limited to 'Timeline/Services/TimelineService.cs') diff --git a/Timeline/Services/TimelineService.cs b/Timeline/Services/TimelineService.cs index 28b1f91d..eff0c3fc 100644 --- a/Timeline/Services/TimelineService.cs +++ b/Timeline/Services/TimelineService.cs @@ -1,10 +1,12 @@ -using System; +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 { @@ -135,14 +137,14 @@ namespace Timeline.Services /// The inner exception is /// when one of the username is invalid. /// The inner exception is - /// when one of the user to add does not exist. + /// when one of the user to change does not exist. /// /// - /// Operating on a username that is of bad format always throws. + /// 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 a user that does not exist will throw . - /// But remove one does not throw. + /// 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); @@ -151,6 +153,7 @@ namespace Timeline.Services /// /// 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. @@ -164,7 +167,12 @@ namespace Timeline.Services /// For personal timeline, it means the user of that username does not exist /// and the inner exception should be a . /// - /// True if can read, false if can't read. + /// + /// Thrown when is of bad format. + /// + /// + /// Thrown when does not exist. + /// Task HasReadPermission(string name, string? username); /// @@ -285,4 +293,320 @@ namespace Timeline.Services /// 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; + } + } + + } } -- cgit v1.2.3