From 4a46206ea5f004ecb595de4bfd573b6263ac462b Mon Sep 17 00:00:00 2001 From: crupest Date: Wed, 17 Jun 2020 17:10:40 +0800 Subject: refactor(back): Refactor timeline service. --- Timeline/Controllers/TimelineController.cs | 2 +- Timeline/Resources/Services/Exception.Designer.cs | 45 - Timeline/Resources/Services/Exception.resx | 15 - Timeline/Resources/Services/Exceptions.Designer.cs | 56 +- Timeline/Resources/Services/Exceptions.resx | 24 +- .../Resources/Services/TimelineService.Designer.cs | 2 +- Timeline/Resources/Services/TimelineService.resx | 2 +- Timeline/Services/BadPostTypeException.cs | 21 - Timeline/Services/Exceptions/ImageException.cs | 57 ++ .../Exceptions/TimelinePostNoDataException.cs | 15 + .../Exceptions/TimelinePostNotExistException.cs | 2 +- Timeline/Services/ImageException.cs | 54 -- Timeline/Services/ImageValidator.cs | 3 +- Timeline/Services/TimelineService.cs | 904 +++++++++------------ Timeline/Services/UserAvatarService.cs | 1 + Timeline/Startup.cs | 2 - 16 files changed, 538 insertions(+), 667 deletions(-) delete mode 100644 Timeline/Services/BadPostTypeException.cs create mode 100644 Timeline/Services/Exceptions/ImageException.cs create mode 100644 Timeline/Services/Exceptions/TimelinePostNoDataException.cs delete mode 100644 Timeline/Services/ImageException.cs diff --git a/Timeline/Controllers/TimelineController.cs b/Timeline/Controllers/TimelineController.cs index 6c7cfa95..36e9fe6d 100644 --- a/Timeline/Controllers/TimelineController.cs +++ b/Timeline/Controllers/TimelineController.cs @@ -135,7 +135,7 @@ namespace Timeline.Controllers { return NotFound(ErrorResponse.TimelineController.PostNotExist()); } - catch (BadPostTypeException) + catch (TimelinePostNoDataException) { return BadRequest(ErrorResponse.TimelineController.PostNoData()); } diff --git a/Timeline/Resources/Services/Exception.Designer.cs b/Timeline/Resources/Services/Exception.Designer.cs index 889aa1a7..21ca7b86 100644 --- a/Timeline/Resources/Services/Exception.Designer.cs +++ b/Timeline/Resources/Services/Exception.Designer.cs @@ -132,51 +132,6 @@ namespace Timeline.Resources.Services { } } - /// - /// Looks up a localized string similar to Image is in valid because {0}.. - /// - internal static string ImageException { - get { - return ResourceManager.GetString("ImageException", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to image is not a square, aka, width is not equal to height. - /// - internal static string ImageExceptionBadSize { - get { - return ResourceManager.GetString("ImageExceptionBadSize", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to failed to decode image, see inner exception. - /// - internal static string ImageExceptionCantDecode { - get { - return ResourceManager.GetString("ImageExceptionCantDecode", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to unknown error. - /// - internal static string ImageExceptionUnknownError { - get { - return ResourceManager.GetString("ImageExceptionUnknownError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to image's actual mime type is not the specified one. - /// - internal static string ImageExceptionUnmatchedFormat { - get { - return ResourceManager.GetString("ImageExceptionUnmatchedFormat", resourceCulture); - } - } - /// /// Looks up a localized string similar to The token didn't pass verification because {0}.. /// diff --git a/Timeline/Resources/Services/Exception.resx b/Timeline/Resources/Services/Exception.resx index d455f0fc..c31ed7c7 100644 --- a/Timeline/Resources/Services/Exception.resx +++ b/Timeline/Resources/Services/Exception.resx @@ -141,21 +141,6 @@ Unknown format marker. - - Image is in valid because {0}. - - - image is not a square, aka, width is not equal to height - - - failed to decode image, see inner exception - - - unknown error - - - image's actual mime type is not the specified one - The token didn't pass verification because {0}. diff --git a/Timeline/Resources/Services/Exceptions.Designer.cs b/Timeline/Resources/Services/Exceptions.Designer.cs index 84439716..1dbe11c9 100644 --- a/Timeline/Resources/Services/Exceptions.Designer.cs +++ b/Timeline/Resources/Services/Exceptions.Designer.cs @@ -96,6 +96,60 @@ namespace Timeline.Resources.Services { } } + /// + /// Looks up a localized string similar to Image is in valid because {0}.. + /// + internal static string ImageException { + get { + return ResourceManager.GetString("ImageException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to image is not of required size. + /// + internal static string ImageExceptionBadSize { + get { + return ResourceManager.GetString("ImageExceptionBadSize", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to failed to decode image, see inner exception. + /// + internal static string ImageExceptionCantDecode { + get { + return ResourceManager.GetString("ImageExceptionCantDecode", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to unknown error. + /// + internal static string ImageExceptionUnknownError { + get { + return ResourceManager.GetString("ImageExceptionUnknownError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to image's actual mime type is not the specified one. + /// + internal static string ImageExceptionUnmatchedFormat { + get { + return ResourceManager.GetString("ImageExceptionUnmatchedFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The timeline has no data.. + /// + internal static string TimelineNoDataException { + get { + return ResourceManager.GetString("TimelineNoDataException", resourceCulture); + } + } + /// /// Looks up a localized string similar to Request timeline name is "{0}". If this is a personal timeline whose name starts with '@', it means the user does not exist and inner exception should be a UserNotExistException.. /// @@ -115,7 +169,7 @@ namespace Timeline.Resources.Services { } /// - /// Looks up a localized string similar to Request timeline name is "{0}". Request timeline post id is "{1}". The post does not exist because it has been deleted.. + /// Looks up a localized string similar to Request timeline name is "{0}". Request timeline post id is "{1}". The post does not exist because it is deleted.. /// internal static string TimelinePostNotExistExceptionDeleted { get { diff --git a/Timeline/Resources/Services/Exceptions.resx b/Timeline/Resources/Services/Exceptions.resx index b064fd44..e9595caa 100644 --- a/Timeline/Resources/Services/Exceptions.resx +++ b/Timeline/Resources/Services/Exceptions.resx @@ -112,13 +112,31 @@ Request timeline name is "{0}". If this is a personal timeline whose name starts with '@', it means the user does not exist and inner exception should be a UserNotExistException. - - Request timeline name is "{0}". Request timeline post id is "{1}". The post does not exist because it has been deleted. - Request timeline name is "{0}". Request timeline post id is "{1}". Request username is "{0}". Request id is "{1}". + + The timeline has no data. + + + Image is in valid because {0}. + + + image is not of required size + + + failed to decode image, see inner exception + + + unknown error + + + image's actual mime type is not the specified one + + + Request timeline name is "{0}". Request timeline post id is "{1}". The post does not exist because it is deleted. + \ No newline at end of file diff --git a/Timeline/Resources/Services/TimelineService.Designer.cs b/Timeline/Resources/Services/TimelineService.Designer.cs index 4c3de1cd..cfc381f9 100644 --- a/Timeline/Resources/Services/TimelineService.Designer.cs +++ b/Timeline/Resources/Services/TimelineService.Designer.cs @@ -106,7 +106,7 @@ namespace Timeline.Resources.Services { } /// - /// Looks up a localized string similar to The timeline name is of bad format because {0}.. + /// Looks up a localized string similar to The timeline name is of bad format.. /// internal static string ExceptionTimelineNameBadFormat { get { diff --git a/Timeline/Resources/Services/TimelineService.resx b/Timeline/Resources/Services/TimelineService.resx index 97269943..c4f49b30 100644 --- a/Timeline/Resources/Services/TimelineService.resx +++ b/Timeline/Resources/Services/TimelineService.resx @@ -133,7 +133,7 @@ Can't get data of a non-image post. - The timeline name is of bad format because {0}. + The timeline name is of bad format. The timeline with given name already exists. diff --git a/Timeline/Services/BadPostTypeException.cs b/Timeline/Services/BadPostTypeException.cs deleted file mode 100644 index 795ca2b6..00000000 --- a/Timeline/Services/BadPostTypeException.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; - -namespace Timeline.Services -{ - [Serializable] - public class BadPostTypeException : Exception - { - public BadPostTypeException() { } - public BadPostTypeException(string message) : base(message) { } - public BadPostTypeException(string message, Exception inner) : base(message, inner) { } - public BadPostTypeException(string requestType, string requiredType) : this() { RequestType = requestType; RequiredType = requiredType; } - public BadPostTypeException(string requestType, string requiredType, string message) : this(message) { RequestType = requestType; RequiredType = requiredType; } - public BadPostTypeException(string requestType, string requiredType, string message, Exception inner) : this(message, inner) { RequestType = requestType; RequiredType = requiredType; } - protected BadPostTypeException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - - public string RequestType { get; set; } = ""; - public string RequiredType { get; set; } = ""; - } -} diff --git a/Timeline/Services/Exceptions/ImageException.cs b/Timeline/Services/Exceptions/ImageException.cs new file mode 100644 index 00000000..20dd48ae --- /dev/null +++ b/Timeline/Services/Exceptions/ImageException.cs @@ -0,0 +1,57 @@ +using System; +using System.Globalization; + +namespace Timeline.Services.Exceptions +{ + [Serializable] + public class ImageException : Exception + { + public enum ErrorReason + { + /// + /// Decoding image failed. + /// + CantDecode, + /// + /// Decoding succeeded but the real type is not the specified type. + /// + UnmatchedFormat, + /// + /// Image is not of required size. + /// + NotSquare, + /// + /// Other unknown errer. + /// + Unknown + } + + public ImageException() : this(null) { } + public ImageException(string? message) : this(message, null) { } + public ImageException(string? message, Exception? inner) : this(ErrorReason.Unknown, null, null, null, message, inner) { } + + public ImageException(ErrorReason error, byte[]? data, string? requestType = null, string? realType = null, string? message = null, Exception? inner = null) : base(MakeMessage(error).AppendAdditionalMessage(message), inner) { Error = error; ImageData = data; RequestType = requestType; RealType = realType; } + + protected ImageException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + + private static string MakeMessage(ErrorReason? reason) => + string.Format(CultureInfo.InvariantCulture, Resources.Services.Exceptions.ImageException, reason switch + { + ErrorReason.CantDecode => Resources.Services.Exceptions.ImageExceptionCantDecode, + ErrorReason.UnmatchedFormat => Resources.Services.Exceptions.ImageExceptionUnmatchedFormat, + ErrorReason.NotSquare => Resources.Services.Exceptions.ImageExceptionBadSize, + _ => Resources.Services.Exceptions.ImageExceptionUnknownError + }); + + public ErrorReason Error { get; } +#pragma warning disable CA1819 // Properties should not return arrays + public byte[]? ImageData { get; } +#pragma warning restore CA1819 // Properties should not return arrays + public string? RequestType { get; } + + // This field will be null if decoding failed. + public string? RealType { get; } + } +} diff --git a/Timeline/Services/Exceptions/TimelinePostNoDataException.cs b/Timeline/Services/Exceptions/TimelinePostNoDataException.cs new file mode 100644 index 00000000..c4b6bf62 --- /dev/null +++ b/Timeline/Services/Exceptions/TimelinePostNoDataException.cs @@ -0,0 +1,15 @@ +using System; + +namespace Timeline.Services.Exceptions +{ + [Serializable] + public class TimelinePostNoDataException : Exception + { + public TimelinePostNoDataException() : this(null, null) { } + public TimelinePostNoDataException(string? message) : this(message, null) { } + public TimelinePostNoDataException(string? message, Exception? inner) : base(Resources.Services.Exceptions.TimelineNoDataException.AppendAdditionalMessage(message), inner) { } + protected TimelinePostNoDataException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + } +} diff --git a/Timeline/Services/Exceptions/TimelinePostNotExistException.cs b/Timeline/Services/Exceptions/TimelinePostNotExistException.cs index bbb9e908..f95dd410 100644 --- a/Timeline/Services/Exceptions/TimelinePostNotExistException.cs +++ b/Timeline/Services/Exceptions/TimelinePostNotExistException.cs @@ -6,7 +6,7 @@ namespace Timeline.Services.Exceptions [Serializable] public class TimelinePostNotExistException : EntityNotExistException { - public TimelinePostNotExistException() { } + public TimelinePostNotExistException() : this(null, null, false, null, null) { } [Obsolete("This has no meaning.")] public TimelinePostNotExistException(string? message) : this(message, null) { } [Obsolete("This has no meaning.")] diff --git a/Timeline/Services/ImageException.cs b/Timeline/Services/ImageException.cs deleted file mode 100644 index c6126aa3..00000000 --- a/Timeline/Services/ImageException.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using System.Globalization; - -namespace Timeline.Services -{ - [Serializable] - public class ImageException : Exception - { - public enum ErrorReason - { - /// - /// Decoding image failed. - /// - CantDecode, - /// - /// Decoding succeeded but the real type is not the specified type. - /// - UnmatchedFormat, - /// - /// Image is not a square. - /// - NotSquare - } - - public ImageException() : base(MakeMessage(null)) { } - public ImageException(string message) : base(message) { } - public ImageException(string message, Exception inner) : base(message, inner) { } - - public ImageException(ErrorReason error, byte[]? data = null, string? requestType = null, string? realType = null) : base(MakeMessage(error)) { Error = error; ImageData = data; RequestType = requestType; RealType = realType; } - public ImageException(Exception inner, ErrorReason error, byte[]? data = null, string? requestType = null, string? realType = null) : base(MakeMessage(error), inner) { Error = error; ImageData = data; RequestType = requestType; RealType = realType; } - - protected ImageException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - - private static string MakeMessage(ErrorReason? reason) => - string.Format(CultureInfo.InvariantCulture, Resources.Services.Exception.ImageException, reason switch - { - ErrorReason.CantDecode => Resources.Services.Exception.ImageExceptionCantDecode, - ErrorReason.UnmatchedFormat => Resources.Services.Exception.ImageExceptionUnmatchedFormat, - ErrorReason.NotSquare => Resources.Services.Exception.ImageExceptionBadSize, - _ => Resources.Services.Exception.ImageExceptionUnknownError - }); - - public ErrorReason? Error { get; } -#pragma warning disable CA1819 // Properties should not return arrays - public byte[]? ImageData { get; } -#pragma warning restore CA1819 // Properties should not return arrays - public string? RequestType { get; } - - // This field will be null if decoding failed. - public string? RealType { get; } - } -} diff --git a/Timeline/Services/ImageValidator.cs b/Timeline/Services/ImageValidator.cs index c331d912..59424a7c 100644 --- a/Timeline/Services/ImageValidator.cs +++ b/Timeline/Services/ImageValidator.cs @@ -3,6 +3,7 @@ using SixLabors.ImageSharp.Formats; using System; using System.Linq; using System.Threading.Tasks; +using Timeline.Services.Exceptions; namespace Timeline.Services { @@ -44,7 +45,7 @@ namespace Timeline.Services } catch (UnknownImageFormatException e) { - throw new ImageException(e, ImageException.ErrorReason.CantDecode, data, requestType, null); + throw new ImageException(ImageException.ErrorReason.CantDecode, data, requestType, null, null, e); } }); return format; diff --git a/Timeline/Services/TimelineService.cs b/Timeline/Services/TimelineService.cs index 0b845a57..d232f3e1 100644 --- a/Timeline/Services/TimelineService.cs +++ b/Timeline/Services/TimelineService.cs @@ -41,148 +41,153 @@ namespace Timeline.Services #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; } + public DateTime? LastModified { get; set; } // TODO: Why nullable? } /// - /// This define the common interface of both personal timeline and normal timeline. + /// This define the interface of both personal timeline and ordinary timeline. /// - /// - /// The "name" parameter in each method has different meaning. - /// => name of the ordinary timeline - /// => username of the owner of the personal timeline - /// => username if begin with '@' otherwise timeline name - /// - /// is thrown when name is illegal. - /// For ordinary timeline, it means the name is not a valid timeline name. - /// For personal timeline, it means the name is not a valid username. - /// - /// is thrown when timeline does not exist. - /// For ordinary timeline, it means the timeline of the name does not exist. - /// For personal timeline, it means the user with the username does not exist and the inner exception should be a . - /// - public interface IBaseTimelineService + public interface ITimelineService { /// /// Get the timeline info. /// - /// See remarks of . + /// The name of the timeline. /// The timeline info. - /// Thrown when is null. - /// See remarks of . - /// See remarks of . - Task GetTimeline(string name); + /// 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. /// - /// See remarks of . + /// The name of the timeline. /// The new properties. Null member means not to change. - /// Thrown when or is null. - /// See remarks of . - /// See remarks of . - Task ChangeProperty(string name, TimelineChangePropertyRequest newProperties); + /// 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. /// - /// See remarks of . + /// The name of the timeline. /// A list of all posts. - /// Thrown when is null. - /// See remarks of . - /// See remarks of . - Task> GetPosts(string name); + /// 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); /// /// Get the etag of data of a post. /// - /// See remarks of . + /// The name of the timeline of the post. /// The id of the post. /// The etag of the data. - /// Thrown when is null. - /// See remarks of . - /// See remarks of . + /// 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. See remarks. + /// Thrown when post has no data. /// - Task GetPostDataETag(string name, long postId); + Task GetPostDataETag(string timelineName, long postId); /// /// Get the data of a post. /// - /// See remarks of . + /// The name of the timeline of the post. /// The id of the post. - /// The data and its type. - /// Thrown when is null. - /// See remarks of . - /// See remarks of . + /// 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. See remarks. - /// - /// Use this method to retrieve the image of image post. - /// + /// Thrown when post has no data. /// - Task GetPostData(string name, long postId); + Task GetPostData(string timelineName, long postId); /// /// Create a new text post in timeline. /// - /// See remarks of . + /// 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 use current time. + /// The time of the post. If null, then current time is used. /// The info of the created post. - /// Thrown when or is null. - /// See remarks of . - /// See remarks of . - /// Thrown if user with does not exist. - Task CreateTextPost(string name, long authorId, string text, DateTime? time); + /// 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. /// - /// See remarks of . + /// The name of the timeline to create post against. /// The author's user id. - /// The image data. + /// 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. - /// See remarks of . - /// See remarks of . - /// Thrown if user with does not exist. + /// 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 name, long authorId, byte[] data, DateTime? time); + Task CreateImagePost(string timelineName, long authorId, byte[] imageData, DateTime? time); /// /// Delete a post. /// - /// See remarks of . - /// The id of the post to delete. - /// Thrown when is null. - /// See remarks of . - /// See remarks of . - /// - /// Thrown when the post with given id does not exist or is deleted already. + /// 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. + /// First use to check the permission. /// - Task DeletePost(string name, long id); + Task DeletePost(string timelineName, long postId); /// - /// Remove members to a timeline. + /// Change member of timeline. /// - /// 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. - /// See remarks of . - /// See remarks of . - /// Thrown when names in or is not a valid username. - /// - /// Thrown when one of the user to change does not exist. + /// 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. @@ -190,79 +195,86 @@ namespace Timeline.Services /// 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); + Task ChangeMember(string timelineName, IList? membersToAdd, IList? membersToRemove); /// /// Check whether a user can manage(change timeline info, member, ...) a timeline. /// - /// See remarks of . - /// The user id. + /// 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. - /// See remarks of . - /// See remarks of . - /// + /// 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 name, long userId); + Task HasManagePermission(string timelineName, long userId); /// /// Verify whether a visitor has the permission to read a timeline. /// - /// See remarks of . + /// 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. - /// See remarks of . - /// See remarks of . - /// + /// 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 name, long? visitorId); + Task HasReadPermission(string timelineName, long? visitorId); /// /// Verify whether a user has the permission to modify a post. /// - /// See remarks of . + /// 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. - /// See remarks of . - /// See remarks of . + /// 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 name, long id, long modifierId, bool throwOnPostNotExist = false); + Task HasPostModifyPermission(string timelineName, long postId, long modifierId, bool throwOnPostNotExist = false); /// /// Verify whether a user is member of a timeline. /// - /// See remarks of . + /// 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. - /// See remarks of . - /// See remarks of . + /// 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 name, long userId); - } + Task IsMemberOf(string timelineName, long userId); - /// - /// Service for normal timeline. - /// - public interface ITimelineService : IBaseTimelineService - { /// - /// Get all timelines including personal timelines. + /// 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. @@ -275,45 +287,69 @@ namespace Timeline.Services /// /// Create a timeline. /// - /// The name of the timeline. - /// The id of owner of the 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 is null. /// Thrown when timeline name is invalid. - /// Thrown when the timeline already exists. + /// Thrown when the timeline already exists. /// Thrown when the owner user does not exist. - Task CreateTimeline(string name, long owner); + Task CreateTimeline(string timelineName, long ownerId); /// /// Delete a timeline. /// - /// The name of the timeline. - /// Thrown when is null. + /// 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 name); + Task DeleteTimeline(string timelineName); } - public interface IOrdinaryTimelineService : IBaseTimelineService + 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; - public interface IPersonalTimelineService : IBaseTimelineService - { + private readonly DatabaseContext _database; - } + private readonly IDataManager _dataManager; - internal static class TimelineServiceHelper - { - public static async Task MapTimelineFromEntity(IUserService userService, TimelineEntity entity) + 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) { - var owner = await userService.GetUserById(entity.OwnerId); + 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)); + members.Add(await _userService.GetUserById(memberEntity.UserId)); } return new Models.Timeline @@ -326,145 +362,186 @@ namespace Timeline.Services Members = members }; } - } - public abstract class BaseTimelineService : IBaseTimelineService - { - protected BaseTimelineService(ILoggerFactory loggerFactory, DatabaseContext database, IImageValidator imageValidator, IDataManager dataManager, IUserService userService, IClock clock) + private TimelineEntity CreateNewTimelineEntity(string? name, long ownerId) + { + return new TimelineEntity + { + Name = name, + OwnerId = ownerId, + Visibility = TimelineVisibility.Register, + CreateTime = _clock.GetCurrentTime(), + CurrentPostLocalId = 0, + }; + } + + private static string ExtractTimelineName(string name, out bool isPersonal) { - _logger = loggerFactory.CreateLogger(); - Clock = clock; - Database = database; - ImageValidator = imageValidator; - DataManager = dataManager; - UserService = userService; + if (name.StartsWith("@", StringComparison.OrdinalIgnoreCase)) + { + isPersonal = true; + return name.Substring(1); + } + else + { + isPersonal = false; + return name; + } } - private readonly ILogger _logger; + // 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 = ExtractTimelineName(timelineName, out var isPersonal); - protected IClock Clock { get; } + 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); + } - protected UsernameValidator UsernameValidator { get; } = new UsernameValidator(); + var timelineEntity = await _database.Timelines.Where(t => t.OwnerId == userId && t.Name == null).Select(t => new { t.Id }).SingleOrDefaultAsync(); - protected DatabaseContext Database { get; } + if (timelineEntity != null) + { + return timelineEntity.Id; + } + else + { + var newTimelineEntity = CreateNewTimelineEntity(null, userId); + _database.Timelines.Add(newTimelineEntity); + await _database.SaveChangesAsync(); - protected IImageValidator ImageValidator { get; } - protected IDataManager DataManager { get; } - protected IUserService UserService { get; } + return newTimelineEntity.Id; + } + } + else + { + if (timelineName == null) + throw new ArgumentNullException(nameof(timelineName)); - /// - /// 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 is illegal. It is not a valid timeline name (for normal timeline service) or a valid username (for personal timeline service). - /// - /// 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); + ValidateTimelineName(timelineName, nameof(timelineName)); - protected abstract string GenerateName(string name); + 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 GetTimeline(string name) + public async Task GetTimeline(string timelineName) { - if (name == null) - throw new ArgumentNullException(nameof(name)); + if (timelineName == null) + throw new ArgumentNullException(nameof(timelineName)); - var timelineId = await FindTimelineId(name); + var timelineId = await FindTimelineId(timelineName); - var timelineEntity = await Database.Timelines.Where(t => t.Id == timelineId).Include(t => t.Members).SingleAsync(); + var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Include(t => t.Members).SingleAsync(); - return await TimelineServiceHelper.MapTimelineFromEntity(UserService, timelineEntity); + return await MapTimelineFromEntity(timelineEntity); } - public async Task> GetPosts(string name) + public async Task> GetPosts(string timelineName) { - if (name == null) - throw new ArgumentNullException(nameof(name)); + if (timelineName == null) + throw new ArgumentNullException(nameof(timelineName)); - var timelineId = await FindTimelineId(name); - var postEntities = await Database.TimelinePosts.OrderBy(p => p.Time).Where(p => p.TimelineId == timelineId && p.Content != null).ToListAsync(); + var timelineId = await FindTimelineId(timelineName); + var postEntities = await _database.TimelinePosts.OrderBy(p => p.Time).Where(p => p.TimelineId == timelineId && p.Content != null).ToListAsync(); var posts = new List(); foreach (var entity in postEntities) { - if (entity.Content != null) // otherwise it is deleted + if (entity.Content == null) { - var author = await UserService.GetUserById(entity.AuthorId); + throw new Exception(); + } - var type = entity.ContentType; + var author = await _userService.GetUserById(entity.AuthorId); - ITimelinePostContent content = type switch - { - TimelinePostContentTypes.Text => new TextTimelinePostContent(entity.Content), - TimelinePostContentTypes.Image => new ImageTimelinePostContent(entity.Content), - _ => throw new DatabaseCorruptedException(string.Format(CultureInfo.InvariantCulture, ExceptionDatabaseUnknownContentType, type)) - }; - - posts.Add(new TimelinePost( - id: entity.LocalId, - content: content, - time: entity.Time, - author: author, - lastUpdated: entity.LastUpdated, - timelineName: GenerateName(name) - )); - } + var type = entity.ContentType; + + ITimelinePostContent content = type switch + { + TimelinePostContentTypes.Text => new TextTimelinePostContent(entity.Content), + TimelinePostContentTypes.Image => new ImageTimelinePostContent(entity.Content), + _ => throw new DatabaseCorruptedException(string.Format(CultureInfo.InvariantCulture, ExceptionDatabaseUnknownContentType, type)) + }; + + posts.Add(new TimelinePost( + id: entity.LocalId, + author: author, + content: content, + time: entity.Time, + lastUpdated: entity.LastUpdated, + timelineName: timelineName + )); } return posts; } - public async Task GetPostDataETag(string name, long postId) + public async Task GetPostDataETag(string timelineName, long postId) { - if (name == null) - throw new ArgumentNullException(nameof(name)); + if (timelineName == null) + throw new ArgumentNullException(nameof(timelineName)); + + var timelineId = await FindTimelineId(timelineName); - var timelineId = await FindTimelineId(name); - var postEntity = await Database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == postId).SingleOrDefaultAsync(); + var postEntity = await _database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == postId).SingleOrDefaultAsync(); if (postEntity == null) - throw new TimelinePostNotExistException(name, postId, false); + throw new TimelinePostNotExistException(timelineName, postId, false); if (postEntity.Content == null) - throw new TimelinePostNotExistException(name, postId, true); + throw new TimelinePostNotExistException(timelineName, postId, true); if (postEntity.ContentType != TimelinePostContentTypes.Image) - throw new BadPostTypeException(postEntity.ContentType, TimelinePostContentTypes.Image, ExceptionGetDataNonImagePost); + throw new TimelinePostNoDataException(ExceptionGetDataNonImagePost); var tag = postEntity.Content; return tag; } - public async Task GetPostData(string name, long postId) + public async Task GetPostData(string timelineName, long postId) { - if (name == null) - throw new ArgumentNullException(nameof(name)); + if (timelineName == null) + throw new ArgumentNullException(nameof(timelineName)); - var timelineId = await FindTimelineId(name); - var postEntity = await Database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == postId).SingleOrDefaultAsync(); + 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(name, postId, false); + throw new TimelinePostNotExistException(timelineName, postId, false); if (postEntity.Content == null) - throw new TimelinePostNotExistException(name, postId, true); + throw new TimelinePostNotExistException(timelineName, postId, true); if (postEntity.ContentType != TimelinePostContentTypes.Image) - throw new BadPostTypeException(postEntity.ContentType, TimelinePostContentTypes.Image, ExceptionGetDataNonImagePost); + throw new TimelinePostNoDataException(ExceptionGetDataNonImagePost); var tag = postEntity.Content; @@ -472,7 +549,7 @@ namespace Timeline.Services try { - data = await DataManager.GetEntry(tag); + data = await _dataManager.GetEntry(tag); } catch (InvalidOperationException e) { @@ -484,7 +561,7 @@ namespace Timeline.Services _logger.LogWarning(LogGetDataNoFormat); var format = Image.DetectFormat(data); postEntity.ExtraContent = format.DefaultMimeType; - await Database.SaveChangesAsync(); + await _database.SaveChangesAsync(); } return new PostData @@ -496,19 +573,19 @@ namespace Timeline.Services }; } - public async Task CreateTextPost(string name, long authorId, string text, DateTime? time) + public async Task CreateTextPost(string timelineName, long authorId, string text, DateTime? time) { - if (name == null) - throw new ArgumentNullException(nameof(name)); + if (timelineName == null) + throw new ArgumentNullException(nameof(timelineName)); if (text == null) throw new ArgumentNullException(nameof(text)); - var timelineId = await FindTimelineId(name); - var timelineEntity = await Database.Timelines.Where(t => t.Id == timelineId).SingleAsync(); + var timelineId = await FindTimelineId(timelineName); + var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync(); - var author = await UserService.GetUserById(authorId); + var author = await _userService.GetUserById(authorId); - var currentTime = Clock.GetCurrentTime(); + var currentTime = _clock.GetCurrentTime(); var finalTime = time ?? currentTime; timelineEntity.CurrentPostLocalId += 1; @@ -523,8 +600,8 @@ namespace Timeline.Services Time = finalTime, LastUpdated = currentTime }; - Database.TimelinePosts.Add(postEntity); - await Database.SaveChangesAsync(); + _database.TimelinePosts.Add(postEntity); + await _database.SaveChangesAsync(); return new TimelinePost( @@ -533,29 +610,29 @@ namespace Timeline.Services time: finalTime, author: author, lastUpdated: currentTime, - timelineName: GenerateName(name) + timelineName: timelineName ); } - public async Task CreateImagePost(string name, long authorId, byte[] data, DateTime? time) + public async Task CreateImagePost(string timelineName, long authorId, byte[] data, DateTime? time) { - if (name == null) - throw new ArgumentNullException(nameof(name)); + if (timelineName == null) + throw new ArgumentNullException(nameof(timelineName)); if (data == null) throw new ArgumentNullException(nameof(data)); - var timelineId = await FindTimelineId(name); - var timelineEntity = await Database.Timelines.Where(t => t.Id == timelineId).SingleAsync(); + var timelineId = await FindTimelineId(timelineName); + var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync(); - var author = await UserService.GetUserById(authorId); + var author = await _userService.GetUserById(authorId); - var imageFormat = await ImageValidator.Validate(data); + var imageFormat = await _imageValidator.Validate(data); var imageFormatText = imageFormat.DefaultMimeType; - var tag = await DataManager.RetainEntry(data); + var tag = await _dataManager.RetainEntry(data); - var currentTime = Clock.GetCurrentTime(); + var currentTime = _clock.GetCurrentTime(); var finalTime = time ?? currentTime; timelineEntity.CurrentPostLocalId += 1; @@ -571,8 +648,8 @@ namespace Timeline.Services Time = finalTime, LastUpdated = currentTime }; - Database.TimelinePosts.Add(postEntity); - await Database.SaveChangesAsync(); + _database.TimelinePosts.Add(postEntity); + await _database.SaveChangesAsync(); return new TimelinePost( id: postEntity.LocalId, @@ -580,21 +657,24 @@ namespace Timeline.Services time: finalTime, author: author, lastUpdated: currentTime, - timelineName: GenerateName(name) + timelineName: timelineName ); } - public async Task DeletePost(string name, long id) + public async Task DeletePost(string timelineName, long id) { - if (name == null) - throw new ArgumentNullException(nameof(name)); + if (timelineName == null) + throw new ArgumentNullException(nameof(timelineName)); - var timelineId = await FindTimelineId(name); + var timelineId = await FindTimelineId(timelineName); - var post = await Database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == id).SingleOrDefaultAsync(); + var post = await _database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == id).SingleOrDefaultAsync(); - if (post == null || post.Content == null) - throw new TimelinePostNotExistException(name, id, false); + if (post == null) + throw new TimelinePostNotExistException(timelineName, id, false); + + if (post.Content == null) + throw new TimelinePostNotExistException(timelineName, id, true); string? dataTag = null; @@ -604,26 +684,26 @@ namespace Timeline.Services } post.Content = null; - post.LastUpdated = Clock.GetCurrentTime(); + post.LastUpdated = _clock.GetCurrentTime(); - await Database.SaveChangesAsync(); + await _database.SaveChangesAsync(); if (dataTag != null) { - await DataManager.FreeEntry(dataTag); + await _dataManager.FreeEntry(dataTag); } } - public async Task ChangeProperty(string name, TimelineChangePropertyRequest newProperties) + public async Task ChangeProperty(string timelineName, TimelineChangePropertyRequest newProperties) { - if (name == null) - throw new ArgumentNullException(nameof(name)); + if (timelineName == null) + throw new ArgumentNullException(nameof(timelineName)); if (newProperties == null) throw new ArgumentNullException(nameof(newProperties)); - var timelineId = await FindTimelineId(name); + var timelineId = await FindTimelineId(timelineName); - var timelineEntity = await Database.Timelines.Where(t => t.Id == timelineId).SingleAsync(); + var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync(); if (newProperties.Description != null) { @@ -635,13 +715,13 @@ namespace Timeline.Services timelineEntity.Visibility = newProperties.Visibility.Value; } - await Database.SaveChangesAsync(); + await _database.SaveChangesAsync(); } - public async Task ChangeMember(string name, IList? add, IList? remove) + public async Task ChangeMember(string timelineName, IList? add, IList? remove) { - if (name == null) - throw new ArgumentNullException(nameof(name)); + if (timelineName == null) + throw new ArgumentNullException(nameof(timelineName)); List? RemoveDuplicateAndCheckFormat(IList? list, string paramName) { @@ -656,7 +736,7 @@ namespace Timeline.Services { continue; } - var (validationResult, message) = UsernameValidator.Validate(username); + var (validationResult, message) = _usernameValidator.Validate(username); if (!validationResult) throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, ExceptionChangeMemberUsernameBadFormat, index), nameof(paramName)); result.Add(username); @@ -682,7 +762,7 @@ namespace Timeline.Services } } - var timelineId = await FindTimelineId(name); + var timelineId = await FindTimelineId(timelineName); async Task?> CheckExistenceAndGetId(List? list) { @@ -692,7 +772,7 @@ namespace Timeline.Services List result = new List(); foreach (var username in list) { - result.Add(await UserService.GetUserIdByUsername(username)); + result.Add(await _userService.GetUserIdByUsername(username)); } return result; } @@ -702,37 +782,36 @@ namespace Timeline.Services if (userIdsAdd != null) { var membersToAdd = userIdsAdd.Select(id => new TimelineMemberEntity { UserId = id, TimelineId = timelineId }).ToList(); - Database.TimelineMembers.AddRange(membersToAdd); + _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 membersToRemove = await _database.TimelineMembers.Where(m => m.TimelineId == timelineId && userIdsRemove.Contains(m.UserId)).ToListAsync(); + _database.TimelineMembers.RemoveRange(membersToRemove); } - await Database.SaveChangesAsync(); + await _database.SaveChangesAsync(); } - public async Task HasManagePermission(string name, long userId) + public async Task HasManagePermission(string timelineName, long userId) { - if (name == null) - throw new ArgumentNullException(nameof(name)); + if (timelineName == null) + throw new ArgumentNullException(nameof(timelineName)); - var timelineId = await FindTimelineId(name); - var timelineEntity = await Database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.OwnerId }).SingleAsync(); + 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 name, long? visitorId) + public async Task HasReadPermission(string timelineName, long? visitorId) { - if (name == null) - throw new ArgumentNullException(nameof(name)); - - var timelineId = await FindTimelineId(name); + if (timelineName == null) + throw new ArgumentNullException(nameof(timelineName)); - var timelineEntity = await Database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.Visibility }).SingleAsync(); + 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; @@ -746,172 +825,51 @@ namespace Timeline.Services } else { - var memberEntity = await Database.TimelineMembers.Where(m => m.UserId == visitorId && m.TimelineId == timelineId).SingleOrDefaultAsync(); + var memberEntity = await _database.TimelineMembers.Where(m => m.UserId == visitorId && m.TimelineId == timelineId).SingleOrDefaultAsync(); return memberEntity != null; } } - public async Task HasPostModifyPermission(string name, long id, long modifierId, bool throwOnPostNotExist = false) + public async Task HasPostModifyPermission(string timelineName, long postId, long modifierId, bool throwOnPostNotExist = false) { - if (name == null) - throw new ArgumentNullException(nameof(name)); + if (timelineName == null) + throw new ArgumentNullException(nameof(timelineName)); - var timelineId = await FindTimelineId(name); + var timelineId = await FindTimelineId(timelineName); - var timelineEntity = await Database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.OwnerId }).SingleAsync(); + 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 == id).Select(p => new { p.AuthorId }).SingleOrDefaultAsync(); + var postEntity = await _database.TimelinePosts.Where(p => p.Id == postId).Select(p => new { p.Content, p.AuthorId }).SingleOrDefaultAsync(); - if (postEntity == null && throwOnPostNotExist) + if (postEntity == null) { - throw new TimelinePostNotExistException(name, id, false); + if (throwOnPostNotExist) + throw new TimelinePostNotExistException(timelineName, postId, false); + else + return true; } - return timelineEntity.OwnerId == modifierId || postEntity == null || postEntity.AuthorId == modifierId; - } - - public async Task IsMemberOf(string name, long userId) - { - if (name == null) - throw new ArgumentNullException(nameof(name)); - - var timelineId = await FindTimelineId(name); - - 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 class OrdinaryTimelineService : BaseTimelineService, IOrdinaryTimelineService - { - private readonly TimelineNameValidator _timelineNameValidator = new TimelineNameValidator(); - - private void ValidateTimelineName(string name, string paramName) - { - if (!_timelineNameValidator.Validate(name, out var message)) + if (postEntity.Content == null && throwOnPostNotExist) { - throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, ExceptionTimelineNameBadFormat, message), paramName); + throw new TimelinePostNotExistException(timelineName, postId, true); } - } - - public OrdinaryTimelineService(ILoggerFactory loggerFactory, DatabaseContext database, IImageValidator imageValidator, IDataManager dataManager, IUserService userService, IClock clock) - : base(loggerFactory, database, imageValidator, dataManager, userService, clock) - { + return timelineEntity.OwnerId == modifierId || postEntity.AuthorId == modifierId; } - protected override async Task FindTimelineId(string name) + public async Task IsMemberOf(string timelineName, long userId) { - if (name == null) - throw new ArgumentNullException(nameof(name)); - - ValidateTimelineName(name, nameof(name)); + if (timelineName == null) + throw new ArgumentNullException(nameof(timelineName)); - var timelineEntity = await Database.Timelines.Where(t => t.Name == name).Select(t => new { t.Id }).SingleOrDefaultAsync(); + var timelineId = await FindTimelineId(timelineName); - if (timelineEntity == null) - { - throw new TimelineNotExistException(name); - } - else - { - return timelineEntity.Id; - } - } + var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.OwnerId }).SingleAsync(); - protected override string GenerateName(string name) - { - return name; - } - } - - public class PersonalTimelineService : BaseTimelineService, IPersonalTimelineService - { - public PersonalTimelineService(ILoggerFactory loggerFactory, DatabaseContext database, IImageValidator imageValidator, IDataManager dataManager, IUserService userService, IClock clock) - : base(loggerFactory, database, imageValidator, dataManager, userService, clock) - { - - } - - protected override async Task FindTimelineId(string name) - { - if (name == null) - throw new ArgumentNullException(nameof(name)); - - long userId; - try - { - userId = await UserService.GetUserIdByUsername(name); - } - catch (ArgumentException e) - { - throw new ArgumentException(ExceptionFindTimelineUsernameBadFormat, nameof(name), e); - } - catch (UserNotExistException e) - { - throw new TimelineNotExistException(name, 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 = new TimelineEntity - { - CurrentPostLocalId = 0, - Name = null, - OwnerId = userId, - Visibility = TimelineVisibility.Register, - CreateTime = Clock.GetCurrentTime() - }; - Database.Timelines.Add(newTimelineEntity); - await Database.SaveChangesAsync(); - - return newTimelineEntity.Id; - } - } - - protected override string GenerateName(string name) - { - return "@" + name; - } - } - - public class TimelineService : ITimelineService - { - private readonly TimelineNameValidator _timelineNameValidator = new TimelineNameValidator(); - - private readonly DatabaseContext _database; - - private readonly IUserService _userService; - private readonly IClock _clock; - - private readonly IOrdinaryTimelineService _ordinaryTimelineService; - private readonly IPersonalTimelineService _personalTimelineService; - - public TimelineService(DatabaseContext database, IUserService userService, IClock clock, IOrdinaryTimelineService ordinaryTimelineService, IPersonalTimelineService personalTimelineService) - { - _database = database; - _userService = userService; - _clock = clock; - _ordinaryTimelineService = ordinaryTimelineService; - _personalTimelineService = personalTimelineService; - } + if (userId == timelineEntity.OwnerId) + return true; - private void ValidateTimelineName(string name, string paramName) - { - if (!_timelineNameValidator.Validate(name, out var message)) - { - throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, ExceptionTimelineNameBadFormat, message), paramName); - } + return await _database.TimelineMembers.AnyAsync(m => m.TimelineId == timelineId && m.UserId == userId); } public async Task> GetTimelines(TimelineUserRelationship? relate = null, List? visibility = null) @@ -952,7 +910,7 @@ namespace Timeline.Services foreach (var entity in entities) { - result.Add(await TimelineServiceHelper.MapTimelineFromEntity(_userService, entity)); + result.Add(await MapTimelineFromEntity(entity)); } return result; @@ -985,7 +943,7 @@ namespace Timeline.Services _database.Timelines.Add(newEntity); await _database.SaveChangesAsync(); - return await TimelineServiceHelper.MapTimelineFromEntity(_userService, newEntity); + return await MapTimelineFromEntity(newEntity); } public async Task DeleteTimeline(string name) @@ -1003,101 +961,5 @@ namespace Timeline.Services _database.Timelines.Remove(entity); await _database.SaveChangesAsync(); } - - - private IBaseTimelineService BranchName(string name, out string realName) - { - if (name == null) - throw new ArgumentNullException(nameof(name)); - - if (name.StartsWith('@')) - { - realName = name.Substring(1); - return _personalTimelineService; - } - else - { - realName = name; - return _ordinaryTimelineService; - } - } - - public Task GetTimeline(string name) - { - var s = BranchName(name, out var realName); - return s.GetTimeline(realName); - } - - public Task ChangeProperty(string name, TimelineChangePropertyRequest newProperties) - { - var s = BranchName(name, out var realName); - return s.ChangeProperty(realName, newProperties); - } - - public Task> GetPosts(string name) - { - var s = BranchName(name, out var realName); - return s.GetPosts(realName); - } - - public Task GetPostDataETag(string name, long postId) - { - var s = BranchName(name, out var realName); - return s.GetPostDataETag(realName, postId); - } - - public Task GetPostData(string name, long postId) - { - var s = BranchName(name, out var realName); - return s.GetPostData(realName, postId); - } - - public Task CreateTextPost(string name, long authorId, string text, DateTime? time) - { - var s = BranchName(name, out var realName); - return s.CreateTextPost(realName, authorId, text, time); - } - - public Task CreateImagePost(string name, long authorId, byte[] data, DateTime? time) - { - var s = BranchName(name, out var realName); - return s.CreateImagePost(realName, authorId, data, time); - } - - public Task DeletePost(string name, long id) - { - var s = BranchName(name, out var realName); - return s.DeletePost(realName, id); - } - - public Task ChangeMember(string name, IList? add, IList? remove) - { - var s = BranchName(name, out var realName); - return s.ChangeMember(realName, add, remove); - } - - public Task HasManagePermission(string name, long userId) - { - var s = BranchName(name, out var realName); - return s.HasManagePermission(realName, userId); - } - - public Task HasReadPermission(string name, long? visitorId) - { - var s = BranchName(name, out var realName); - return s.HasReadPermission(realName, visitorId); - } - - public Task HasPostModifyPermission(string name, long id, long modifierId, bool throwOnPostNotExist = false) - { - var s = BranchName(name, out var realName); - return s.HasPostModifyPermission(realName, id, modifierId, throwOnPostNotExist); - } - - public Task IsMemberOf(string name, long userId) - { - var s = BranchName(name, out var realName); - return s.IsMemberOf(realName, userId); - } } } diff --git a/Timeline/Services/UserAvatarService.cs b/Timeline/Services/UserAvatarService.cs index 3ab8f14d..2bf8bddc 100644 --- a/Timeline/Services/UserAvatarService.cs +++ b/Timeline/Services/UserAvatarService.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Threading.Tasks; using Timeline.Entities; using Timeline.Helpers; +using Timeline.Services.Exceptions; namespace Timeline.Services { diff --git a/Timeline/Startup.cs b/Timeline/Startup.cs index 11b6fa9e..84f0e8ba 100644 --- a/Timeline/Startup.cs +++ b/Timeline/Startup.cs @@ -82,8 +82,6 @@ namespace Timeline services.AddUserAvatarService(); services.AddScoped(); - services.AddScoped(); - services.AddScoped(); services.TryAddSingleton(); -- cgit v1.2.3