diff options
-rw-r--r-- | BackEnd/Timeline.Tests/GlobalSuppressions.cs | 25 | ||||
-rw-r--r-- | BackEnd/Timeline.Tests/Services/TimelinePostServiceTest.cs | 200 | ||||
-rw-r--r-- | BackEnd/Timeline.Tests/Services/TimelineServiceTest.cs | 170 | ||||
-rw-r--r-- | BackEnd/Timeline.Tests/Services/UserDeleteServiceTest.cs | 4 | ||||
-rw-r--r-- | BackEnd/Timeline/Controllers/TimelineController.cs | 18 | ||||
-rw-r--r-- | BackEnd/Timeline/Services/BasicTimelineService.cs | 122 | ||||
-rw-r--r-- | BackEnd/Timeline/Services/BasicUserService.cs | 66 | ||||
-rw-r--r-- | BackEnd/Timeline/Services/HighlightTimelineService.cs | 4 | ||||
-rw-r--r-- | BackEnd/Timeline/Services/TimelinePostService.cs | 493 | ||||
-rw-r--r-- | BackEnd/Timeline/Services/TimelineService.cs | 588 | ||||
-rw-r--r-- | BackEnd/Timeline/Services/UserDeleteService.cs | 8 | ||||
-rw-r--r-- | BackEnd/Timeline/Services/UserService.cs | 41 | ||||
-rw-r--r-- | BackEnd/Timeline/Startup.cs | 18 |
13 files changed, 939 insertions, 818 deletions
diff --git a/BackEnd/Timeline.Tests/GlobalSuppressions.cs b/BackEnd/Timeline.Tests/GlobalSuppressions.cs index 0f873033..da9481f1 100644 --- a/BackEnd/Timeline.Tests/GlobalSuppressions.cs +++ b/BackEnd/Timeline.Tests/GlobalSuppressions.cs @@ -3,14 +3,17 @@ // Project-level suppressions either have no target or are given
// a specific target and scoped to a namespace, type, member, etc.
-[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task", Justification = "This is not a UI application.")]
-[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Tests name have underscores.")]
-[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Test may catch all exceptions.")]
-[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1034:Nested types should not be visible", Justification = "Test classes can be nested.")]
-[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "This is redundant.")]
-[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1063:Implement IDisposable Correctly", Justification = "Test classes do not need to implement it that way.")]
-[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA1816:Dispose methods should call SuppressFinalize", Justification = "Test classes do not need to implement it that way.")]
-[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2234:Pass system uri objects instead of strings", Justification = "I really don't understand this rule.")]
-[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Globalization", "CA1303:Do not pass literals as localized parameters", Justification = "Tests do not need make strings resources.")]
-[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1054:Uri parameters should not be strings", Justification = "That's unnecessary.")]
-[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1056:Uri properties should not be strings", Justification = "That's unnecessary.")]
+using System.Diagnostics.CodeAnalysis;
+
+[assembly: SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task", Justification = "This is not a UI application.")]
+[assembly: SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Tests name have underscores.")]
+[assembly: SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Test may catch all exceptions.")]
+[assembly: SuppressMessage("Design", "CA1034:Nested types should not be visible", Justification = "Test classes can be nested.")]
+[assembly: SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "This is redundant.")]
+[assembly: SuppressMessage("Design", "CA1063:Implement IDisposable Correctly", Justification = "Test classes do not need to implement it that way.")]
+[assembly: SuppressMessage("Usage", "CA1816:Dispose methods should call SuppressFinalize", Justification = "Test classes do not need to implement it that way.")]
+[assembly: SuppressMessage("Usage", "CA2234:Pass system uri objects instead of strings", Justification = "I really don't understand this rule.")]
+[assembly: SuppressMessage("Globalization", "CA1303:Do not pass literals as localized parameters", Justification = "Tests do not need make strings resources.")]
+[assembly: SuppressMessage("Design", "CA1054:Uri parameters should not be strings", Justification = "That's unnecessary.")]
+[assembly: SuppressMessage("Design", "CA1056:Uri properties should not be strings", Justification = "That's unnecessary.")]
+[assembly: SuppressMessage("Design", "CA1001:Types that own disposable fields should be disposable", Justification = "We use another contract like IAsyncLifetime.")]
diff --git a/BackEnd/Timeline.Tests/Services/TimelinePostServiceTest.cs b/BackEnd/Timeline.Tests/Services/TimelinePostServiceTest.cs new file mode 100644 index 00000000..97512be5 --- /dev/null +++ b/BackEnd/Timeline.Tests/Services/TimelinePostServiceTest.cs @@ -0,0 +1,200 @@ +using FluentAssertions;
+using Microsoft.Extensions.Logging.Abstractions;
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Timeline.Models;
+using Timeline.Services;
+using Timeline.Tests.Helpers;
+using Xunit;
+
+namespace Timeline.Tests.Services
+{
+ public class TimelinePostServiceTest : DatabaseBasedTest
+ {
+ private readonly PasswordService _passwordService = new PasswordService();
+
+ private readonly ETagGenerator _eTagGenerator = new ETagGenerator();
+
+ private readonly ImageValidator _imageValidator = new ImageValidator();
+
+ private readonly TestClock _clock = new TestClock();
+
+ private DataManager _dataManager = default!;
+
+ private UserPermissionService _userPermissionService = default!;
+
+ private UserService _userService = default!;
+
+ private TimelineService _timelineService = default!;
+
+ private TimelinePostService _timelinePostService = default!;
+
+ private UserDeleteService _userDeleteService = default!;
+
+ protected override void OnDatabaseCreated()
+ {
+ _dataManager = new DataManager(Database, _eTagGenerator);
+ _userPermissionService = new UserPermissionService(Database);
+ _userService = new UserService(NullLogger<UserService>.Instance, Database, _passwordService, _clock, _userPermissionService);
+ _timelineService = new TimelineService(Database, _userService, _clock);
+ _timelinePostService = new TimelinePostService(NullLogger<TimelinePostService>.Instance, Database, _timelineService, _userService, _dataManager, _imageValidator, _clock);
+ _userDeleteService = new UserDeleteService(NullLogger<UserDeleteService>.Instance, Database, _timelinePostService);
+ }
+
+ protected override void BeforeDatabaseDestroy()
+ {
+ _eTagGenerator.Dispose();
+ }
+
+ [Theory]
+ [InlineData("@user")]
+ [InlineData("tl")]
+ public async Task GetPosts_ModifiedSince(string timelineName)
+ {
+ _clock.ForwardCurrentTime();
+
+ var userId = await _userService.GetUserIdByUsername("user");
+
+ var _ = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal);
+ if (!isPersonal)
+ await _timelineService.CreateTimeline(timelineName, userId);
+
+ var postContentList = new string[] { "a", "b", "c", "d" };
+
+ DateTime testPoint = new DateTime();
+
+ foreach (var (content, index) in postContentList.Select((v, i) => (v, i)))
+ {
+ var t = _clock.ForwardCurrentTime();
+ if (index == 1)
+ testPoint = t;
+ await _timelinePostService.CreateTextPost(timelineName, userId, content, null);
+ }
+
+ var posts = await _timelinePostService.GetPosts(timelineName, testPoint);
+ posts.Should().HaveCount(3)
+ .And.Subject.Select(p => ((TextTimelinePostContent)p.Content!).Text).Should().Equal(postContentList.Skip(1));
+ }
+
+ [Theory]
+ [InlineData("@user")]
+ [InlineData("tl")]
+ public async Task GetPosts_IncludeDeleted(string timelineName)
+ {
+ var userId = await _userService.GetUserIdByUsername("user");
+
+ var _ = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal);
+ if (!isPersonal)
+ await _timelineService.CreateTimeline(timelineName, userId);
+
+ var postContentList = new string[] { "a", "b", "c", "d" };
+
+ foreach (var content in postContentList)
+ {
+ await _timelinePostService.CreateTextPost(timelineName, userId, content, null);
+ }
+
+ var posts = await _timelinePostService.GetPosts(timelineName);
+ posts.Should().HaveCount(4);
+ posts.Select(p => p.Deleted).Should().Equal(Enumerable.Repeat(false, posts.Count));
+ posts.Select(p => ((TextTimelinePostContent)p.Content!).Text).Should().Equal(postContentList);
+
+ foreach (var id in new long[] { posts[0].Id, posts[2].Id })
+ {
+ await _timelinePostService.DeletePost(timelineName, id);
+ }
+
+ posts = await _timelinePostService.GetPosts(timelineName);
+ posts.Should().HaveCount(2);
+ posts.Select(p => p.Deleted).Should().Equal(Enumerable.Repeat(false, posts.Count));
+ posts.Select(p => ((TextTimelinePostContent)p.Content!).Text).Should().Equal(new string[] { "b", "d" });
+
+ posts = await _timelinePostService.GetPosts(timelineName, includeDeleted: true);
+ posts.Should().HaveCount(4);
+ posts.Select(p => p.Deleted).Should().Equal(new bool[] { true, false, true, false });
+ posts.Where(p => !p.Deleted).Select(p => ((TextTimelinePostContent)p.Content!).Text).Should().Equal(new string[] { "b", "d" });
+ }
+
+ [Theory]
+ [InlineData("@admin")]
+ [InlineData("tl")]
+ public async Task GetPosts_ModifiedSince_UsernameChange(string timelineName)
+ {
+ var time1 = _clock.ForwardCurrentTime();
+
+ var userId = await _userService.GetUserIdByUsername("user");
+
+ var _ = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal);
+ if (!isPersonal)
+ await _timelineService.CreateTimeline(timelineName, userId);
+
+ var postContentList = new string[] { "a", "b", "c", "d" };
+
+ foreach (var (content, index) in postContentList.Select((v, i) => (v, i)))
+ {
+ await _timelinePostService.CreateTextPost(timelineName, userId, content, null);
+ }
+
+ var time2 = _clock.ForwardCurrentTime();
+
+ {
+ var posts = await _timelinePostService.GetPosts(timelineName, time2);
+ posts.Should().HaveCount(0);
+ }
+
+ {
+ await _userService.ModifyUser(userId, new ModifyUserParams { Nickname = "haha" });
+ var posts = await _timelinePostService.GetPosts(timelineName, time2);
+ posts.Should().HaveCount(0);
+ }
+
+ {
+ await _userService.ModifyUser(userId, new ModifyUserParams { Username = "haha" });
+ var posts = await _timelinePostService.GetPosts(timelineName, time2);
+ posts.Should().HaveCount(4);
+ }
+ }
+
+ [Theory]
+ [InlineData("@admin")]
+ [InlineData("tl")]
+ public async Task GetPosts_ModifiedSince_UserDelete(string timelineName)
+ {
+ var time1 = _clock.ForwardCurrentTime();
+
+ var userId = await _userService.GetUserIdByUsername("user");
+ var adminId = await _userService.GetUserIdByUsername("admin");
+
+ var _ = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal);
+ if (!isPersonal)
+ await _timelineService.CreateTimeline(timelineName, adminId);
+
+ var postContentList = new string[] { "a", "b", "c", "d" };
+
+ foreach (var (content, index) in postContentList.Select((v, i) => (v, i)))
+ {
+ await _timelinePostService.CreateTextPost(timelineName, userId, content, null);
+ }
+
+ var time2 = _clock.ForwardCurrentTime();
+
+ {
+ var posts = await _timelinePostService.GetPosts(timelineName, time2);
+ posts.Should().HaveCount(0);
+ }
+
+ await _userDeleteService.DeleteUser("user");
+
+ {
+ var posts = await _timelinePostService.GetPosts(timelineName, time2);
+ posts.Should().HaveCount(0);
+ }
+
+ {
+ var posts = await _timelinePostService.GetPosts(timelineName, time2, true);
+ posts.Should().HaveCount(4);
+ }
+ }
+ }
+}
diff --git a/BackEnd/Timeline.Tests/Services/TimelineServiceTest.cs b/BackEnd/Timeline.Tests/Services/TimelineServiceTest.cs index 5f2c20e8..fac0b6f3 100644 --- a/BackEnd/Timeline.Tests/Services/TimelineServiceTest.cs +++ b/BackEnd/Timeline.Tests/Services/TimelineServiceTest.cs @@ -2,7 +2,6 @@ using Microsoft.Extensions.Logging.Abstractions;
using System;
using System.Collections.Generic;
-using System.Linq;
using System.Threading.Tasks;
using Timeline.Models;
using Timeline.Services;
@@ -12,38 +11,23 @@ using Xunit; namespace Timeline.Tests.Services
{
- public class TimelineServiceTest : DatabaseBasedTest, IDisposable
+ public class TimelineServiceTest : DatabaseBasedTest
{
private readonly PasswordService _passwordService = new PasswordService();
- private readonly ETagGenerator _eTagGenerator = new ETagGenerator();
-
- private readonly ImageValidator _imageValidator = new ImageValidator();
-
private readonly TestClock _clock = new TestClock();
- private DataManager _dataManager = default!;
-
private UserPermissionService _userPermissionService = default!;
private UserService _userService = default!;
private TimelineService _timelineService = default!;
- private UserDeleteService _userDeleteService = default!;
-
protected override void OnDatabaseCreated()
{
- _dataManager = new DataManager(Database, _eTagGenerator);
_userPermissionService = new UserPermissionService(Database);
_userService = new UserService(NullLogger<UserService>.Instance, Database, _passwordService, _clock, _userPermissionService);
- _timelineService = new TimelineService(NullLogger<TimelineService>.Instance, Database, _dataManager, _userService, _imageValidator, _clock);
- _userDeleteService = new UserDeleteService(NullLogger<UserDeleteService>.Instance, Database, _timelineService);
- }
-
- public void Dispose()
- {
- _eTagGenerator.Dispose();
+ _timelineService = new TimelineService(Database, _userService, _clock);
}
[Theory]
@@ -110,156 +94,6 @@ namespace Timeline.Tests.Services }
[Theory]
- [InlineData("@user")]
- [InlineData("tl")]
- public async Task GetPosts_ModifiedSince(string timelineName)
- {
- _clock.ForwardCurrentTime();
-
- var userId = await _userService.GetUserIdByUsername("user");
-
- var _ = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal);
- if (!isPersonal)
- await _timelineService.CreateTimeline(timelineName, userId);
-
- var postContentList = new string[] { "a", "b", "c", "d" };
-
- DateTime testPoint = new DateTime();
-
- foreach (var (content, index) in postContentList.Select((v, i) => (v, i)))
- {
- var t = _clock.ForwardCurrentTime();
- if (index == 1)
- testPoint = t;
- await _timelineService.CreateTextPost(timelineName, userId, content, null);
- }
-
- var posts = await _timelineService.GetPosts(timelineName, testPoint);
- posts.Should().HaveCount(3)
- .And.Subject.Select(p => ((TextTimelinePostContent)p.Content!).Text).Should().Equal(postContentList.Skip(1));
- }
-
- [Theory]
- [InlineData("@user")]
- [InlineData("tl")]
- public async Task GetPosts_IncludeDeleted(string timelineName)
- {
- var userId = await _userService.GetUserIdByUsername("user");
-
- var _ = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal);
- if (!isPersonal)
- await _timelineService.CreateTimeline(timelineName, userId);
-
- var postContentList = new string[] { "a", "b", "c", "d" };
-
- foreach (var content in postContentList)
- {
- await _timelineService.CreateTextPost(timelineName, userId, content, null);
- }
-
- var posts = await _timelineService.GetPosts(timelineName);
- posts.Should().HaveCount(4);
- posts.Select(p => p.Deleted).Should().Equal(Enumerable.Repeat(false, posts.Count));
- posts.Select(p => ((TextTimelinePostContent)p.Content!).Text).Should().Equal(postContentList);
-
- foreach (var id in new long[] { posts[0].Id, posts[2].Id })
- {
- await _timelineService.DeletePost(timelineName, id);
- }
-
- posts = await _timelineService.GetPosts(timelineName);
- posts.Should().HaveCount(2);
- posts.Select(p => p.Deleted).Should().Equal(Enumerable.Repeat(false, posts.Count));
- posts.Select(p => ((TextTimelinePostContent)p.Content!).Text).Should().Equal(new string[] { "b", "d" });
-
- posts = await _timelineService.GetPosts(timelineName, includeDeleted: true);
- posts.Should().HaveCount(4);
- posts.Select(p => p.Deleted).Should().Equal(new bool[] { true, false, true, false });
- posts.Where(p => !p.Deleted).Select(p => ((TextTimelinePostContent)p.Content!).Text).Should().Equal(new string[] { "b", "d" });
- }
-
- [Theory]
- [InlineData("@admin")]
- [InlineData("tl")]
- public async Task GetPosts_ModifiedSince_UsernameChange(string timelineName)
- {
- var time1 = _clock.ForwardCurrentTime();
-
- var userId = await _userService.GetUserIdByUsername("user");
-
- var _ = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal);
- if (!isPersonal)
- await _timelineService.CreateTimeline(timelineName, userId);
-
- var postContentList = new string[] { "a", "b", "c", "d" };
-
- foreach (var (content, index) in postContentList.Select((v, i) => (v, i)))
- {
- await _timelineService.CreateTextPost(timelineName, userId, content, null);
- }
-
- var time2 = _clock.ForwardCurrentTime();
-
- {
- var posts = await _timelineService.GetPosts(timelineName, time2);
- posts.Should().HaveCount(0);
- }
-
- {
- await _userService.ModifyUser(userId, new ModifyUserParams { Nickname = "haha" });
- var posts = await _timelineService.GetPosts(timelineName, time2);
- posts.Should().HaveCount(0);
- }
-
- {
- await _userService.ModifyUser(userId, new ModifyUserParams { Username = "haha" });
- var posts = await _timelineService.GetPosts(timelineName, time2);
- posts.Should().HaveCount(4);
- }
- }
-
- [Theory]
- [InlineData("@admin")]
- [InlineData("tl")]
- public async Task GetPosts_ModifiedSince_UserDelete(string timelineName)
- {
- var time1 = _clock.ForwardCurrentTime();
-
- var userId = await _userService.GetUserIdByUsername("user");
- var adminId = await _userService.GetUserIdByUsername("admin");
-
- var _ = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal);
- if (!isPersonal)
- await _timelineService.CreateTimeline(timelineName, adminId);
-
- var postContentList = new string[] { "a", "b", "c", "d" };
-
- foreach (var (content, index) in postContentList.Select((v, i) => (v, i)))
- {
- await _timelineService.CreateTextPost(timelineName, userId, content, null);
- }
-
- var time2 = _clock.ForwardCurrentTime();
-
- {
- var posts = await _timelineService.GetPosts(timelineName, time2);
- posts.Should().HaveCount(0);
- }
-
- await _userDeleteService.DeleteUser("user");
-
- {
- var posts = await _timelineService.GetPosts(timelineName, time2);
- posts.Should().HaveCount(0);
- }
-
- {
- var posts = await _timelineService.GetPosts(timelineName, time2, true);
- posts.Should().HaveCount(4);
- }
- }
-
- [Theory]
[InlineData("@admin")]
[InlineData("tl")]
public async Task Title(string timelineName)
diff --git a/BackEnd/Timeline.Tests/Services/UserDeleteServiceTest.cs b/BackEnd/Timeline.Tests/Services/UserDeleteServiceTest.cs index be11564e..59c0a9af 100644 --- a/BackEnd/Timeline.Tests/Services/UserDeleteServiceTest.cs +++ b/BackEnd/Timeline.Tests/Services/UserDeleteServiceTest.cs @@ -10,12 +10,12 @@ namespace Timeline.Tests.Services {
public class UserDeleteServiceTest : DatabaseBasedTest
{
- private readonly Mock<ITimelineService> _mockTimelineService = new Mock<ITimelineService>();
+ private readonly Mock<ITimelinePostService> _mockTimelinePostService = new Mock<ITimelinePostService>();
private UserDeleteService _service = default!;
protected override void OnDatabaseCreated()
{
- _service = new UserDeleteService(NullLogger<UserDeleteService>.Instance, Database, _mockTimelineService.Object);
+ _service = new UserDeleteService(NullLogger<UserDeleteService>.Instance, Database, _mockTimelinePostService.Object);
}
[Fact]
diff --git a/BackEnd/Timeline/Controllers/TimelineController.cs b/BackEnd/Timeline/Controllers/TimelineController.cs index 45060b5d..0ffadc50 100644 --- a/BackEnd/Timeline/Controllers/TimelineController.cs +++ b/BackEnd/Timeline/Controllers/TimelineController.cs @@ -29,17 +29,19 @@ namespace Timeline.Controllers private readonly IUserService _userService;
private readonly ITimelineService _service;
+ private readonly ITimelinePostService _postService;
private readonly IMapper _mapper;
/// <summary>
///
/// </summary>
- public TimelineController(ILogger<TimelineController> logger, IUserService userService, ITimelineService service, IMapper mapper)
+ public TimelineController(ILogger<TimelineController> logger, IUserService userService, ITimelineService service, ITimelinePostService timelinePostService, IMapper mapper)
{
_logger = logger;
_userService = userService;
_service = service;
+ _postService = timelinePostService;
_mapper = mapper;
}
@@ -187,7 +189,7 @@ namespace Timeline.Controllers return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid());
}
- List<TimelinePost> posts = await _service.GetPosts(name, modifiedSince, includeDeleted ?? false);
+ List<TimelinePost> posts = await _postService.GetPosts(name, modifiedSince, includeDeleted ?? false);
var result = _mapper.Map<List<TimelinePostInfo>>(posts);
return result;
@@ -217,9 +219,9 @@ namespace Timeline.Controllers try
{
- return await DataCacheHelper.GenerateActionResult(this, () => _service.GetPostDataETag(name, id), async () =>
+ return await DataCacheHelper.GenerateActionResult(this, () => _postService.GetPostDataETag(name, id), async () =>
{
- var data = await _service.GetPostData(name, id);
+ var data = await _postService.GetPostData(name, id);
return data;
});
}
@@ -264,7 +266,7 @@ namespace Timeline.Controllers {
return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_TextContentTextRequired));
}
- post = await _service.CreateTextPost(name, id, text, body.Time);
+ post = await _postService.CreateTextPost(name, id, text, body.Time);
}
else if (content.Type == TimelinePostContentTypes.Image)
{
@@ -285,7 +287,7 @@ namespace Timeline.Controllers try
{
- post = await _service.CreateImagePost(name, id, data, body.Time);
+ post = await _postService.CreateImagePost(name, id, data, body.Time);
}
catch (ImageException)
{
@@ -315,13 +317,13 @@ namespace Timeline.Controllers [ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<ActionResult<CommonDeleteResponse>> PostDelete([FromRoute][GeneralTimelineName] string name, [FromRoute] long id)
{
- if (!UserHasAllTimelineManagementPermission && !await _service.HasPostModifyPermission(name, id, this.GetUserId()))
+ if (!UserHasAllTimelineManagementPermission && !await _postService.HasPostModifyPermission(name, id, this.GetUserId()))
{
return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid());
}
try
{
- await _service.DeletePost(name, id);
+ await _postService.DeletePost(name, id);
return CommonDeleteResponse.Delete();
}
catch (TimelinePostNotExistException)
diff --git a/BackEnd/Timeline/Services/BasicTimelineService.cs b/BackEnd/Timeline/Services/BasicTimelineService.cs new file mode 100644 index 00000000..0d9f64a9 --- /dev/null +++ b/BackEnd/Timeline/Services/BasicTimelineService.cs @@ -0,0 +1,122 @@ +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.Validation;
+using Timeline.Services.Exceptions;
+
+namespace Timeline.Services
+{
+ /// <summary>
+ /// This service provide some basic timeline functions, which should be used internally for other services.
+ /// </summary>
+ public interface IBasicTimelineService
+ {
+ /// <summary>
+ /// Get the timeline id by name.
+ /// </summary>
+ /// <param name="timelineName">Timeline name.</param>
+ /// <returns>Id of the timeline.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> is null.</exception>
+ /// <exception cref="ArgumentException">Throw when <paramref name="timelineName"/> is of bad format.</exception>
+ /// <exception cref="TimelineNotExistException">
+ /// Thrown when timeline with name <paramref name="timelineName"/> does not exist.
+ /// If it is a personal timeline, then inner exception is <see cref="UserNotExistException"/>.
+ /// </exception>
+ /// <remarks>
+ /// If name is of personal timeline and the timeline does not exist, it will be created if user exists.
+ /// If the user does not exist, <see cref="TimelineNotExistException"/> will be thrown with <see cref="UserNotExistException"/> as inner exception.
+ ///</remarks>
+ Task<long> GetTimelineIdByName(string timelineName);
+ }
+
+
+ public class BasicTimelineService : IBasicTimelineService
+ {
+ private readonly DatabaseContext _database;
+
+ private readonly IBasicUserService _basicUserService;
+ private readonly IClock _clock;
+
+ private readonly GeneralTimelineNameValidator _generalTimelineNameValidator = new GeneralTimelineNameValidator();
+
+ public BasicTimelineService(DatabaseContext database, IBasicUserService basicUserService, IClock clock)
+ {
+ _database = database;
+ _basicUserService = basicUserService;
+ _clock = clock;
+ }
+
+ protected TimelineEntity CreateNewTimelineEntity(string? name, long ownerId)
+ {
+ var currentTime = _clock.GetCurrentTime();
+
+ return new TimelineEntity
+ {
+ Name = name,
+ NameLastModified = currentTime,
+ OwnerId = ownerId,
+ Visibility = TimelineVisibility.Register,
+ CreateTime = currentTime,
+ LastModified = currentTime,
+ CurrentPostLocalId = 0,
+ Members = new List<TimelineMemberEntity>()
+ };
+ }
+
+ public async Task<long> GetTimelineIdByName(string timelineName)
+ {
+ if (timelineName == null)
+ throw new ArgumentNullException(nameof(timelineName));
+
+ if (!_generalTimelineNameValidator.Validate(timelineName, out var message))
+ throw new ArgumentException(message);
+
+ timelineName = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal);
+
+ if (isPersonal)
+ {
+ long userId;
+ try
+ {
+ userId = await _basicUserService.GetUserIdByUsername(timelineName);
+ }
+ catch (UserNotExistException e)
+ {
+ throw new TimelineNotExistException(timelineName, 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 = CreateNewTimelineEntity(null, userId);
+ _database.Timelines.Add(newTimelineEntity);
+ await _database.SaveChangesAsync();
+
+ return newTimelineEntity.Id;
+ }
+ }
+ else
+ {
+ 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;
+ }
+ }
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Services/BasicUserService.cs b/BackEnd/Timeline/Services/BasicUserService.cs new file mode 100644 index 00000000..fbbb6677 --- /dev/null +++ b/BackEnd/Timeline/Services/BasicUserService.cs @@ -0,0 +1,66 @@ +using Microsoft.EntityFrameworkCore;
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Timeline.Entities;
+using Timeline.Models.Validation;
+using Timeline.Services.Exceptions;
+
+namespace Timeline.Services
+{
+ /// <summary>
+ /// This service provide some basic user features, which should be used internally for other services.
+ /// </summary>
+ public interface IBasicUserService
+ {
+ /// <summary>
+ /// Check if a user exists.
+ /// </summary>
+ /// <param name="id">The id of the user.</param>
+ /// <returns>True if exists. Otherwise false.</returns>
+ Task<bool> CheckUserExistence(long id);
+
+ /// <summary>
+ /// Get the user id of given username.
+ /// </summary>
+ /// <param name="username">Username of the user.</param>
+ /// <returns>The id of the user.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="username"/> is null.</exception>
+ /// <exception cref="ArgumentException">Thrown when <paramref name="username"/> is of bad format.</exception>
+ /// <exception cref="UserNotExistException">Thrown when the user with given username does not exist.</exception>
+ Task<long> GetUserIdByUsername(string username);
+ }
+
+ public class BasicUserService : IBasicUserService
+ {
+ private readonly DatabaseContext _database;
+
+ private readonly UsernameValidator _usernameValidator = new UsernameValidator();
+
+ public BasicUserService(DatabaseContext database)
+ {
+ _database = database;
+ }
+
+ public async Task<bool> CheckUserExistence(long id)
+ {
+ return await _database.Users.AnyAsync(u => u.Id == id);
+ }
+
+ public async Task<long> GetUserIdByUsername(string username)
+ {
+ if (username == null)
+ throw new ArgumentNullException(nameof(username));
+
+ if (!_usernameValidator.Validate(username, out var message))
+ throw new ArgumentException(message);
+
+ var entity = await _database.Users.Where(user => user.Username == username).Select(u => new { u.Id }).SingleOrDefaultAsync();
+
+ if (entity == null)
+ throw new UserNotExistException(username);
+
+ return entity.Id;
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Services/HighlightTimelineService.cs b/BackEnd/Timeline/Services/HighlightTimelineService.cs index 7528d9b0..88ad4a4b 100644 --- a/BackEnd/Timeline/Services/HighlightTimelineService.cs +++ b/BackEnd/Timeline/Services/HighlightTimelineService.cs @@ -43,10 +43,10 @@ namespace Timeline.Services public class HighlightTimelineService : IHighlightTimelineService
{
private readonly DatabaseContext _database;
- private readonly IUserService _userService;
+ private readonly IBasicUserService _userService;
private readonly ITimelineService _timelineService;
- public HighlightTimelineService(DatabaseContext database, IUserService userService, ITimelineService timelineService)
+ public HighlightTimelineService(DatabaseContext database, IBasicUserService userService, ITimelineService timelineService)
{
_database = database;
_userService = userService;
diff --git a/BackEnd/Timeline/Services/TimelinePostService.cs b/BackEnd/Timeline/Services/TimelinePostService.cs new file mode 100644 index 00000000..36fcdbca --- /dev/null +++ b/BackEnd/Timeline/Services/TimelinePostService.cs @@ -0,0 +1,493 @@ +using Microsoft.EntityFrameworkCore;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Threading.Tasks;
+using Timeline.Entities;
+using Timeline.Helpers;
+using Timeline.Models;
+using Timeline.Services.Exceptions;
+using SixLabors.ImageSharp;
+using static Timeline.Resources.Services.TimelineService;
+using Microsoft.Extensions.Logging;
+
+namespace Timeline.Services
+{
+ public class PostData : ICacheableData
+ {
+#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; } // TODO: Why nullable?
+ }
+
+ public interface ITimelinePostService
+ {
+ /// <summary>
+ /// Get all the posts in the timeline.
+ /// </summary>
+ /// <param name="timelineName">The name of the timeline.</param>
+ /// <param name="modifiedSince">The time that posts have been modified since.</param>
+ /// <param name="includeDeleted">Whether include deleted posts.</param>
+ /// <returns>A list of all posts.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> is null.</exception>
+ /// <exception cref="ArgumentException">Throw when <paramref name="timelineName"/> is of bad format.</exception>
+ /// <exception cref="TimelineNotExistException">
+ /// Thrown when timeline with name <paramref name="timelineName"/> does not exist.
+ /// If it is a personal timeline, then inner exception is <see cref="UserNotExistException"/>.
+ /// </exception>
+ Task<List<TimelinePost>> GetPosts(string timelineName, DateTime? modifiedSince = null, bool includeDeleted = false);
+
+ /// <summary>
+ /// Get the etag of data of a post.
+ /// </summary>
+ /// <param name="timelineName">The name of the timeline of the post.</param>
+ /// <param name="postId">The id of the post.</param>
+ /// <returns>The etag of the data.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> is null.</exception>
+ /// <exception cref="ArgumentException">Throw when <paramref name="timelineName"/> is of bad format.</exception>
+ /// <exception cref="TimelineNotExistException">
+ /// Thrown when timeline with name <paramref name="timelineName"/> does not exist.
+ /// If it is a personal timeline, then inner exception is <see cref="UserNotExistException"/>.
+ /// </exception>
+ /// <exception cref="TimelinePostNotExistException">Thrown when post of <paramref name="postId"/> does not exist or has been deleted.</exception>
+ /// <exception cref="TimelinePostNoDataException">Thrown when post has no data.</exception>
+ /// <seealso cref="GetPostData(string, long)"/>
+ Task<string> GetPostDataETag(string timelineName, long postId);
+
+ /// <summary>
+ /// Get the data of a post.
+ /// </summary>
+ /// <param name="timelineName">The name of the timeline of the post.</param>
+ /// <param name="postId">The id of the post.</param>
+ /// <returns>The etag of the data.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> is null.</exception>
+ /// <exception cref="ArgumentException">Throw when <paramref name="timelineName"/> is of bad format.</exception>
+ /// <exception cref="TimelineNotExistException">
+ /// Thrown when timeline with name <paramref name="timelineName"/> does not exist.
+ /// If it is a personal timeline, then inner exception is <see cref="UserNotExistException"/>.
+ /// </exception>
+ /// <exception cref="TimelinePostNotExistException">Thrown when post of <paramref name="postId"/> does not exist or has been deleted.</exception>
+ /// <exception cref="TimelinePostNoDataException">Thrown when post has no data.</exception>
+ /// <seealso cref="GetPostDataETag(string, long)"/>
+ Task<PostData> GetPostData(string timelineName, long postId);
+
+ /// <summary>
+ /// Create a new text post in timeline.
+ /// </summary>
+ /// <param name="timelineName">The name of the timeline to create post against.</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 current time is used.</param>
+ /// <returns>The info of the created post.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> or <paramref name="text"/> is null.</exception>
+ /// <exception cref="ArgumentException">Throw when <paramref name="timelineName"/> is of bad format.</exception>
+ /// <exception cref="TimelineNotExistException">
+ /// Thrown when timeline with name <paramref name="timelineName"/> does not exist.
+ /// If it is a personal timeline, then inner exception is <see cref="UserNotExistException"/>.
+ /// </exception>
+ /// <exception cref="UserNotExistException">Thrown if user of <paramref name="authorId"/> does not exist.</exception>
+ Task<TimelinePost> CreateTextPost(string timelineName, long authorId, string text, DateTime? time);
+
+ /// <summary>
+ /// Create a new image post in timeline.
+ /// </summary>
+ /// <param name="timelineName">The name of the timeline to create post against.</param>
+ /// <param name="authorId">The author's user id.</param>
+ /// <param name="imageData">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="timelineName"/> or <paramref name="imageData"/> is null.</exception>
+ /// <exception cref="ArgumentException">Throw when <paramref name="timelineName"/> is of bad format.</exception>
+ /// <exception cref="TimelineNotExistException">
+ /// Thrown when timeline with name <paramref name="timelineName"/> does not exist.
+ /// If it is a personal timeline, then inner exception is <see cref="UserNotExistException"/>.
+ /// </exception>
+ /// <exception cref="UserNotExistException">Thrown if user of <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 timelineName, long authorId, byte[] imageData, DateTime? time);
+
+ /// <summary>
+ /// Delete a post.
+ /// </summary>
+ /// <param name="timelineName">The name of the timeline to delete post against.</param>
+ /// <param name="postId">The id of the post to delete.</param>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> is null.</exception>
+ /// <exception cref="ArgumentException">Throw when <paramref name="timelineName"/> is of bad format.</exception>
+ /// <exception cref="TimelineNotExistException">
+ /// Thrown when timeline with name <paramref name="timelineName"/> does not exist.
+ /// If it is a personal timeline, then inner exception is <see cref="UserNotExistException"/>.
+ /// </exception>
+ /// <exception cref="TimelinePostNotExistException">Thrown when the post with given id does not exist or is deleted already.</exception>
+ /// <remarks>
+ /// First use <see cref="HasPostModifyPermission(string, long, long, bool)"/> to check the permission.
+ /// </remarks>
+ Task DeletePost(string timelineName, long postId);
+
+ /// <summary>
+ /// Delete all posts of the given user. Used when delete a user.
+ /// </summary>
+ /// <param name="userId">The id of the user.</param>
+ Task DeleteAllPostsOfUser(long userId);
+
+ /// <summary>
+ /// Verify whether a user has the permission to modify a post.
+ /// </summary>
+ /// <param name="timelineName">The name of the timeline.</param>
+ /// <param name="postId">The id of the post.</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="timelineName"/> is null.</exception>
+ /// <exception cref="ArgumentException">Throw when <paramref name="timelineName"/> is of bad format.</exception>
+ /// <exception cref="TimelineNotExistException">
+ /// Thrown when timeline with name <paramref name="timelineName"/> does not exist.
+ /// If it is a personal timeline, then inner exception is <see cref="UserNotExistException"/>.
+ /// </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>
+ /// Unless <paramref name="throwOnPostNotExist"/> 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 <paramref name="throwOnPostNotExist"/> 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.
+ /// </remarks>
+ Task<bool> HasPostModifyPermission(string timelineName, long postId, long modifierId, bool throwOnPostNotExist = false);
+ }
+
+ public class TimelinePostService : ITimelinePostService
+ {
+ private readonly ILogger<TimelinePostService> _logger;
+ private readonly DatabaseContext _database;
+ private readonly IBasicTimelineService _basicTimelineService;
+ private readonly IUserService _userService;
+ private readonly IDataManager _dataManager;
+ private readonly IImageValidator _imageValidator;
+ private readonly IClock _clock;
+
+ public TimelinePostService(ILogger<TimelinePostService> logger, DatabaseContext database, IBasicTimelineService basicTimelineService, IUserService userService, IDataManager dataManager, IImageValidator imageValidator, IClock clock)
+ {
+ _logger = logger;
+ _database = database;
+ _basicTimelineService = basicTimelineService;
+ _userService = userService;
+ _dataManager = dataManager;
+ _imageValidator = imageValidator;
+ _clock = clock;
+ }
+
+ private async Task<TimelinePost> MapTimelinePostFromEntity(TimelinePostEntity entity, string timelineName)
+ {
+ User? author = entity.AuthorId.HasValue ? await _userService.GetUser(entity.AuthorId.Value) : null;
+
+ ITimelinePostContent? content = null;
+
+ if (entity.Content != null)
+ {
+ var type = entity.ContentType;
+
+ content = type switch
+ {
+ TimelinePostContentTypes.Text => new TextTimelinePostContent(entity.Content),
+ TimelinePostContentTypes.Image => new ImageTimelinePostContent(entity.Content),
+ _ => throw new DatabaseCorruptedException(string.Format(CultureInfo.InvariantCulture, ExceptionDatabaseUnknownContentType, type))
+ };
+ }
+
+ return new TimelinePost(
+ id: entity.LocalId,
+ author: author,
+ content: content,
+ time: entity.Time,
+ lastUpdated: entity.LastUpdated,
+ timelineName: timelineName
+ );
+ }
+
+ public async Task<List<TimelinePost>> GetPosts(string timelineName, DateTime? modifiedSince = null, bool includeDeleted = false)
+ {
+ modifiedSince = modifiedSince?.MyToUtc();
+
+ if (timelineName == null)
+ throw new ArgumentNullException(nameof(timelineName));
+
+ var timelineId = await _basicTimelineService.GetTimelineIdByName(timelineName);
+ IQueryable<TimelinePostEntity> query = _database.TimelinePosts.Where(p => p.TimelineId == timelineId);
+
+ if (!includeDeleted)
+ {
+ query = query.Where(p => p.Content != null);
+ }
+
+ if (modifiedSince.HasValue)
+ {
+ query = query.Include(p => p.Author).Where(p => p.LastUpdated >= modifiedSince || (p.Author != null && p.Author.UsernameChangeTime >= modifiedSince));
+ }
+
+ query = query.OrderBy(p => p.Time);
+
+ var postEntities = await query.ToListAsync();
+
+ var posts = new List<TimelinePost>();
+ foreach (var entity in postEntities)
+ {
+ posts.Add(await MapTimelinePostFromEntity(entity, timelineName));
+ }
+ return posts;
+ }
+
+ public async Task<string> GetPostDataETag(string timelineName, long postId)
+ {
+ if (timelineName == null)
+ throw new ArgumentNullException(nameof(timelineName));
+
+ var timelineId = await _basicTimelineService.GetTimelineIdByName(timelineName);
+
+ var postEntity = await _database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == postId).SingleOrDefaultAsync();
+
+ if (postEntity == null)
+ throw new TimelinePostNotExistException(timelineName, postId, false);
+
+ if (postEntity.Content == null)
+ throw new TimelinePostNotExistException(timelineName, postId, true);
+
+ if (postEntity.ContentType != TimelinePostContentTypes.Image)
+ throw new TimelinePostNoDataException(ExceptionGetDataNonImagePost);
+
+ var tag = postEntity.Content;
+
+ return tag;
+ }
+
+ public async Task<PostData> GetPostData(string timelineName, long postId)
+ {
+ if (timelineName == null)
+ throw new ArgumentNullException(nameof(timelineName));
+
+ var timelineId = await _basicTimelineService.GetTimelineIdByName(timelineName);
+ var postEntity = await _database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == postId).SingleOrDefaultAsync();
+
+ if (postEntity == null)
+ throw new TimelinePostNotExistException(timelineName, postId, false);
+
+ if (postEntity.Content == null)
+ throw new TimelinePostNotExistException(timelineName, postId, true);
+
+ if (postEntity.ContentType != TimelinePostContentTypes.Image)
+ throw new TimelinePostNoDataException(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 timelineName, long authorId, string text, DateTime? time)
+ {
+ time = time?.MyToUtc();
+
+ if (timelineName == null)
+ throw new ArgumentNullException(nameof(timelineName));
+ if (text == null)
+ throw new ArgumentNullException(nameof(text));
+
+ var timelineId = await _basicTimelineService.GetTimelineIdByName(timelineName);
+ var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync();
+
+ var author = await _userService.GetUser(authorId);
+
+ var currentTime = _clock.GetCurrentTime();
+ var finalTime = time ?? currentTime;
+
+ timelineEntity.CurrentPostLocalId += 1;
+
+ var postEntity = new TimelinePostEntity
+ {
+ LocalId = timelineEntity.CurrentPostLocalId,
+ ContentType = TimelinePostContentTypes.Text,
+ Content = text,
+ AuthorId = authorId,
+ TimelineId = timelineId,
+ Time = finalTime,
+ LastUpdated = currentTime
+ };
+ _database.TimelinePosts.Add(postEntity);
+ await _database.SaveChangesAsync();
+
+
+ return new TimelinePost(
+ id: postEntity.LocalId,
+ content: new TextTimelinePostContent(text),
+ time: finalTime,
+ author: author,
+ lastUpdated: currentTime,
+ timelineName: timelineName
+ );
+ }
+
+ public async Task<TimelinePost> CreateImagePost(string timelineName, long authorId, byte[] data, DateTime? time)
+ {
+ time = time?.MyToUtc();
+
+ if (timelineName == null)
+ throw new ArgumentNullException(nameof(timelineName));
+ if (data == null)
+ throw new ArgumentNullException(nameof(data));
+
+ var timelineId = await _basicTimelineService.GetTimelineIdByName(timelineName);
+ var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync();
+
+ var author = await _userService.GetUser(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
+ {
+ 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: timelineName
+ );
+ }
+
+ public async Task DeletePost(string timelineName, long id)
+ {
+ if (timelineName == null)
+ throw new ArgumentNullException(nameof(timelineName));
+
+ var timelineId = await _basicTimelineService.GetTimelineIdByName(timelineName);
+
+ var post = await _database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == id).SingleOrDefaultAsync();
+
+ if (post == null)
+ throw new TimelinePostNotExistException(timelineName, id, false);
+
+ if (post.Content == null)
+ throw new TimelinePostNotExistException(timelineName, id, true);
+
+ 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 DeleteAllPostsOfUser(long userId)
+ {
+ var posts = await _database.TimelinePosts.Where(p => p.AuthorId == userId).ToListAsync();
+
+ var now = _clock.GetCurrentTime();
+
+ var dataTags = new List<string>();
+
+ foreach (var post in posts)
+ {
+ if (post.Content != null)
+ {
+ if (post.ContentType == TimelinePostContentTypes.Image)
+ {
+ dataTags.Add(post.Content);
+ }
+ post.Content = null;
+ }
+ post.LastUpdated = now;
+ }
+
+ await _database.SaveChangesAsync();
+
+ foreach (var dataTag in dataTags)
+ {
+ await _dataManager.FreeEntry(dataTag);
+ }
+ }
+
+ public async Task<bool> HasPostModifyPermission(string timelineName, long postId, long modifierId, bool throwOnPostNotExist = false)
+ {
+ if (timelineName == null)
+ throw new ArgumentNullException(nameof(timelineName));
+
+ var timelineId = await _basicTimelineService.GetTimelineIdByName(timelineName);
+
+ 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 == postId).Select(p => new { p.Content, p.AuthorId }).SingleOrDefaultAsync();
+
+ if (postEntity == null)
+ {
+ if (throwOnPostNotExist)
+ throw new TimelinePostNotExistException(timelineName, postId, false);
+ else
+ return true;
+ }
+
+ if (postEntity.Content == null && throwOnPostNotExist)
+ {
+ throw new TimelinePostNotExistException(timelineName, postId, true);
+ }
+
+ return timelineEntity.OwnerId == modifierId || postEntity.AuthorId == modifierId;
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Services/TimelineService.cs b/BackEnd/Timeline/Services/TimelineService.cs index f8c729bf..f943f8b4 100644 --- a/BackEnd/Timeline/Services/TimelineService.cs +++ b/BackEnd/Timeline/Services/TimelineService.cs @@ -51,20 +51,10 @@ namespace Timeline.Services public long UserId { get; set; }
}
- public class PostData : ICacheableData
- {
-#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; } // TODO: Why nullable?
- }
-
/// <summary>
/// This define the interface of both personal timeline and ordinary timeline.
/// </summary>
- public interface ITimelineService
+ public interface ITimelineService : IBasicTimelineService
{
/// <summary>
/// Get the timeline last modified time (not include name change).
@@ -80,19 +70,6 @@ namespace Timeline.Services Task<DateTime> GetTimelineLastModifiedTime(string timelineName);
/// <summary>
- /// Get the timeline id by name.
- /// </summary>
- /// <param name="timelineName">Timeline name.</param>
- /// <returns>Id of the timeline.</returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> is null.</exception>
- /// <exception cref="ArgumentException">Throw when <paramref name="timelineName"/> is of bad format.</exception>
- /// <exception cref="TimelineNotExistException">
- /// Thrown when timeline with name <paramref name="timelineName"/> does not exist.
- /// If it is a personal timeline, then inner exception is <see cref="UserNotExistException"/>.
- /// </exception>
- Task<long> GetTimelineIdByName(string timelineName);
-
- /// <summary>
/// Get the timeline unique id.
/// </summary>
/// <param name="timelineName">The name of the timeline.</param>
@@ -139,112 +116,7 @@ namespace Timeline.Services /// </exception>
Task ChangeProperty(string timelineName, TimelineChangePropertyRequest newProperties);
- /// <summary>
- /// Get all the posts in the timeline.
- /// </summary>
- /// <param name="timelineName">The name of the timeline.</param>
- /// <param name="modifiedSince">The time that posts have been modified since.</param>
- /// <param name="includeDeleted">Whether include deleted posts.</param>
- /// <returns>A list of all posts.</returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> is null.</exception>
- /// <exception cref="ArgumentException">Throw when <paramref name="timelineName"/> is of bad format.</exception>
- /// <exception cref="TimelineNotExistException">
- /// Thrown when timeline with name <paramref name="timelineName"/> does not exist.
- /// If it is a personal timeline, then inner exception is <see cref="UserNotExistException"/>.
- /// </exception>
- Task<List<TimelinePost>> GetPosts(string timelineName, DateTime? modifiedSince = null, bool includeDeleted = false);
- /// <summary>
- /// Get the etag of data of a post.
- /// </summary>
- /// <param name="timelineName">The name of the timeline of the post.</param>
- /// <param name="postId">The id of the post.</param>
- /// <returns>The etag of the data.</returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> is null.</exception>
- /// <exception cref="ArgumentException">Throw when <paramref name="timelineName"/> is of bad format.</exception>
- /// <exception cref="TimelineNotExistException">
- /// Thrown when timeline with name <paramref name="timelineName"/> does not exist.
- /// If it is a personal timeline, then inner exception is <see cref="UserNotExistException"/>.
- /// </exception>
- /// <exception cref="TimelinePostNotExistException">Thrown when post of <paramref name="postId"/> does not exist or has been deleted.</exception>
- /// <exception cref="TimelinePostNoDataException">Thrown when post has no data.</exception>
- /// <seealso cref="GetPostData(string, long)"/>
- Task<string> GetPostDataETag(string timelineName, long postId);
-
- /// <summary>
- /// Get the data of a post.
- /// </summary>
- /// <param name="timelineName">The name of the timeline of the post.</param>
- /// <param name="postId">The id of the post.</param>
- /// <returns>The etag of the data.</returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> is null.</exception>
- /// <exception cref="ArgumentException">Throw when <paramref name="timelineName"/> is of bad format.</exception>
- /// <exception cref="TimelineNotExistException">
- /// Thrown when timeline with name <paramref name="timelineName"/> does not exist.
- /// If it is a personal timeline, then inner exception is <see cref="UserNotExistException"/>.
- /// </exception>
- /// <exception cref="TimelinePostNotExistException">Thrown when post of <paramref name="postId"/> does not exist or has been deleted.</exception>
- /// <exception cref="TimelinePostNoDataException">Thrown when post has no data.</exception>
- /// <seealso cref="GetPostDataETag(string, long)"/>
- Task<PostData> GetPostData(string timelineName, long postId);
-
- /// <summary>
- /// Create a new text post in timeline.
- /// </summary>
- /// <param name="timelineName">The name of the timeline to create post against.</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 current time is used.</param>
- /// <returns>The info of the created post.</returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> or <paramref name="text"/> is null.</exception>
- /// <exception cref="ArgumentException">Throw when <paramref name="timelineName"/> is of bad format.</exception>
- /// <exception cref="TimelineNotExistException">
- /// Thrown when timeline with name <paramref name="timelineName"/> does not exist.
- /// If it is a personal timeline, then inner exception is <see cref="UserNotExistException"/>.
- /// </exception>
- /// <exception cref="UserNotExistException">Thrown if user of <paramref name="authorId"/> does not exist.</exception>
- Task<TimelinePost> CreateTextPost(string timelineName, long authorId, string text, DateTime? time);
-
- /// <summary>
- /// Create a new image post in timeline.
- /// </summary>
- /// <param name="timelineName">The name of the timeline to create post against.</param>
- /// <param name="authorId">The author's user id.</param>
- /// <param name="imageData">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="timelineName"/> or <paramref name="imageData"/> is null.</exception>
- /// <exception cref="ArgumentException">Throw when <paramref name="timelineName"/> is of bad format.</exception>
- /// <exception cref="TimelineNotExistException">
- /// Thrown when timeline with name <paramref name="timelineName"/> does not exist.
- /// If it is a personal timeline, then inner exception is <see cref="UserNotExistException"/>.
- /// </exception>
- /// <exception cref="UserNotExistException">Thrown if user of <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 timelineName, long authorId, byte[] imageData, DateTime? time);
-
- /// <summary>
- /// Delete a post.
- /// </summary>
- /// <param name="timelineName">The name of the timeline to delete post against.</param>
- /// <param name="postId">The id of the post to delete.</param>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> is null.</exception>
- /// <exception cref="ArgumentException">Throw when <paramref name="timelineName"/> is of bad format.</exception>
- /// <exception cref="TimelineNotExistException">
- /// Thrown when timeline with name <paramref name="timelineName"/> does not exist.
- /// If it is a personal timeline, then inner exception is <see cref="UserNotExistException"/>.
- /// </exception>
- /// <exception cref="TimelinePostNotExistException">Thrown when the post with given id does not exist or is deleted already.</exception>
- /// <remarks>
- /// First use <see cref="HasPostModifyPermission(string, long, long, bool)"/> to check the permission.
- /// </remarks>
- Task DeletePost(string timelineName, long postId);
-
- /// <summary>
- /// Delete all posts of the given user. Used when delete a user.
- /// </summary>
- /// <param name="userId">The id of the user.</param>
- Task DeleteAllPostsOfUser(long userId);
/// <summary>
/// Change member of timeline.
@@ -305,29 +177,6 @@ namespace Timeline.Services /// </remarks>
Task<bool> HasReadPermission(string timelineName, long? visitorId);
- /// <summary>
- /// Verify whether a user has the permission to modify a post.
- /// </summary>
- /// <param name="timelineName">The name of the timeline.</param>
- /// <param name="postId">The id of the post.</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="timelineName"/> is null.</exception>
- /// <exception cref="ArgumentException">Throw when <paramref name="timelineName"/> is of bad format.</exception>
- /// <exception cref="TimelineNotExistException">
- /// Thrown when timeline with name <paramref name="timelineName"/> does not exist.
- /// If it is a personal timeline, then inner exception is <see cref="UserNotExistException"/>.
- /// </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>
- /// Unless <paramref name="throwOnPostNotExist"/> 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 <paramref name="throwOnPostNotExist"/> 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.
- /// </remarks>
- Task<bool> HasPostModifyPermission(string timelineName, long postId, long modifierId, bool throwOnPostNotExist = false);
/// <summary>
/// Verify whether a user is member of a timeline.
@@ -395,28 +244,20 @@ namespace Timeline.Services Task<Models.Timeline> ChangeTimelineName(string oldTimelineName, string newTimelineName);
}
- public class TimelineService : ITimelineService
+ public class TimelineService : BasicTimelineService, ITimelineService
{
- public TimelineService(ILogger<TimelineService> logger, DatabaseContext database, IDataManager dataManager, IUserService userService, IImageValidator imageValidator, IClock clock)
+ public TimelineService(DatabaseContext database, IUserService userService, IClock clock)
+ : base(database, userService, clock)
{
- _logger = logger;
_database = database;
- _dataManager = dataManager;
_userService = userService;
- _imageValidator = imageValidator;
_clock = clock;
}
- private readonly ILogger<TimelineService> _logger;
-
private readonly DatabaseContext _database;
- private readonly IDataManager _dataManager;
-
private readonly IUserService _userService;
- private readonly IImageValidator _imageValidator;
-
private readonly IClock _clock;
private readonly UsernameValidator _usernameValidator = new UsernameValidator();
@@ -459,122 +300,12 @@ namespace Timeline.Services };
}
- private async Task<TimelinePost> MapTimelinePostFromEntity(TimelinePostEntity entity, string timelineName)
- {
- User? author = entity.AuthorId.HasValue ? await _userService.GetUser(entity.AuthorId.Value) : null;
-
- ITimelinePostContent? content = null;
-
- if (entity.Content != null)
- {
- var type = entity.ContentType;
-
- content = type switch
- {
- TimelinePostContentTypes.Text => new TextTimelinePostContent(entity.Content),
- TimelinePostContentTypes.Image => new ImageTimelinePostContent(entity.Content),
- _ => throw new DatabaseCorruptedException(string.Format(CultureInfo.InvariantCulture, ExceptionDatabaseUnknownContentType, type))
- };
- }
-
- return new TimelinePost(
- id: entity.LocalId,
- author: author,
- content: content,
- time: entity.Time,
- lastUpdated: entity.LastUpdated,
- timelineName: timelineName
- );
- }
-
- private TimelineEntity CreateNewTimelineEntity(string? name, long ownerId)
- {
- var currentTime = _clock.GetCurrentTime();
-
- return new TimelineEntity
- {
- Name = name,
- NameLastModified = currentTime,
- OwnerId = ownerId,
- Visibility = TimelineVisibility.Register,
- CreateTime = currentTime,
- LastModified = currentTime,
- CurrentPostLocalId = 0,
- Members = new List<TimelineMemberEntity>()
- };
- }
-
-
-
- // 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<long> FindTimelineId(string timelineName)
- {
- timelineName = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal);
-
- 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);
- }
-
- 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 = CreateNewTimelineEntity(null, userId);
- _database.Timelines.Add(newTimelineEntity);
- await _database.SaveChangesAsync();
-
- return newTimelineEntity.Id;
- }
- }
- else
- {
- if (timelineName == null)
- throw new ArgumentNullException(nameof(timelineName));
-
- ValidateTimelineName(timelineName, nameof(timelineName));
-
- 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<DateTime> GetTimelineLastModifiedTime(string timelineName)
{
if (timelineName == null)
throw new ArgumentNullException(nameof(timelineName));
- var timelineId = await FindTimelineId(timelineName);
+ var timelineId = await GetTimelineIdByName(timelineName);
var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.LastModified }).SingleAsync();
@@ -586,31 +317,19 @@ namespace Timeline.Services if (timelineName == null)
throw new ArgumentNullException(nameof(timelineName));
- var timelineId = await FindTimelineId(timelineName);
+ var timelineId = await GetTimelineIdByName(timelineName);
var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.UniqueId }).SingleAsync();
return timelineEntity.UniqueId;
}
- public async Task<long> GetTimelineIdByName(string timelineName)
- {
- if (timelineName == null)
- throw new ArgumentNullException(nameof(timelineName));
-
- var timelineId = await FindTimelineId(timelineName);
-
- var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.Id }).SingleAsync();
-
- return timelineEntity.Id;
- }
-
public async Task<Models.Timeline> GetTimeline(string timelineName)
{
if (timelineName == null)
throw new ArgumentNullException(nameof(timelineName));
- var timelineId = await FindTimelineId(timelineName);
+ var timelineId = await GetTimelineIdByName(timelineName);
var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Include(t => t.Members).SingleAsync();
@@ -627,262 +346,6 @@ namespace Timeline.Services return await MapTimelineFromEntity(timelineEntity);
}
- public async Task<List<TimelinePost>> GetPosts(string timelineName, DateTime? modifiedSince = null, bool includeDeleted = false)
- {
- modifiedSince = modifiedSince?.MyToUtc();
-
- if (timelineName == null)
- throw new ArgumentNullException(nameof(timelineName));
-
- var timelineId = await FindTimelineId(timelineName);
- IQueryable<TimelinePostEntity> query = _database.TimelinePosts.Where(p => p.TimelineId == timelineId);
-
- if (!includeDeleted)
- {
- query = query.Where(p => p.Content != null);
- }
-
- if (modifiedSince.HasValue)
- {
- query = query.Include(p => p.Author).Where(p => p.LastUpdated >= modifiedSince || (p.Author != null && p.Author.UsernameChangeTime >= modifiedSince));
- }
-
- query = query.OrderBy(p => p.Time);
-
- var postEntities = await query.ToListAsync();
-
- var posts = new List<TimelinePost>();
- foreach (var entity in postEntities)
- {
- posts.Add(await MapTimelinePostFromEntity(entity, timelineName));
- }
- return posts;
- }
-
- public async Task<string> GetPostDataETag(string timelineName, long postId)
- {
- if (timelineName == null)
- throw new ArgumentNullException(nameof(timelineName));
-
- 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(timelineName, postId, false);
-
- if (postEntity.Content == null)
- throw new TimelinePostNotExistException(timelineName, postId, true);
-
- if (postEntity.ContentType != TimelinePostContentTypes.Image)
- throw new TimelinePostNoDataException(ExceptionGetDataNonImagePost);
-
- var tag = postEntity.Content;
-
- return tag;
- }
-
- public async Task<PostData> GetPostData(string timelineName, long postId)
- {
- if (timelineName == null)
- throw new ArgumentNullException(nameof(timelineName));
-
- 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(timelineName, postId, false);
-
- if (postEntity.Content == null)
- throw new TimelinePostNotExistException(timelineName, postId, true);
-
- if (postEntity.ContentType != TimelinePostContentTypes.Image)
- throw new TimelinePostNoDataException(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 timelineName, long authorId, string text, DateTime? time)
- {
- time = time?.MyToUtc();
-
- if (timelineName == null)
- throw new ArgumentNullException(nameof(timelineName));
- if (text == null)
- throw new ArgumentNullException(nameof(text));
-
- var timelineId = await FindTimelineId(timelineName);
- var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync();
-
- var author = await _userService.GetUser(authorId);
-
- var currentTime = _clock.GetCurrentTime();
- var finalTime = time ?? currentTime;
-
- timelineEntity.CurrentPostLocalId += 1;
-
- var postEntity = new TimelinePostEntity
- {
- LocalId = timelineEntity.CurrentPostLocalId,
- ContentType = TimelinePostContentTypes.Text,
- Content = text,
- AuthorId = authorId,
- TimelineId = timelineId,
- Time = finalTime,
- LastUpdated = currentTime
- };
- _database.TimelinePosts.Add(postEntity);
- await _database.SaveChangesAsync();
-
-
- return new TimelinePost(
- id: postEntity.LocalId,
- content: new TextTimelinePostContent(text),
- time: finalTime,
- author: author,
- lastUpdated: currentTime,
- timelineName: timelineName
- );
- }
-
- public async Task<TimelinePost> CreateImagePost(string timelineName, long authorId, byte[] data, DateTime? time)
- {
- time = time?.MyToUtc();
-
- if (timelineName == null)
- throw new ArgumentNullException(nameof(timelineName));
- if (data == null)
- throw new ArgumentNullException(nameof(data));
-
- var timelineId = await FindTimelineId(timelineName);
- var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync();
-
- var author = await _userService.GetUser(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
- {
- 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: timelineName
- );
- }
-
- public async Task DeletePost(string timelineName, long id)
- {
- if (timelineName == null)
- throw new ArgumentNullException(nameof(timelineName));
-
- var timelineId = await FindTimelineId(timelineName);
-
- var post = await _database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == id).SingleOrDefaultAsync();
-
- if (post == null)
- throw new TimelinePostNotExistException(timelineName, id, false);
-
- if (post.Content == null)
- throw new TimelinePostNotExistException(timelineName, id, true);
-
- 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 DeleteAllPostsOfUser(long userId)
- {
- var posts = await _database.TimelinePosts.Where(p => p.AuthorId == userId).ToListAsync();
-
- var now = _clock.GetCurrentTime();
-
- var dataTags = new List<string>();
-
- foreach (var post in posts)
- {
- if (post.Content != null)
- {
- if (post.ContentType == TimelinePostContentTypes.Image)
- {
- dataTags.Add(post.Content);
- }
- post.Content = null;
- }
- post.LastUpdated = now;
- }
-
- await _database.SaveChangesAsync();
-
- foreach (var dataTag in dataTags)
- {
- await _dataManager.FreeEntry(dataTag);
- }
- }
-
public async Task ChangeProperty(string timelineName, TimelineChangePropertyRequest newProperties)
{
if (timelineName == null)
@@ -890,7 +353,7 @@ namespace Timeline.Services if (newProperties == null)
throw new ArgumentNullException(nameof(newProperties));
- var timelineId = await FindTimelineId(timelineName);
+ var timelineId = await GetTimelineIdByName(timelineName);
var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync();
@@ -976,7 +439,7 @@ namespace Timeline.Services if (simplifiedAdd == null && simplifiedRemove == null)
return;
- var timelineId = await FindTimelineId(timelineName);
+ var timelineId = await GetTimelineIdByName(timelineName);
async Task<List<long>?> CheckExistenceAndGetId(List<string>? list)
{
@@ -1016,7 +479,7 @@ namespace Timeline.Services if (timelineName == null)
throw new ArgumentNullException(nameof(timelineName));
- var timelineId = await FindTimelineId(timelineName);
+ var timelineId = await GetTimelineIdByName(timelineName);
var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.OwnerId }).SingleAsync();
return userId == timelineEntity.OwnerId;
@@ -1027,7 +490,7 @@ namespace Timeline.Services if (timelineName == null)
throw new ArgumentNullException(nameof(timelineName));
- var timelineId = await FindTimelineId(timelineName);
+ var timelineId = await GetTimelineIdByName(timelineName);
var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.Visibility }).SingleAsync();
if (timelineEntity.Visibility == TimelineVisibility.Public)
@@ -1047,39 +510,12 @@ namespace Timeline.Services }
}
- public async Task<bool> HasPostModifyPermission(string timelineName, long postId, long modifierId, bool throwOnPostNotExist = false)
- {
- if (timelineName == null)
- throw new ArgumentNullException(nameof(timelineName));
-
- var timelineId = await FindTimelineId(timelineName);
-
- 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 == postId).Select(p => new { p.Content, p.AuthorId }).SingleOrDefaultAsync();
-
- if (postEntity == null)
- {
- if (throwOnPostNotExist)
- throw new TimelinePostNotExistException(timelineName, postId, false);
- else
- return true;
- }
-
- if (postEntity.Content == null && throwOnPostNotExist)
- {
- throw new TimelinePostNotExistException(timelineName, postId, true);
- }
-
- return timelineEntity.OwnerId == modifierId || postEntity.AuthorId == modifierId;
- }
-
public async Task<bool> IsMemberOf(string timelineName, long userId)
{
if (timelineName == null)
throw new ArgumentNullException(nameof(timelineName));
- var timelineId = await FindTimelineId(timelineName);
+ var timelineId = await GetTimelineIdByName(timelineName);
var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.OwnerId }).SingleAsync();
diff --git a/BackEnd/Timeline/Services/UserDeleteService.cs b/BackEnd/Timeline/Services/UserDeleteService.cs index 5365313b..a4e77abc 100644 --- a/BackEnd/Timeline/Services/UserDeleteService.cs +++ b/BackEnd/Timeline/Services/UserDeleteService.cs @@ -31,15 +31,15 @@ namespace Timeline.Services private readonly DatabaseContext _databaseContext;
- private readonly ITimelineService _timelineService;
+ private readonly ITimelinePostService _timelinePostService;
private readonly UsernameValidator _usernameValidator = new UsernameValidator();
- public UserDeleteService(ILogger<UserDeleteService> logger, DatabaseContext databaseContext, ITimelineService timelineService)
+ public UserDeleteService(ILogger<UserDeleteService> logger, DatabaseContext databaseContext, ITimelinePostService timelinePostService)
{
_logger = logger;
_databaseContext = databaseContext;
- _timelineService = timelineService;
+ _timelinePostService = timelinePostService;
}
public async Task<bool> DeleteUser(string username)
@@ -59,7 +59,7 @@ namespace Timeline.Services if (user.Id == 1)
throw new InvalidOperationOnRootUserException("Can't delete root user.");
- await _timelineService.DeleteAllPostsOfUser(user.Id);
+ await _timelinePostService.DeleteAllPostsOfUser(user.Id);
_databaseContext.Users.Remove(user);
diff --git a/BackEnd/Timeline/Services/UserService.cs b/BackEnd/Timeline/Services/UserService.cs index 76c24666..cded3ff1 100644 --- a/BackEnd/Timeline/Services/UserService.cs +++ b/BackEnd/Timeline/Services/UserService.cs @@ -24,7 +24,7 @@ namespace Timeline.Services public string? Nickname { get; set; }
}
- public interface IUserService
+ public interface IUserService : IBasicUserService
{
/// <summary>
/// Try to verify the given username and password.
@@ -38,12 +38,6 @@ namespace Timeline.Services /// <exception cref="BadPasswordException">Thrown when password is wrong.</exception>
Task<User> VerifyCredential(string username, string password);
- /// <summary>
- /// Check if a user exists.
- /// </summary>
- /// <param name="id">The id of the user.</param>
- /// <returns>True if exists. Otherwise false.</returns>
- Task<bool> CheckUserExistence(long id);
/// <summary>
/// Try to get a user by id.
@@ -53,15 +47,6 @@ namespace Timeline.Services /// <exception cref="UserNotExistException">Thrown when the user with given id does not exist.</exception>
Task<User> GetUser(long id);
- /// <summary>
- /// Get the user id of given username.
- /// </summary>
- /// <param name="username">Username of the user.</param>
- /// <returns>The id of the user.</returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="username"/> is null.</exception>
- /// <exception cref="ArgumentException">Thrown when <paramref name="username"/> is of bad format.</exception>
- /// <exception cref="UserNotExistException">Thrown when the user with given username does not exist.</exception>
- Task<long> GetUserIdByUsername(string username);
/// <summary>
/// List all users.
@@ -106,7 +91,7 @@ namespace Timeline.Services Task ChangePassword(long id, string oldPassword, string newPassword);
}
- public class UserService : IUserService
+ public class UserService : BasicUserService, IUserService
{
private readonly ILogger<UserService> _logger;
private readonly IClock _clock;
@@ -119,7 +104,7 @@ namespace Timeline.Services private readonly UsernameValidator _usernameValidator = new UsernameValidator();
private readonly NicknameValidator _nicknameValidator = new NicknameValidator();
- public UserService(ILogger<UserService> logger, DatabaseContext databaseContext, IPasswordService passwordService, IClock clock, IUserPermissionService userPermissionService)
+ public UserService(ILogger<UserService> logger, DatabaseContext databaseContext, IPasswordService passwordService, IClock clock, IUserPermissionService userPermissionService) : base(databaseContext)
{
_logger = logger;
_clock = clock;
@@ -195,11 +180,6 @@ namespace Timeline.Services return await CreateUserFromEntity(entity);
}
- public async Task<bool> CheckUserExistence(long id)
- {
- return await _databaseContext.Users.AnyAsync(u => u.Id == id);
- }
-
public async Task<User> GetUser(long id)
{
var user = await _databaseContext.Users.Where(u => u.Id == id).SingleOrDefaultAsync();
@@ -210,21 +190,6 @@ namespace Timeline.Services return await CreateUserFromEntity(user);
}
- public async Task<long> GetUserIdByUsername(string username)
- {
- if (username == null)
- throw new ArgumentNullException(nameof(username));
-
- CheckUsernameFormat(username, nameof(username));
-
- var entity = await _databaseContext.Users.Where(user => user.Username == username).Select(u => new { u.Id }).SingleOrDefaultAsync();
-
- if (entity == null)
- throw new UserNotExistException(username);
-
- return entity.Id;
- }
-
public async Task<List<User>> GetUsers()
{
List<User> result = new();
diff --git a/BackEnd/Timeline/Startup.cs b/BackEnd/Timeline/Startup.cs index 532c63d0..63e2a0dd 100644 --- a/BackEnd/Timeline/Startup.cs +++ b/BackEnd/Timeline/Startup.cs @@ -72,34 +72,34 @@ namespace Timeline services.AddAuthentication(AuthenticationConstants.Scheme)
.AddScheme<MyAuthenticationOptions, MyAuthenticationHandler>(AuthenticationConstants.Scheme, AuthenticationConstants.DisplayName, o => { });
services.AddAuthorization();
-
services.AddSingleton<IAuthorizationPolicyProvider, PermissionPolicyProvider>();
- services.AddSingleton<IPathProvider, PathProvider>();
+ services.TryAddSingleton<IActionContextAccessor, ActionContextAccessor>();
+ services.AddSingleton<IPathProvider, PathProvider>();
services.AddSingleton<IDatabaseBackupService, DatabaseBackupService>();
services.AddAutoMapper(GetType().Assembly);
services.AddTransient<IClock, Clock>();
+ services.AddScoped<IETagGenerator, ETagGenerator>();
+ services.AddScoped<IDataManager, DataManager>();
+ services.AddScoped<IImageValidator, ImageValidator>();
+
services.AddTransient<IPasswordService, PasswordService>();
+ services.AddScoped<IBasicUserService, BasicUserService>();
services.AddScoped<IUserService, UserService>();
services.AddScoped<IUserDeleteService, UserDeleteService>();
services.AddScoped<IUserTokenService, JwtUserTokenService>();
services.AddScoped<IUserTokenManager, UserTokenManager>();
services.AddScoped<IUserPermissionService, UserPermissionService>();
-
- services.AddScoped<IETagGenerator, ETagGenerator>();
- services.AddScoped<IDataManager, DataManager>();
-
- services.AddScoped<IImageValidator, ImageValidator>();
-
services.AddUserAvatarService();
+ services.AddScoped<IBasicTimelineService, BasicTimelineService>();
services.AddScoped<ITimelineService, TimelineService>();
+ services.AddScoped<ITimelinePostService, TimelinePostService>();
- services.TryAddSingleton<IActionContextAccessor, ActionContextAccessor>();
services.AddDbContext<DatabaseContext>((services, options) =>
{
|