aboutsummaryrefslogtreecommitdiff
path: root/Timeline/Services
diff options
context:
space:
mode:
Diffstat (limited to 'Timeline/Services')
-rw-r--r--Timeline/Services/AvatarFormatException.cs51
-rw-r--r--Timeline/Services/ImageException.cs54
-rw-r--r--Timeline/Services/ImageValidator.cs53
-rw-r--r--Timeline/Services/TimelinePostNotExistException.cs11
-rw-r--r--Timeline/Services/TimelineService.cs682
-rw-r--r--Timeline/Services/User.cs18
-rw-r--r--Timeline/Services/UserAvatarService.cs47
-rw-r--r--Timeline/Services/UserService.cs1
-rw-r--r--Timeline/Services/UserTokenManager.cs1
9 files changed, 594 insertions, 324 deletions
diff --git a/Timeline/Services/AvatarFormatException.cs b/Timeline/Services/AvatarFormatException.cs
deleted file mode 100644
index 788eabb2..00000000
--- a/Timeline/Services/AvatarFormatException.cs
+++ /dev/null
@@ -1,51 +0,0 @@
-using System;
-using System.Globalization;
-
-namespace Timeline.Services
-{
- /// <summary>
- /// Thrown when avatar is of bad format.
- /// </summary>
- [Serializable]
- public class AvatarFormatException : Exception
- {
- public enum ErrorReason
- {
- /// <summary>
- /// Decoding image failed.
- /// </summary>
- CantDecode,
- /// <summary>
- /// Decoding succeeded but the real type is not the specified type.
- /// </summary>
- UnmatchedFormat,
- /// <summary>
- /// Image is not a square.
- /// </summary>
- BadSize
- }
-
- public AvatarFormatException() : base(MakeMessage(null)) { }
- public AvatarFormatException(string message) : base(message) { }
- public AvatarFormatException(string message, Exception inner) : base(message, inner) { }
-
- public AvatarFormatException(Avatar avatar, ErrorReason error) : base(MakeMessage(error)) { Avatar = avatar; Error = error; }
- public AvatarFormatException(Avatar avatar, ErrorReason error, Exception inner) : base(MakeMessage(error), inner) { Avatar = avatar; Error = error; }
-
- protected AvatarFormatException(
- 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.AvatarFormatException, reason switch
- {
- ErrorReason.CantDecode => Resources.Services.Exception.AvatarFormatExceptionCantDecode,
- ErrorReason.UnmatchedFormat => Resources.Services.Exception.AvatarFormatExceptionUnmatchedFormat,
- ErrorReason.BadSize => Resources.Services.Exception.AvatarFormatExceptionBadSize,
- _ => Resources.Services.Exception.AvatarFormatExceptionUnknownError
- });
-
- public ErrorReason? Error { get; set; }
- public Avatar? Avatar { get; set; }
- }
-}
diff --git a/Timeline/Services/ImageException.cs b/Timeline/Services/ImageException.cs
new file mode 100644
index 00000000..c6126aa3
--- /dev/null
+++ b/Timeline/Services/ImageException.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Globalization;
+
+namespace Timeline.Services
+{
+ [Serializable]
+ public class ImageException : Exception
+ {
+ public enum ErrorReason
+ {
+ /// <summary>
+ /// Decoding image failed.
+ /// </summary>
+ CantDecode,
+ /// <summary>
+ /// Decoding succeeded but the real type is not the specified type.
+ /// </summary>
+ UnmatchedFormat,
+ /// <summary>
+ /// Image is not a square.
+ /// </summary>
+ 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
new file mode 100644
index 00000000..c331d912
--- /dev/null
+++ b/Timeline/Services/ImageValidator.cs
@@ -0,0 +1,53 @@
+using SixLabors.ImageSharp;
+using SixLabors.ImageSharp.Formats;
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Timeline.Services
+{
+ public interface IImageValidator
+ {
+ /// <summary>
+ /// Validate a image data.
+ /// </summary>
+ /// <param name="data">The data of the image. Can't be null.</param>
+ /// <param name="requestType">If not null, the real image format will be check against the requested format and throw if not match. If null, then do not check.</param>
+ /// <param name="square">If true, image must be square.</param>
+ /// <returns>The format.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="data"/> is null.</exception>
+ /// <exception cref="ImageException">Thrown when image data can't be decoded or real type does not match request type or image is not square when required.</exception>
+ Task<IImageFormat> Validate(byte[] data, string? requestType = null, bool square = false);
+ }
+
+ public class ImageValidator : IImageValidator
+ {
+ public ImageValidator()
+ {
+ }
+
+ public async Task<IImageFormat> Validate(byte[] data, string? requestType = null, bool square = false)
+ {
+ if (data == null)
+ throw new ArgumentNullException(nameof(data));
+
+ var format = await Task.Run(() =>
+ {
+ try
+ {
+ using var image = Image.Load(data, out IImageFormat format);
+ if (requestType != null && !format.MimeTypes.Contains(requestType))
+ throw new ImageException(ImageException.ErrorReason.UnmatchedFormat, data, requestType, format.DefaultMimeType);
+ if (square && image.Width != image.Height)
+ throw new ImageException(ImageException.ErrorReason.NotSquare, data, requestType, format.DefaultMimeType);
+ return format;
+ }
+ catch (UnknownImageFormatException e)
+ {
+ throw new ImageException(e, ImageException.ErrorReason.CantDecode, data, requestType, null);
+ }
+ });
+ return format;
+ }
+ }
+}
diff --git a/Timeline/Services/TimelinePostNotExistException.cs b/Timeline/Services/TimelinePostNotExistException.cs
index 97e5d550..c542e63e 100644
--- a/Timeline/Services/TimelinePostNotExistException.cs
+++ b/Timeline/Services/TimelinePostNotExistException.cs
@@ -12,12 +12,17 @@ namespace Timeline.Services
System.Runtime.Serialization.SerializationInfo info,
System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
- public TimelinePostNotExistException(long id) : base(Resources.Services.Exception.TimelinePostNotExistException) { Id = id; }
+ public TimelinePostNotExistException(string timelineName, long id, bool isDelete = false) : base(Resources.Services.Exception.TimelinePostNotExistException) { TimelineName = timelineName; Id = id; IsDelete = isDelete; }
- public TimelinePostNotExistException(long id, string message) : base(message) { Id = id; }
+ public TimelinePostNotExistException(string timelineName, long id, bool isDelete, string message) : base(message) { TimelineName = timelineName; Id = id; IsDelete = isDelete; }
- public TimelinePostNotExistException(long id, string message, Exception inner) : base(message, inner) { Id = id; }
+ public TimelinePostNotExistException(string timelineName, long id, bool isDelete, string message, Exception inner) : base(message, inner) { TimelineName = timelineName; Id = id; IsDelete = isDelete; }
+ public string TimelineName { get; set; } = "";
public long Id { get; set; }
+ /// <summary>
+ /// True if the post is deleted. False if the post does not exist at all.
+ /// </summary>
+ public bool IsDelete { get; set; } = false;
}
}
diff --git a/Timeline/Services/TimelineService.cs b/Timeline/Services/TimelineService.cs
index 379ec8f5..301a1d97 100644
--- a/Timeline/Services/TimelineService.cs
+++ b/Timeline/Services/TimelineService.cs
@@ -1,13 +1,13 @@
-using AutoMapper;
-using Microsoft.EntityFrameworkCore;
+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.Models.Http;
+using Timeline.Models;
using Timeline.Models.Validation;
using static Timeline.Resources.Services.TimelineService;
@@ -32,95 +32,118 @@ namespace Timeline.Services
public long UserId { get; set; }
}
+ public class PostData
+ {
+#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; } = default!;
+ }
+
/// <summary>
- /// This define the common interface of both personal timeline
- /// and normal timeline.
+ /// This define the common interface of both personal timeline and normal timeline.
/// </summary>
/// <remarks>
- /// The "name" parameter in method means name of timeline in
- /// <see cref="ITimelineService"/> while username of the owner
- /// of the personal timeline in <see cref="IPersonalTimelineService"/>.
+ /// The "name" parameter in each method has different meaning.
+ /// <see cref="IOrdinaryTimelineService"/> => name of the ordinary timeline
+ /// <see cref="IPersonalTimelineService"/> => username of the owner of the personal timeline
+ /// <see cref="ITimelineService"/> => username if begin with '@' otherwise timeline name
+ ///
+ /// <see cref="ArgumentException"/> 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.
+ ///
+ /// <see cref="TimelineNotExistException"> 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 <see cref="UserNotExistException"/>.
/// </remarks>
public interface IBaseTimelineService
{
/// <summary>
/// Get the timeline info.
/// </summary>
- /// <param name="name">Username or the timeline name. See remarks of <see cref="IBaseTimelineService"/>.</param>
+ /// <param name="name">See remarks of <see cref="IBaseTimelineService"/>.</param>
/// <returns>The timeline info.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> is null.</exception>
- /// <exception cref="ArgumentException">Thrown when <paramref name="name"/> is illegal. It is not a valid timeline name (for normal timeline service) or a valid username (for personal timeline service).</exception>
- /// <exception cref="TimelineNotExistException">
- /// 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 <see cref="UserNotExistException"/>.
- /// </exception>
- Task<TimelineInfo> GetTimeline(string name);
+ /// <exception cref="ArgumentException">See remarks of <see cref="IBaseTimelineService"/>.</exception>
+ /// <exception cref="TimelineNotExistException">See remarks of <see cref="IBaseTimelineService"/>.</exception>
+ Task<Models.Timeline> GetTimeline(string name);
/// <summary>
/// Set the properties of a timeline.
/// </summary>
- /// <param name="name">Username or the timeline name. See remarks of <see cref="IBaseTimelineService"/>.</param>
+ /// <param name="name">See remarks of <see cref="IBaseTimelineService"/>.</param>
/// <param name="newProperties">The new properties. Null member means not to change.</param>
- /// <returns>The timeline info.</returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> is null.</exception>
- /// <exception cref="ArgumentException">Thrown when <paramref name="name"/> is illegal. It is not a valid timeline name (for normal timeline service) or a valid username (for personal timeline service).</exception>
- /// <exception cref="TimelineNotExistException">
- /// 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 <see cref="UserNotExistException"/>.
- /// </exception>
- Task ChangeProperty(string name, TimelinePatchRequest newProperties);
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> or <paramref name="newProperties"/> is null.</exception>
+ /// <exception cref="ArgumentException">See remarks of <see cref="IBaseTimelineService"/>.</exception>
+ /// <exception cref="TimelineNotExistException">See remarks of <see cref="IBaseTimelineService"/>.</exception>
+ Task ChangeProperty(string name, TimelineChangePropertyRequest newProperties);
/// <summary>
/// Get all the posts in the timeline.
/// </summary>
- /// <param name="name">Username or the timeline name. See remarks of <see cref="IBaseTimelineService"/>.</param>
+ /// <param name="name">See remarks of <see cref="IBaseTimelineService"/>.</param>
/// <returns>A list of all posts.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> is null.</exception>
- /// <exception cref="ArgumentException">Thrown when <paramref name="name"/> is illegal. It is not a valid timeline name (for normal timeline service) or a valid username (for personal timeline service).</exception>
- /// <exception cref="TimelineNotExistException">
- /// 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 <see cref="UserNotExistException"/>.
- /// </exception>
- Task<List<TimelinePostInfo>> GetPosts(string name);
+ /// <exception cref="ArgumentException">See remarks of <see cref="IBaseTimelineService"/>.</exception>
+ /// <exception cref="TimelineNotExistException">See remarks of <see cref="IBaseTimelineService"/>.</exception>
+ Task<List<TimelinePost>> GetPosts(string name);
/// <summary>
- /// Create a new post in timeline.
+ /// Get the data of a post.
/// </summary>
- /// <param name="name">Username or the timeline name. See remarks of <ssee cref="IBaseTimelineService"/>.</param>
- /// <param name="authorId">The author's id.</param>
- /// <param name="content">The content.</param>
+ /// <param name="name">See remarks of <see cref="IBaseTimelineService"/>.</param>
+ /// <param name="postId">The id of the post.</param>
+ /// <returns>The data and its type.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> is null.</exception>
+ /// <exception cref="ArgumentException">See remarks of <see cref="IBaseTimelineService"/>.</exception>
+ /// <exception cref="TimelineNotExistException">See remarks of <see cref="IBaseTimelineService"/>.</exception>
+ /// <exception cref="TimelinePostNotExistException">Thrown when post of <paramref name="postId"/> does not exist or has been deleted.</exception>
+ /// <exception cref="InvalidOperationException">Thrown when post has no data. See remarks.</exception>
+ /// <remarks>
+ /// Use this method to retrieve the image of image post.
+ /// </remarks>
+ Task<PostData> GetPostData(string name, long postId);
+
+ /// <summary>
+ /// Create a new text post in timeline.
+ /// </summary>
+ /// <param name="name">See remarks of <see cref="IBaseTimelineService"/>.</param>
+ /// <param name="authorId">The author's user id.</param>
+ /// <param name="text">The content text.</param>
/// <param name="time">The time of the post. If null, then use current time.</param>
/// <returns>The info of the created post.</returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> or <paramref name="content"/> is null.</exception>
- /// <exception cref="ArgumentException">Thrown when <paramref name="name"/> is illegal. It is not a valid timeline name (for normal timeline service) or a valid username (for personal timeline service).</exception>
- /// <exception cref="TimelineNotExistException">
- /// 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 <see cref="UserNotExistException"/>.
- /// </exception>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> or <paramref name="text"/> is null.</exception>
+ /// <exception cref="ArgumentException">See remarks of <see cref="IBaseTimelineService"/>.</exception>
+ /// <exception cref="TimelineNotExistException">See remarks of <see cref="IBaseTimelineService"/>.</exception>
/// <exception cref="UserNotExistException">Thrown if user with <paramref name="authorId"/> does not exist.</exception>
- Task<TimelinePostInfo> CreatePost(string name, long authorId, string content, DateTime? time);
+ Task<TimelinePost> CreateTextPost(string name, long authorId, string text, DateTime? time);
/// <summary>
- /// Delete a post
+ /// Create a new image post in timeline.
/// </summary>
- /// <param name="name">Username or the timeline name. See remarks of <see cref="IBaseTimelineService"/>.</param>
+ /// <param name="name">See remarks of <see cref="IBaseTimelineService"/>.</param>
+ /// <param name="authorId">The author's user id.</param>
+ /// <param name="data">The image data.</param>
+ /// <param name="time">The time of the post. If null, then use current time.</param>
+ /// <returns>The info of the created post.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> or <paramref name="data"/> is null.</exception>
+ /// <exception cref="ArgumentException">See remarks of <see cref="IBaseTimelineService"/>.</exception>
+ /// <exception cref="TimelineNotExistException">See remarks of <see cref="IBaseTimelineService"/>.</exception>
+ /// <exception cref="UserNotExistException">Thrown if user with <paramref name="authorId"/> does not exist.</exception>
+ /// <exception cref="ImageException">Thrown if data is not a image. Validated by <see cref="ImageValidator"/>.</exception>
+ Task<TimelinePost> CreateImagePost(string name, long authorId, byte[] data, DateTime? time);
+
+ /// <summary>
+ /// Delete a post.
+ /// </summary>
+ /// <param name="name">See remarks of <see cref="IBaseTimelineService"/>.</param>
/// <param name="id">The id of the post to delete.</param>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> or <paramref name="username"/> is null.</exception>
- /// <exception cref="ArgumentException">Thrown when <paramref name="name"/> is illegal. It is not a valid timeline name (for normal timeline service) or a valid username (for personal timeline service).</exception>
- /// <exception cref="TimelineNotExistException">
- /// 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 <see cref="UserNotExistException"/>.
- /// </exception>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> is null.</exception>
+ /// <exception cref="ArgumentException">See remarks of <see cref="IBaseTimelineService"/>.</exception>
+ /// <exception cref="TimelineNotExistException">See remarks of <see cref="IBaseTimelineService"/>.</exception>
/// <exception cref="TimelinePostNotExistException">
/// Thrown when the post with given id does not exist or is deleted already.
/// </exception>
@@ -133,18 +156,13 @@ namespace Timeline.Services
/// <summary>
/// Remove members to a timeline.
/// </summary>
- /// <param name="name">Username or the timeline name. See remarks of <see cref="IBaseTimelineService"/>.</param>
+ /// <param name="name">See remarks of <see cref="IBaseTimelineService"/>.</param>
/// <param name="add">A list of usernames of members to add. May be null.</param>
/// <param name="remove">A list of usernames of members to remove. May be null.</param>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> is null.</exception>
- /// <exception cref="ArgumentException">Thrown when <paramref name="name"/> is illegal. It is not a valid timeline name (for normal timeline service) or a valid username (for personal timeline service).</exception>
+ /// <exception cref="ArgumentException">See remarks of <see cref="IBaseTimelineService"/>.</exception>
+ /// <exception cref="TimelineNotExistException">See remarks of <see cref="IBaseTimelineService"/>.</exception>
/// <exception cref="ArgumentException">Thrown when names in <paramref name="add"/> or <paramref name="remove"/> is not a valid username.</exception>
- /// <exception cref="TimelineNotExistException">
- /// 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 <see cref="UserNotExistException"/>.
- /// </exception>
/// <exception cref="UserNotExistException">
/// Thrown when one of the user to change does not exist.
/// </exception>
@@ -160,17 +178,12 @@ namespace Timeline.Services
/// <summary>
/// Check whether a user can manage(change timeline info, member, ...) a timeline.
/// </summary>
- /// <param name="name"></param>
- /// <param name="id"></param>
+ /// <param name="name">See remarks of <see cref="IBaseTimelineService"/>.</param>
+ /// <param name="userId">The user id.</param>
/// <returns>True if the user can manage the timeline, otherwise false.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> is null.</exception>
- /// <exception cref="ArgumentException">Thrown when <paramref name="name"/> is illegal. It is not a valid timeline name (for normal timeline service) or a valid username (for personal timeline service).</exception>
- /// <exception cref="TimelineNotExistException">
- /// 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 <see cref="UserNotExistException"/>.
- /// </exception>
+ /// <exception cref="ArgumentException">See remarks of <see cref="IBaseTimelineService"/>.</exception>
+ /// <exception cref="TimelineNotExistException">See remarks of <see cref="IBaseTimelineService"/>.</exception>
/// <remarks>
/// This method does not check whether visitor is administrator.
/// Return false if user with user id does not exist.
@@ -180,17 +193,12 @@ namespace Timeline.Services
/// <summary>
/// Verify whether a visitor has the permission to read a timeline.
/// </summary>
- /// <param name="name">Username or the timeline name. See remarks of <see cref="IBaseTimelineService"/>.</param>
+ /// <param name="name">See remarks of <see cref="IBaseTimelineService"/>.</param>
/// <param name="visitorId">The id of the user to check on. Null means visitor without account.</param>
/// <returns>True if can read, false if can't read.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> is null.</exception>
- /// <exception cref="ArgumentException">Thrown when <paramref name="name"/> is illegal. It is not a valid timeline name (for normal timeline service) or a valid username (for personal timeline service).</exception>
- /// <exception cref="TimelineNotExistException">
- /// 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 <see cref="UserNotExistException"/>.
- /// </exception>
+ /// <exception cref="ArgumentException">See remarks of <see cref="IBaseTimelineService"/>.</exception>
+ /// <exception cref="TimelineNotExistException">See remarks of <see cref="IBaseTimelineService"/>.</exception>
/// <remarks>
/// This method does not check whether visitor is administrator.
/// Return false if user with visitor id does not exist.
@@ -200,41 +208,30 @@ namespace Timeline.Services
/// <summary>
/// Verify whether a user has the permission to modify a post.
/// </summary>
- /// <param name="name">Username or the timeline name. See remarks of <see cref="IBaseTimelineService"/>.</param>
+ /// <param name="name">See remarks of <see cref="IBaseTimelineService"/>.</param>
/// <param name="modifierId">The id of the user to check on.</param>
+ /// <param name="throwOnPostNotExist">True if you want it to throw <see cref="TimelinePostNotExistException"/>. Default false.</param>
/// <returns>True if can modify, false if can't modify.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> is null.</exception>
- /// <exception cref="ArgumentException">Thrown when <paramref name="name"/> is illegal. It is not a valid timeline name (for normal timeline service) or a valid username (for personal timeline service).</exception>
- /// <exception cref="TimelineNotExistException">
- /// 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 <see cref="UserNotExistException"/>.
- /// </exception>
- /// <exception cref="TimelinePostNotExistException">
- /// Thrown when the post with given id does not exist or is deleted already.
- /// </exception>
+ /// <exception cref="ArgumentException">See remarks of <see cref="IBaseTimelineService"/>.</exception>
+ /// <exception cref="TimelineNotExistException">See remarks of <see cref="IBaseTimelineService"/>.</exception>
+ /// <exception cref="TimelinePostNotExistException">Thrown when the post with given id does not exist or is deleted already and <paramref name="throwOnPostNotExist"/> is true.</exception>
/// <remarks>
/// 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.
/// </remarks>
- Task<bool> HasPostModifyPermission(string name, long id, long modifierId);
+ Task<bool> HasPostModifyPermission(string name, long id, long modifierId, bool throwOnPostNotExist = false);
/// <summary>
/// Verify whether a user is member of a timeline.
/// </summary>
- /// <param name="name">Username or the timeline name. See remarks of <see cref="IBaseTimelineService"/>.</param>
+ /// <param name="name">See remarks of <see cref="IBaseTimelineService"/>.</param>
/// <param name="userId">The id of user to check on.</param>
/// <returns>True if it is a member, false if not.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> is null.</exception>
- /// <exception cref="ArgumentException">Thrown when <paramref name="name"/> is illegal. It is not a valid timeline name (for normal timeline service) or a valid username (for personal timeline service).</exception>
- /// <exception cref="TimelineNotExistException">
- /// 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 <see cref="UserNotExistException"/>.
- /// </exception>
+ /// <exception cref="ArgumentException">See remarks of <see cref="IBaseTimelineService"/>.</exception>
+ /// <exception cref="TimelineNotExistException">See remarks of <see cref="IBaseTimelineService"/>.</exception>
/// <remarks>
/// Timeline owner is also considered as a member.
/// Return false when user with user id does not exist.
@@ -256,7 +253,7 @@ namespace Timeline.Services
/// <remarks>
/// If user with related user id does not exist, empty list will be returned.
/// </remarks>
- Task<List<TimelineInfo>> GetTimelines(TimelineUserRelationship? relate = null, List<TimelineVisibility>? visibility = null);
+ Task<List<Models.Timeline>> GetTimelines(TimelineUserRelationship? relate = null, List<TimelineVisibility>? visibility = null);
/// <summary>
/// Create a timeline.
@@ -268,7 +265,7 @@ namespace Timeline.Services
/// <exception cref="ArgumentException">Thrown when timeline name is invalid.</exception>
/// <exception cref="ConflictException">Thrown when the timeline already exists.</exception>
/// <exception cref="UserNotExistException">Thrown when the owner user does not exist.</exception>
- Task<TimelineInfo> CreateTimeline(string name, long owner);
+ Task<Models.Timeline> CreateTimeline(string name, long owner);
/// <summary>
/// Delete a timeline.
@@ -280,6 +277,11 @@ namespace Timeline.Services
Task DeleteTimeline(string name);
}
+ public interface IOrdinaryTimelineService : IBaseTimelineService
+ {
+
+ }
+
public interface IPersonalTimelineService : IBaseTimelineService
{
@@ -287,36 +289,28 @@ namespace Timeline.Services
public abstract class BaseTimelineService : IBaseTimelineService
{
- protected BaseTimelineService(ILoggerFactory loggerFactory, DatabaseContext database, IUserService userService, IMapper mapper, IClock clock)
+ protected BaseTimelineService(ILoggerFactory loggerFactory, DatabaseContext database, IImageValidator imageValidator, IDataManager dataManager, IUserService userService, IClock clock)
{
+ _logger = loggerFactory.CreateLogger<BaseTimelineService>();
Clock = clock;
Database = database;
+ ImageValidator = imageValidator;
+ DataManager = dataManager;
UserService = userService;
- Mapper = mapper;
}
+ private ILogger<BaseTimelineService> _logger;
+
protected IClock Clock { get; }
protected UsernameValidator UsernameValidator { get; } = new UsernameValidator();
protected DatabaseContext Database { get; }
+ protected IImageValidator ImageValidator { get; }
+ protected IDataManager DataManager { get; }
protected IUserService UserService { get; }
- protected IMapper Mapper { get; }
-
- protected TimelineEntity CreateNewEntity(string? name, long owner)
- {
- return new TimelineEntity
- {
- CurrentPostLocalId = 0,
- Name = name,
- OwnerId = owner,
- Visibility = TimelineVisibility.Register,
- CreateTime = Clock.GetCurrentTime()
- };
- }
-
/// <summary>
/// Find the timeline id by the name.
/// For details, see remarks.
@@ -341,7 +335,9 @@ namespace Timeline.Services
/// </remarks>
protected abstract Task<long> FindTimelineId(string name);
- public async Task<TimelineInfo> GetTimeline(string name)
+ protected abstract string GenerateName(string name);
+
+ public async Task<Models.Timeline> GetTimeline(string name)
{
if (name == null)
throw new ArgumentNullException(nameof(name));
@@ -352,17 +348,17 @@ namespace Timeline.Services
var timelineMemberEntities = await Database.TimelineMembers.Where(m => m.TimelineId == timelineId).Select(m => new { m.UserId }).ToListAsync();
- var owner = Mapper.Map<UserInfo>(await UserService.GetUserById(timelineEntity.OwnerId));
+ var owner = await UserService.GetUserById(timelineEntity.OwnerId);
- var members = new List<UserInfo>();
+ var members = new List<User>();
foreach (var memberEntity in timelineMemberEntities)
{
- members.Add(Mapper.Map<UserInfo>(await UserService.GetUserById(memberEntity.UserId)));
+ members.Add(await UserService.GetUserById(memberEntity.UserId));
}
- return new TimelineInfo
+ return new Models.Timeline
{
- Name = timelineEntity.Name,
+ Name = GenerateName(name),
Description = timelineEntity.Description ?? "",
Owner = owner,
Visibility = timelineEntity.Visibility,
@@ -370,45 +366,100 @@ namespace Timeline.Services
};
}
- public async Task<List<TimelinePostInfo>> GetPosts(string name)
+ public async Task<List<TimelinePost>> GetPosts(string name)
{
if (name == null)
throw new ArgumentNullException(nameof(name));
-
var timelineId = await FindTimelineId(name);
var postEntities = await Database.TimelinePosts.OrderBy(p => p.Time).Where(p => p.TimelineId == timelineId && p.Content != null).ToListAsync();
- var posts = new List<TimelinePostInfo>();
+ var posts = new List<TimelinePost>();
foreach (var entity in postEntities)
{
if (entity.Content != null) // otherwise it is deleted
{
- var author = Mapper.Map<UserInfo>(await UserService.GetUserById(entity.AuthorId));
- posts.Add(new TimelinePostInfo
+ var author = await UserService.GetUserById(entity.AuthorId);
+
+ var type = entity.ContentType;
+
+ ITimelinePostContent content = type switch
{
- Id = entity.LocalId,
- Content = entity.Content,
- Author = author,
- Time = entity.Time,
- LastUpdated = entity.LastUpdated
- });
+ 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)
+ ));
}
}
return posts;
}
+ public async Task<PostData> GetPostData(string name, long postId)
+ {
+ if (name == null)
+ throw new ArgumentNullException(nameof(name));
+
+ var timelineId = await FindTimelineId(name);
+ var postEntity = await Database.TimelinePosts.Where(p => p.LocalId == postId).SingleOrDefaultAsync();
- public async Task<TimelinePostInfo> CreatePost(string name, long authorId, string content, DateTime? time)
+ if (postEntity == null)
+ throw new TimelinePostNotExistException(name, postId);
+
+ if (postEntity.Content == null)
+ throw new TimelinePostNotExistException(name, postId, true);
+
+ if (postEntity.ContentType != TimelinePostContentTypes.Image)
+ throw new InvalidOperationException(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<TimelinePost> CreateTextPost(string name, long authorId, string text, DateTime? time)
{
if (name == null)
throw new ArgumentNullException(nameof(name));
- if (content == null)
- throw new ArgumentNullException(nameof(content));
+ 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 author = Mapper.Map<UserInfo>(await UserService.GetUserById(authorId));
+ var author = await UserService.GetUserById(authorId);
var currentTime = Clock.GetCurrentTime();
var finalTime = time ?? currentTime;
@@ -418,7 +469,8 @@ namespace Timeline.Services
var postEntity = new TimelinePostEntity
{
LocalId = timelineEntity.CurrentPostLocalId,
- Content = content,
+ ContentType = TimelinePostContentTypes.Text,
+ Content = text,
AuthorId = authorId,
TimelineId = timelineId,
Time = finalTime,
@@ -427,14 +479,62 @@ namespace Timeline.Services
Database.TimelinePosts.Add(postEntity);
await Database.SaveChangesAsync();
- return new TimelinePostInfo
+
+ return new TimelinePost(
+ id: postEntity.LocalId,
+ content: new TextTimelinePostContent(text),
+ time: finalTime,
+ author: author,
+ lastUpdated: currentTime,
+ timelineName: GenerateName(name)
+ );
+ }
+
+ public async Task<TimelinePost> CreateImagePost(string name, long authorId, byte[] data, DateTime? time)
+ {
+ if (name == null)
+ throw new ArgumentNullException(nameof(name));
+ 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 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
{
- Id = postEntity.LocalId,
- Content = content,
- Author = author,
+ 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: GenerateName(name)
+ );
}
public async Task DeletePost(string name, long id)
@@ -446,16 +546,28 @@ namespace Timeline.Services
var post = await Database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == id).SingleOrDefaultAsync();
- if (post == null)
- throw new TimelinePostNotExistException(id);
+ if (post == null || post.Content == null)
+ throw new TimelinePostNotExistException(name, id);
+
+ 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 ChangeProperty(string name, TimelinePatchRequest newProperties)
+ public async Task ChangeProperty(string name, TimelineChangePropertyRequest newProperties)
{
if (name == null)
throw new ArgumentNullException(nameof(name));
@@ -592,7 +704,7 @@ namespace Timeline.Services
}
}
- public async Task<bool> HasPostModifyPermission(string name, long id, long modifierId)
+ public async Task<bool> HasPostModifyPermission(string name, long id, long modifierId, bool throwOnPostNotExist = false)
{
if (name == null)
throw new ArgumentNullException(nameof(name));
@@ -603,10 +715,12 @@ namespace Timeline.Services
var postEntity = await Database.TimelinePosts.Where(p => p.Id == id).Select(p => new { p.AuthorId }).SingleOrDefaultAsync();
- if (postEntity == null)
- throw new TimelinePostNotExistException(id);
+ if (postEntity == null && throwOnPostNotExist)
+ {
+ throw new TimelinePostNotExistException(name, id, false);
+ }
- return timelineEntity.OwnerId == modifierId || postEntity.AuthorId == modifierId;
+ return timelineEntity.OwnerId == modifierId || postEntity == null || postEntity.AuthorId == modifierId;
}
public async Task<bool> IsMemberOf(string name, long userId)
@@ -625,7 +739,7 @@ namespace Timeline.Services
}
}
- public class TimelineService : BaseTimelineService, ITimelineService
+ public class OrdinaryTimelineService : BaseTimelineService, IOrdinaryTimelineService
{
private readonly TimelineNameValidator _timelineNameValidator = new TimelineNameValidator();
@@ -637,8 +751,8 @@ namespace Timeline.Services
}
}
- public TimelineService(ILoggerFactory loggerFactory, DatabaseContext database, IUserService userService, IMapper mapper, IClock clock)
- : base(loggerFactory, database, userService, mapper, clock)
+ public OrdinaryTimelineService(ILoggerFactory loggerFactory, DatabaseContext database, IImageValidator imageValidator, IDataManager dataManager, IUserService userService, IClock clock)
+ : base(loggerFactory, database, imageValidator, dataManager, userService, clock)
{
}
@@ -662,7 +776,98 @@ namespace Timeline.Services
}
}
- public async Task<List<TimelineInfo>> GetTimelines(TimelineUserRelationship? relate = null, List<TimelineVisibility>? visibility = null)
+ 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<long> 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;
+ }
+
+ private void ValidateTimelineName(string name, string paramName)
+ {
+ if (!_timelineNameValidator.Validate(name, out var message))
+ {
+ throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, ExceptionTimelineNameBadFormat, message), paramName);
+ }
+ }
+
+ public async Task<List<Models.Timeline>> GetTimelines(TimelineUserRelationship? relate = null, List<TimelineVisibility>? visibility = null)
{
List<TimelineEntity> entities;
@@ -679,7 +884,7 @@ namespace Timeline.Services
if (relate == null)
{
- entities = await ApplyTimelineVisibilityFilter(Database.Timelines).Include(t => t.Members).ToListAsync();
+ entities = await ApplyTimelineVisibilityFilter(_database.Timelines).Include(t => t.Members).ToListAsync();
}
else
{
@@ -687,31 +892,32 @@ namespace Timeline.Services
if ((relate.Type & TimelineUserRelationshipType.Own) != 0)
{
- entities.AddRange(await ApplyTimelineVisibilityFilter(Database.Timelines.Where(t => t.OwnerId == relate.UserId)).Include(t => t.Members).ToListAsync());
+ 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());
+ 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<TimelineInfo>();
+ var result = new List<Models.Timeline>();
foreach (var entity in entities)
{
- var timeline = new TimelineInfo
+ var owner = await _userService.GetUserById(entity.OwnerId);
+ var timeline = new Models.Timeline
{
- Name = entity.Name,
+ Name = entity.Name ?? ("@" + owner.Username),
Description = entity.Description ?? "",
- Owner = Mapper.Map<UserInfo>(await UserService.GetUserById(entity.OwnerId)),
+ Owner = owner,
Visibility = entity.Visibility,
- Members = new List<UserInfo>()
+ Members = new List<User>()
};
foreach (var m in entity.Members)
{
- timeline.Members.Add(Mapper.Map<UserInfo>(await UserService.GetUserById(m.UserId)));
+ timeline.Members.Add(await _userService.GetUserById(m.UserId));
}
result.Add(timeline);
@@ -720,31 +926,39 @@ namespace Timeline.Services
return result;
}
- public async Task<TimelineInfo> CreateTimeline(string name, long owner)
+ public async Task<Models.Timeline> CreateTimeline(string name, long owner)
{
if (name == null)
throw new ArgumentNullException(nameof(name));
ValidateTimelineName(name, nameof(name));
- var user = await UserService.GetUserById(owner);
+ var user = await _userService.GetUserById(owner);
- var conflict = await Database.Timelines.AnyAsync(t => t.Name == name);
+ var conflict = await _database.Timelines.AnyAsync(t => t.Name == name);
if (conflict)
throw new ConflictException(ExceptionTimelineNameConflict);
- var newEntity = CreateNewEntity(name, owner);
- Database.Timelines.Add(newEntity);
- await Database.SaveChangesAsync();
+ var newEntity = new TimelineEntity
+ {
+ CurrentPostLocalId = 0,
+ Name = name,
+ OwnerId = owner,
+ Visibility = TimelineVisibility.Register,
+ CreateTime = _clock.GetCurrentTime()
+ };
+
+ _database.Timelines.Add(newEntity);
+ await _database.SaveChangesAsync();
- return new TimelineInfo
+ return new Models.Timeline
{
Name = name,
Description = "",
- Owner = Mapper.Map<UserInfo>(user),
+ Owner = user,
Visibility = newEntity.Visibility,
- Members = new List<UserInfo>()
+ Members = new List<User>()
};
}
@@ -755,57 +969,103 @@ namespace Timeline.Services
ValidateTimelineName(name, nameof(name));
- var entity = await Database.Timelines.Where(t => t.Name == name).SingleOrDefaultAsync();
+ 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();
+ _database.Timelines.Remove(entity);
+ await _database.SaveChangesAsync();
}
- }
- public class PersonalTimelineService : BaseTimelineService, IPersonalTimelineService
- {
- public PersonalTimelineService(ILoggerFactory loggerFactory, DatabaseContext database, IUserService userService, IMapper mapper, IClock clock)
- : base(loggerFactory, database, userService, mapper, clock)
- {
- }
-
- protected override async Task<long> FindTimelineId(string name)
+ private IBaseTimelineService BranchName(string name, out string realName)
{
if (name == null)
throw new ArgumentNullException(nameof(name));
- long userId;
- try
+ if (name.StartsWith('@'))
{
- userId = await UserService.GetUserIdByUsername(name);
- }
- catch (ArgumentException e)
- {
- throw new ArgumentException(ExceptionFindTimelineUsernameBadFormat, nameof(name), e);
+ realName = name.Substring(1);
+ return _personalTimelineService;
}
- catch (UserNotExistException e)
+ else
{
- throw new TimelineNotExistException(name, e);
+ realName = name;
+ return _ordinaryTimelineService;
}
+ }
- var timelineEntity = await Database.Timelines.Where(t => t.OwnerId == userId && t.Name == null).Select(t => new { t.Id }).SingleOrDefaultAsync();
+ public Task<Models.Timeline> GetTimeline(string name)
+ {
+ var s = BranchName(name, out var realName);
+ return s.GetTimeline(realName);
+ }
- if (timelineEntity != null)
- {
- return timelineEntity.Id;
- }
- else
- {
- var newTimelineEntity = CreateNewEntity(null, userId);
- Database.Timelines.Add(newTimelineEntity);
- await Database.SaveChangesAsync();
+ public Task ChangeProperty(string name, TimelineChangePropertyRequest newProperties)
+ {
+ var s = BranchName(name, out var realName);
+ return s.ChangeProperty(realName, newProperties);
+ }
- return newTimelineEntity.Id;
- }
+ public Task<List<TimelinePost>> GetPosts(string name)
+ {
+ var s = BranchName(name, out var realName);
+ return s.GetPosts(realName);
+ }
+
+ public Task<PostData> GetPostData(string name, long postId)
+ {
+ var s = BranchName(name, out var realName);
+ return s.GetPostData(realName, postId);
+ }
+
+ public Task<TimelinePost> 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<TimelinePost> 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<string>? add, IList<string>? remove)
+ {
+ var s = BranchName(name, out var realName);
+ return s.ChangeMember(realName, add, remove);
+ }
+
+ public Task<bool> HasManagePermission(string name, long userId)
+ {
+ var s = BranchName(name, out var realName);
+ return s.HasManagePermission(realName, userId);
+ }
+
+ public Task<bool> HasReadPermission(string name, long? visitorId)
+ {
+ var s = BranchName(name, out var realName);
+ return s.HasReadPermission(realName, visitorId);
+ }
+
+ public Task<bool> 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<bool> IsMemberOf(string name, long userId)
+ {
+ var s = BranchName(name, out var realName);
+ return s.IsMemberOf(realName, userId);
}
}
}
diff --git a/Timeline/Services/User.cs b/Timeline/Services/User.cs
deleted file mode 100644
index 09a472e5..00000000
--- a/Timeline/Services/User.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-namespace Timeline.Services
-{
- public class User
- {
- public string? Username { get; set; }
- public string? Nickname { get; set; }
-
- #region adminsecret
- public bool? Administrator { get; set; }
- #endregion adminsecret
-
- #region secret
- public long? Id { get; set; }
- public string? Password { get; set; }
- public long? Version { get; set; }
- #endregion secret
- }
-}
diff --git a/Timeline/Services/UserAvatarService.cs b/Timeline/Services/UserAvatarService.cs
index 52d079a3..1b1be698 100644
--- a/Timeline/Services/UserAvatarService.cs
+++ b/Timeline/Services/UserAvatarService.cs
@@ -1,10 +1,7 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
-using SixLabors.ImageSharp;
-using SixLabors.ImageSharp.Formats;
using System;
using System.IO;
using System.Linq;
@@ -47,16 +44,6 @@ namespace Timeline.Services
Task<AvatarInfo> GetDefaultAvatar();
}
- public interface IUserAvatarValidator
- {
- /// <summary>
- /// Validate a avatar's format and size info.
- /// </summary>
- /// <param name="avatar">The avatar to validate.</param>
- /// <exception cref="AvatarFormatException">Thrown when validation failed.</exception>
- Task Validate(Avatar avatar);
- }
-
public interface IUserAvatarService
{
/// <summary>
@@ -79,7 +66,7 @@ namespace Timeline.Services
/// <param name="id">The id of the user to set avatar for.</param>
/// <param name="avatar">The avatar. Can be null to delete the saved avatar.</param>
/// <exception cref="ArgumentException">Thrown if any field in <paramref name="avatar"/> is null when <paramref name="avatar"/> is not null.</exception>
- /// <exception cref="AvatarFormatException">Thrown if avatar is of bad format.</exception>
+ /// <exception cref="ImageException">Thrown if avatar is of bad format.</exception>
Task SetAvatar(long id, Avatar? avatar);
}
@@ -132,28 +119,6 @@ namespace Timeline.Services
}
}
- public class UserAvatarValidator : IUserAvatarValidator
- {
- public Task Validate(Avatar avatar)
- {
- return Task.Run(() =>
- {
- try
- {
- using var image = Image.Load(avatar.Data, out IImageFormat format);
- if (!format.MimeTypes.Contains(avatar.Type))
- throw new AvatarFormatException(avatar, AvatarFormatException.ErrorReason.UnmatchedFormat);
- if (image.Width != image.Height)
- throw new AvatarFormatException(avatar, AvatarFormatException.ErrorReason.BadSize);
- }
- catch (UnknownImageFormatException e)
- {
- throw new AvatarFormatException(avatar, AvatarFormatException.ErrorReason.CantDecode, e);
- }
- });
- }
- }
-
public class UserAvatarService : IUserAvatarService
{
@@ -162,7 +127,8 @@ namespace Timeline.Services
private readonly DatabaseContext _database;
private readonly IDefaultUserAvatarProvider _defaultUserAvatarProvider;
- private readonly IUserAvatarValidator _avatarValidator;
+
+ private readonly IImageValidator _imageValidator;
private readonly IDataManager _dataManager;
@@ -172,14 +138,14 @@ namespace Timeline.Services
ILogger<UserAvatarService> logger,
DatabaseContext database,
IDefaultUserAvatarProvider defaultUserAvatarProvider,
- IUserAvatarValidator avatarValidator,
+ IImageValidator imageValidator,
IDataManager dataManager,
IClock clock)
{
_logger = logger;
_database = database;
_defaultUserAvatarProvider = defaultUserAvatarProvider;
- _avatarValidator = avatarValidator;
+ _imageValidator = imageValidator;
_dataManager = dataManager;
_clock = clock;
}
@@ -257,7 +223,7 @@ namespace Timeline.Services
}
else
{
- await _avatarValidator.Validate(avatar);
+ await _imageValidator.Validate(avatar.Data, avatar.Type, true);
var tag = await _dataManager.RetainEntry(avatar.Data);
var oldTag = avatarEntity?.DataTag;
var create = avatarEntity == null;
@@ -288,7 +254,6 @@ namespace Timeline.Services
{
services.AddScoped<IUserAvatarService, UserAvatarService>();
services.AddScoped<IDefaultUserAvatarProvider, DefaultUserAvatarProvider>();
- services.AddTransient<IUserAvatarValidator, UserAvatarValidator>();
}
}
}
diff --git a/Timeline/Services/UserService.cs b/Timeline/Services/UserService.cs
index 7dc7159d..e0a5ab50 100644
--- a/Timeline/Services/UserService.cs
+++ b/Timeline/Services/UserService.cs
@@ -6,6 +6,7 @@ using System.Linq;
using System.Threading.Tasks;
using Timeline.Entities;
using Timeline.Helpers;
+using Timeline.Models;
using Timeline.Models.Validation;
using static Timeline.Resources.Services.UserService;
diff --git a/Timeline/Services/UserTokenManager.cs b/Timeline/Services/UserTokenManager.cs
index 4e54c4cd..6decf8f9 100644
--- a/Timeline/Services/UserTokenManager.cs
+++ b/Timeline/Services/UserTokenManager.cs
@@ -1,6 +1,7 @@
using Microsoft.Extensions.Logging;
using System;
using System.Threading.Tasks;
+using Timeline.Models;
namespace Timeline.Services
{