aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Timeline.Tests/Controllers/PersonalTimelineControllerTest.cs2
-rw-r--r--Timeline/Controllers/PersonalTimelineController.cs4
-rw-r--r--Timeline/Entities/DatabaseContext.cs1
-rw-r--r--Timeline/Services/TimelineService.cs336
-rw-r--r--Timeline/Services/UsernameBadFormatException.cs4
5 files changed, 336 insertions, 11 deletions
diff --git a/Timeline.Tests/Controllers/PersonalTimelineControllerTest.cs b/Timeline.Tests/Controllers/PersonalTimelineControllerTest.cs
index aecd10af..a7cbb37e 100644
--- a/Timeline.Tests/Controllers/PersonalTimelineControllerTest.cs
+++ b/Timeline.Tests/Controllers/PersonalTimelineControllerTest.cs
@@ -367,7 +367,7 @@ namespace Timeline.Tests.Controllers
});
result.Should().NotBeNull().And.BeAssignableTo<BadRequestObjectResult>()
.Which.Value.Should().BeAssignableTo<CommonResponse>()
- .Which.Code.Should().Be(ErrorCodes.Http.Timeline.MemberAddNotExist);
+ .Which.Code.Should().Be(ErrorCodes.Http.Timeline.ChangeMemberUserNotExist);
_service.VerifyAll();
}
diff --git a/Timeline/Controllers/PersonalTimelineController.cs b/Timeline/Controllers/PersonalTimelineController.cs
index f0f4e4c2..af6a70f8 100644
--- a/Timeline/Controllers/PersonalTimelineController.cs
+++ b/Timeline/Controllers/PersonalTimelineController.cs
@@ -25,7 +25,7 @@ namespace Timeline
public const int PostOperationCreateForbid = 10040102;
public const int PostOperationDeleteForbid = 10040103;
public const int PostOperationDeleteNotExist = 10040201;
- public const int MemberAddNotExist = 10040301;
+ public const int ChangeMemberUserNotExist = 10040301;
}
}
}
@@ -156,7 +156,7 @@ namespace Timeline.Controllers
}
else if (e.InnerException is UserNotExistException)
{
- return BadRequest(new CommonResponse(ErrorCodes.Http.Timeline.MemberAddNotExist,
+ return BadRequest(new CommonResponse(ErrorCodes.Http.Timeline.ChangeMemberUserNotExist,
string.Format(CultureInfo.CurrentCulture, MessageMemberUserNotExist, e.Index, e.Operation)));
}
diff --git a/Timeline/Entities/DatabaseContext.cs b/Timeline/Entities/DatabaseContext.cs
index 19df32c6..123ae0f3 100644
--- a/Timeline/Entities/DatabaseContext.cs
+++ b/Timeline/Entities/DatabaseContext.cs
@@ -22,5 +22,6 @@ namespace Timeline.Entities
public DbSet<UserDetail> UserDetails { get; set; } = default!;
public DbSet<TimelineEntity> Timelines { get; set; } = default!;
public DbSet<TimelinePostEntity> TimelinePosts { get; set; } = default!;
+ public DbSet<TimelineMemberEntity> TimelineMembers { get; set; } = default!;
}
}
diff --git a/Timeline/Services/TimelineService.cs b/Timeline/Services/TimelineService.cs
index 28b1f91d..eff0c3fc 100644
--- a/Timeline/Services/TimelineService.cs
+++ b/Timeline/Services/TimelineService.cs
@@ -1,10 +1,12 @@
-using System;
+using Microsoft.EntityFrameworkCore;
+using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Timeline.Entities;
using Timeline.Models;
using Timeline.Models.Http;
+using Timeline.Models.Validation;
namespace Timeline.Services
{
@@ -135,14 +137,14 @@ namespace Timeline.Services
/// The inner exception is <see cref="UsernameBadFormatException"/>
/// when one of the username is invalid.
/// The inner exception is <see cref="UserNotExistException"/>
- /// when one of the user to add does not exist.
+ /// when one of the user to change does not exist.
/// </exception>
/// <remarks>
- /// Operating on a username that is of bad format always throws.
+ /// Operating on a username that is of bad format or does not exist always throws.
/// Add a user that already is a member has no effects.
/// Remove a user that is not a member also has not effects.
- /// Add a user that does not exist will throw <see cref="TimelineMemberOperationUserException"/>.
- /// But remove one does not throw.
+ /// Add and remove an identical user results in no effects.
+ /// More than one same usernames are regarded as one.
/// </remarks>
Task ChangeMember(string name, IList<string>? add, IList<string>? remove);
@@ -151,6 +153,7 @@ namespace Timeline.Services
/// </summary>
/// <param name="name">Username or the timeline name. See remarks of <see cref="IBaseTimelineService"/>.</param>
/// <param name="username">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="TimelineNameBadFormatException">
/// Thrown when timeline name is of bad format.
@@ -164,7 +167,12 @@ namespace Timeline.Services
/// For personal timeline, it means the user of that username does not exist
/// and the inner exception should be a <see cref="UserNotExistException"/>.
/// </exception>
- /// <returns>True if can read, false if can't read.</returns>
+ /// <exception cref="UsernameBadFormatException">
+ /// Thrown when <paramref name="username"/> is of bad format.
+ /// </exception>
+ /// <exception cref="UserNotExistException">
+ /// Thrown when <paramref name="username"/> does not exist.
+ /// </exception>
Task<bool> HasReadPermission(string name, string? username);
/// <summary>
@@ -285,4 +293,320 @@ namespace Timeline.Services
/// </exception>
Task<BaseTimelineInfo> GetTimeline(string username);
}
+
+ public abstract class BaseTimelineService : IBaseTimelineService
+ {
+ protected BaseTimelineService(DatabaseContext database, IClock clock)
+ {
+ Clock = clock;
+ Database = database;
+ }
+
+ protected IClock Clock { get; }
+
+ protected UsernameValidator UsernameValidator { get; } = new UsernameValidator();
+
+ protected DatabaseContext Database { get; }
+
+ /// <summary>
+ /// Find the timeline id by the name.
+ /// For details, see remarks.
+ /// </summary>
+ /// <param name="name">The username or the timeline name. See remarks.</param>
+ /// <returns>The id of the timeline entity.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> is null.</exception>
+ /// <exception cref="TimelineNameBadFormatException">
+ /// Thrown when timeline name is of bad format.
+ /// For normal timeline, it means name is an empty string.
+ /// For personal timeline, it means the username is of bad format,
+ /// the inner exception should be a <see cref="UsernameBadFormatException"/>.
+ /// </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>
+ /// <remarks>
+ /// This is the common but different part for both types of timeline service.
+ /// For class that implements <see cref="IPersonalTimelineService"/>, this method should
+ /// find the timeline entity id by the given <paramref name="name"/> as the username of the owner.
+ /// For class that implements <see cref="ITimelineService"/>, this method should
+ /// find the timeline entity id by the given <paramref name="name"/> as the timeline name.
+ /// This method should be called by many other method that follows the contract.
+ /// </remarks>
+ protected abstract Task<long> FindTimelineId(string name);
+
+ public async Task<List<TimelinePostInfo>> GetPosts(string name)
+ {
+ if (name == null)
+ throw new ArgumentNullException(nameof(name));
+
+ var timelineId = await FindTimelineId(name);
+ var postEntities = await Database.TimelinePosts.Where(p => p.TimelineId == timelineId).ToListAsync();
+ var posts = new List<TimelinePostInfo>(await Task.WhenAll(postEntities.Select(async p => new TimelinePostInfo
+ {
+ Id = p.Id,
+ Content = p.Content,
+ Author = (await Database.Users.Where(u => u.Id == p.AuthorId).Select(u => new { u.Name }).SingleAsync()).Name,
+ Time = p.Time
+ })));
+ return posts;
+ }
+
+ public async Task<TimelinePostCreateResponse> CreatePost(string name, string author, string content, DateTime? time)
+ {
+ if (name == null)
+ throw new ArgumentNullException(nameof(name));
+ if (author == null)
+ throw new ArgumentNullException(nameof(author));
+ if (content == null)
+ throw new ArgumentNullException(nameof(content));
+
+ {
+ var (result, message) = UsernameValidator.Validate(author);
+ if (!result)
+ {
+ throw new UsernameBadFormatException(author, message);
+ }
+ }
+
+ var timelineId = await FindTimelineId(name);
+
+ var authorEntity = Database.Users.Where(u => u.Name == author).Select(u => new { u.Id }).SingleOrDefault();
+ if (authorEntity == null)
+ {
+ throw new UserNotExistException(author);
+ }
+ var authorId = authorEntity.Id;
+
+ var currentTime = Clock.GetCurrentTime();
+
+ var postEntity = new TimelinePostEntity
+ {
+ Content = content,
+ AuthorId = authorId,
+ TimelineId = timelineId,
+ Time = time ?? currentTime,
+ LastUpdated = currentTime
+ };
+
+ Database.TimelinePosts.Add(postEntity);
+ await Database.SaveChangesAsync();
+
+ return new TimelinePostCreateResponse
+ {
+ Id = postEntity.Id,
+ Time = postEntity.Time
+ };
+ }
+
+ public async Task DeletePost(string name, long id)
+ {
+ if (name == null)
+ throw new ArgumentNullException(nameof(name));
+
+ var timelineId = FindTimelineId(name);
+
+ var post = await Database.TimelinePosts.Where(p => p.Id == id).SingleOrDefaultAsync();
+
+ if (post == null)
+ throw new TimelinePostNotExistException(id);
+
+ Database.TimelinePosts.Remove(post);
+ await Database.SaveChangesAsync();
+ }
+
+ public async Task ChangeProperty(string name, TimelinePropertyChangeRequest newProperties)
+ {
+ if (name == null)
+ throw new ArgumentNullException(nameof(name));
+ if (newProperties == null)
+ throw new ArgumentNullException(nameof(newProperties));
+
+ var timelineId = await FindTimelineId(name);
+
+ var timelineEntity = await Database.Timelines.Where(t => t.Id == timelineId).SingleAsync();
+
+ if (newProperties.Description != null)
+ {
+ timelineEntity.Description = newProperties.Description;
+ }
+
+ if (newProperties.Visibility.HasValue)
+ {
+ timelineEntity.Visibility = newProperties.Visibility.Value;
+ }
+
+ await Database.SaveChangesAsync();
+ }
+
+ public async Task ChangeMember(string name, IList<string>? add, IList<string>? remove)
+ {
+ if (name == null)
+ throw new ArgumentNullException(nameof(name));
+
+ // remove duplication and check the format of each username.
+ // Return a username->index map.
+ Dictionary<string, int>? RemoveDuplicateAndCheckFormat(IList<string>? list, TimelineMemberOperationUserException.MemberOperation operation)
+ {
+ if (list != null)
+ {
+ Dictionary<string, int> result = new Dictionary<string, int>();
+ var count = 0;
+ for (var index = 0; index < count; index++)
+ {
+ var username = list[index];
+ if (result.ContainsKey(username))
+ {
+ continue;
+ }
+ var (validationResult, message) = UsernameValidator.Validate(username);
+ if (!validationResult)
+ throw new TimelineMemberOperationUserException(
+ index, operation, username,
+ new UsernameBadFormatException(username, message));
+ result.Add(username, index);
+ }
+ return result;
+ }
+ else
+ {
+ return null;
+ }
+ }
+ var simplifiedAdd = RemoveDuplicateAndCheckFormat(add, TimelineMemberOperationUserException.MemberOperation.Add);
+ var simplifiedRemove = RemoveDuplicateAndCheckFormat(remove, TimelineMemberOperationUserException.MemberOperation.Remove);
+
+ // remove those both in add and remove
+ if (simplifiedAdd != null && simplifiedRemove != null)
+ {
+ var usersToClean = simplifiedRemove.Keys.Where(u => simplifiedAdd.ContainsKey(u));
+ foreach (var u in usersToClean)
+ {
+ simplifiedAdd.Remove(u);
+ simplifiedRemove.Remove(u);
+ }
+ }
+
+ var timelineId = await FindTimelineId(name);
+
+ async Task<List<long>?> CheckExistenceAndGetId(Dictionary<string, int>? map, TimelineMemberOperationUserException.MemberOperation operation)
+ {
+ if (map == null)
+ return null;
+
+ List<long> result = new List<long>();
+ foreach (var (username, index) in map)
+ {
+ var user = await Database.Users.Where(u => u.Name == username).Select(u => new { u.Id }).SingleOrDefaultAsync();
+ if (user == null)
+ {
+ throw new TimelineMemberOperationUserException(index, operation, username,
+ new UserNotExistException(username));
+ }
+ result.Add(user.Id);
+ }
+ return result;
+ }
+ var userIdsAdd = await CheckExistenceAndGetId(simplifiedAdd, TimelineMemberOperationUserException.MemberOperation.Add);
+ var userIdsRemove = await CheckExistenceAndGetId(simplifiedRemove, TimelineMemberOperationUserException.MemberOperation.Remove);
+
+ if (userIdsAdd != null)
+ {
+ var membersToAdd = userIdsAdd.Select(id => new TimelineMemberEntity { UserId = id, TimelineId = timelineId }).ToList();
+ Database.TimelineMembers.AddRange(membersToAdd);
+ }
+
+ if (userIdsRemove != null)
+ {
+ var membersToRemove = await Database.TimelineMembers.Where(m => m.TimelineId == timelineId && userIdsRemove.Contains(m.UserId)).ToListAsync();
+ Database.TimelineMembers.RemoveRange(membersToRemove);
+ }
+
+ await Database.SaveChangesAsync();
+ }
+
+ public async Task<bool> HasReadPermission(string name, string? username)
+ {
+ if (name == null)
+ throw new ArgumentNullException(nameof(name));
+
+ long? userId = null;
+ if (username != null)
+ {
+ var (result, message) = UsernameValidator.Validate(username);
+ if (!result)
+ {
+ throw new UsernameBadFormatException(username);
+ }
+
+ var user = await Database.Users.Where(u => u.Name == username).Select(u => new { u.Id }).SingleOrDefaultAsync();
+
+ if (user == null)
+ {
+ throw new UserNotExistException(username);
+ }
+
+ userId = user.Id;
+ }
+
+ var timelineId = await FindTimelineId(name);
+
+ var timelineEntity = await Database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.Visibility }).SingleAsync();
+
+ if (timelineEntity.Visibility == TimelineVisibility.Public)
+ return true;
+
+ if (timelineEntity.Visibility == TimelineVisibility.Register && username != null)
+ return true;
+
+ if (userId == null)
+ {
+ return false;
+ }
+ else
+ {
+ var memberEntity = await Database.TimelineMembers.Where(m => m.UserId == userId && m.TimelineId == timelineId).SingleOrDefaultAsync();
+ return memberEntity != null;
+ }
+ }
+
+ public async Task<bool> HasPostModifyPermission(string name, long id, string username)
+ {
+ if (name == null)
+ throw new ArgumentNullException(nameof(name));
+ if (username == null)
+ throw new ArgumentNullException(nameof(username));
+
+ {
+ var (result, message) = UsernameValidator.Validate(username);
+ if (!result)
+ {
+ throw new UsernameBadFormatException(username);
+ }
+ }
+
+ var user = await Database.Users.Where(u => u.Name == username).Select(u => new { u.Id }).SingleOrDefaultAsync();
+
+ if (user == null)
+ {
+ throw new UserNotExistException(username);
+ }
+
+ var userId = user.Id;
+
+ var timelineId = await FindTimelineId(name);
+
+ var timelineEntity = await Database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.OwnerId }).SingleAsync();
+
+ var postEntitu = await Database.Timelines. // TODO!
+
+ if (timelineEntity.OwnerId == userId)
+ {
+ return true;
+ }
+ }
+
+ }
}
diff --git a/Timeline/Services/UsernameBadFormatException.cs b/Timeline/Services/UsernameBadFormatException.cs
index 04354d22..d82bf962 100644
--- a/Timeline/Services/UsernameBadFormatException.cs
+++ b/Timeline/Services/UsernameBadFormatException.cs
@@ -9,8 +9,8 @@ namespace Timeline.Services
public class UsernameBadFormatException : Exception
{
public UsernameBadFormatException() : base(Resources.Services.Exception.UsernameBadFormatException) { }
- public UsernameBadFormatException(string message) : base(message) { }
- public UsernameBadFormatException(string message, Exception inner) : base(message, inner) { }
+ public UsernameBadFormatException(string username) : this() { Username = username; }
+ public UsernameBadFormatException(string username, Exception inner) : base(Resources.Services.Exception.UsernameBadFormatException, inner) { Username = username; }
public UsernameBadFormatException(string username, string message) : base(message) { Username = username; }
public UsernameBadFormatException(string username, string message, Exception inner) : base(message, inner) { Username = username; }