aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorcrupest <crupest@outlook.com>2021-01-31 15:42:52 +0800
committercrupest <crupest@outlook.com>2021-01-31 15:42:52 +0800
commit7f88e120aba0f218641084aca4f467ffd27981d9 (patch)
tree54b320b4f236ae55895f32a64dbbc61d0022c835
parent607460b87376be19d9117cdd7be11883410d8691 (diff)
downloadtimeline-7f88e120aba0f218641084aca4f467ffd27981d9.tar.gz
timeline-7f88e120aba0f218641084aca4f467ffd27981d9.tar.bz2
timeline-7f88e120aba0f218641084aca4f467ffd27981d9.zip
...
-rw-r--r--BackEnd/Timeline.Tests/IntegratedTests/TimelinePostTest.cs28
-rw-r--r--BackEnd/Timeline.Tests/IntegratedTests/TimelineTest.cs99
-rw-r--r--BackEnd/Timeline/Controllers/TimelineController.cs278
-rw-r--r--BackEnd/Timeline/Controllers/TimelinePostController.cs213
-rw-r--r--BackEnd/Timeline/Models/Http/TimelineController.cs25
-rw-r--r--BackEnd/Timeline/Models/Mapper/TimelineMapper.cs4
-rw-r--r--BackEnd/Timeline/Properties/launchSettings.json14
-rw-r--r--BackEnd/Timeline/Services/TimelineService.cs99
8 files changed, 308 insertions, 452 deletions
diff --git a/BackEnd/Timeline.Tests/IntegratedTests/TimelinePostTest.cs b/BackEnd/Timeline.Tests/IntegratedTests/TimelinePostTest.cs
index 0060ac04..ae7afda1 100644
--- a/BackEnd/Timeline.Tests/IntegratedTests/TimelinePostTest.cs
+++ b/BackEnd/Timeline.Tests/IntegratedTests/TimelinePostTest.cs
@@ -124,6 +124,34 @@ namespace Timeline.Tests.IntegratedTests
[Theory]
[MemberData(nameof(TimelineNameGeneratorTestData))]
+ public async Task Post_ModifiedSince_And_IncludeDeleted(TimelineNameGenerator generator)
+ {
+ using var client = await CreateClientAsUser();
+
+ var postContentList = new List<string> { "a", "b", "c", "d" };
+ var posts = new List<HttpTimelinePost>();
+
+ foreach (var (content, index) in postContentList.Select((v, i) => (v, i)))
+ {
+ var post = await client.TestPostAsync<HttpTimelinePost>($"timelines/{generator(1)}/posts",
+ new HttpTimelinePostCreateRequest { Content = new HttpTimelinePostCreateRequestContent { Text = content, Type = TimelinePostContentTypes.Text } });
+ posts.Add(post);
+ await Task.Delay(1000);
+ }
+
+ await client.TestDeleteAsync($"timelines/{generator(1)}/posts/{posts[2].Id}");
+
+ {
+
+ posts = await client.TestGetAsync<List<HttpTimelinePost>>($"timelines/{generator(1)}/posts?modifiedSince={posts[1].LastUpdated.ToString("s", CultureInfo.InvariantCulture)}&includeDeleted=true");
+ posts.Should().HaveCount(3);
+ posts.Select(p => p.Deleted).Should().Equal(false, true, false);
+ posts.Select(p => p.Content == null).Should().Equal(false, true, false);
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(TimelineNameGeneratorTestData))]
public async Task PostList_IncludeDeleted(TimelineNameGenerator generator)
{
using var client = await CreateClientAsUser();
diff --git a/BackEnd/Timeline.Tests/IntegratedTests/TimelineTest.cs b/BackEnd/Timeline.Tests/IntegratedTests/TimelineTest.cs
index 4247e572..28fcb9fa 100644
--- a/BackEnd/Timeline.Tests/IntegratedTests/TimelineTest.cs
+++ b/BackEnd/Timeline.Tests/IntegratedTests/TimelineTest.cs
@@ -1,8 +1,6 @@
using FluentAssertions;
using System;
using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
using System.Net;
using System.Threading.Tasks;
using Timeline.Models;
@@ -11,7 +9,6 @@ using Xunit;
namespace Timeline.Tests.IntegratedTests
{
-
public class TimelineTest : BaseTimelineTest
{
[Fact]
@@ -22,7 +19,6 @@ namespace Timeline.Tests.IntegratedTests
await client.TestGetAssertInvalidModelAsync("timelines/@!!!");
await client.TestGetAssertInvalidModelAsync("timelines/!!!");
-
{
var body = await client.TestGetAsync<HttpTimeline>("timelines/@user1");
body.Owner.Should().BeEquivalentTo(await client.GetUserAsync("user1"));
@@ -345,90 +341,6 @@ namespace Timeline.Tests.IntegratedTests
[Theory]
[MemberData(nameof(TimelineNameGeneratorTestData))]
- public async Task Post_ModifiedSince_And_IncludeDeleted(TimelineNameGenerator generator)
- {
- using var client = await CreateClientAsUser();
-
- var postContentList = new List<string> { "a", "b", "c", "d" };
- var posts = new List<HttpTimelinePost>();
-
- foreach (var (content, index) in postContentList.Select((v, i) => (v, i)))
- {
- var post = await client.TestPostAsync<HttpTimelinePost>($"timelines/{generator(1)}/posts",
- new HttpTimelinePostCreateRequest { Content = new HttpTimelinePostCreateRequestContent { Text = content, Type = TimelinePostContentTypes.Text } });
- posts.Add(post);
- await Task.Delay(1000);
- }
-
- await client.TestDeleteAsync($"timelines/{generator(1)}/posts/{posts[2].Id}");
-
- {
-
- posts = await client.TestGetAsync<List<HttpTimelinePost>>($"timelines/{generator(1)}/posts?modifiedSince={posts[1].LastUpdated.ToString("s", CultureInfo.InvariantCulture)}&includeDeleted=true");
- posts.Should().HaveCount(3);
- posts.Select(p => p.Deleted).Should().Equal(false, true, false);
- posts.Select(p => p.Content == null).Should().Equal(false, true, false);
- }
- }
-
- [Theory]
- [MemberData(nameof(TimelineNameGeneratorTestData))]
- public async Task Timeline_Get_IfModifiedSince_And_CheckUniqueId(TimelineNameGenerator generator)
- {
- using var client = await CreateClientAsUser();
-
- DateTime lastModifiedTime;
- HttpTimeline timeline;
- string uniqueId;
-
- {
- var body = await client.GetTimelineAsync(generator(1));
- timeline = body;
- lastModifiedTime = body.LastModified;
- uniqueId = body.UniqueId;
- }
-
- {
- await client.TestGetAsync($"timelines/{generator(1)}",
- expectedStatusCode: HttpStatusCode.NotModified,
- headerSetup: (headers, _) =>
- {
- headers.IfModifiedSince = lastModifiedTime.AddSeconds(1);
- });
- }
-
- {
-
- var body = await client.TestGetAsync<HttpTimeline>($"timelines/{generator(1)}",
- headerSetup: (headers, _) =>
- {
- headers.IfModifiedSince = lastModifiedTime.AddSeconds(-1);
- });
- body.Should().BeEquivalentTo(timeline);
- }
-
- {
- await client.TestGetAsync($"timelines/{generator(1)}?ifModifiedSince={lastModifiedTime.AddSeconds(1).ToString("s", CultureInfo.InvariantCulture) }", expectedStatusCode: HttpStatusCode.NotModified);
- }
-
- {
- var body = await client.TestGetAsync<HttpTimeline>($"timelines/{generator(1)}?ifModifiedSince={lastModifiedTime.AddSeconds(-1).ToString("s", CultureInfo.InvariantCulture) }");
- body.Should().BeEquivalentTo(timeline);
- }
-
- {
- await client.TestGetAsync($"timelines/{generator(1)}?ifModifiedSince={lastModifiedTime.AddSeconds(1).ToString("s", CultureInfo.InvariantCulture) }&checkUniqueId={uniqueId}", expectedStatusCode: HttpStatusCode.NotModified);
- }
-
- {
- var testUniqueId = (uniqueId[0] == 'a' ? "b" : "a") + uniqueId[1..];
- var body = await client.TestGetAsync<HttpTimeline>($"timelines/{generator(1)}?ifModifiedSince={lastModifiedTime.AddSeconds(1).ToString("s", CultureInfo.InvariantCulture) }&checkUniqueId={testUniqueId}");
- body.Should().BeEquivalentTo(timeline);
- }
- }
-
- [Theory]
- [MemberData(nameof(TimelineNameGeneratorTestData))]
public async Task Title(TimelineNameGenerator generator)
{
using var client = await CreateClientAsUser();
@@ -454,21 +366,20 @@ namespace Timeline.Tests.IntegratedTests
{
{
using var client = await CreateDefaultClient();
- await client.TestPostAssertUnauthorizedAsync("timelineop/changename", new HttpTimelineChangeNameRequest { OldName = "t1", NewName = "tttttttt" });
+ await client.TestPatchAssertUnauthorizedAsync("timelines/t1", new HttpTimelinePatchRequest { Name = "tttttttt" });
}
{
using var client = await CreateClientAs(2);
- await client.TestPostAssertForbiddenAsync("timelineop/changename", new HttpTimelineChangeNameRequest { OldName = "t1", NewName = "tttttttt" });
+ await client.TestPatchAssertForbiddenAsync("timelines/t1", new HttpTimelinePatchRequest { Name = "tttttttt" });
}
using (var client = await CreateClientAsUser())
{
- await client.TestPostAssertInvalidModelAsync("timelineop/changename", new HttpTimelineChangeNameRequest { OldName = "!!!", NewName = "tttttttt" });
- await client.TestPostAssertInvalidModelAsync("timelineop/changename", new HttpTimelineChangeNameRequest { OldName = "ttt", NewName = "!!!!" });
- await client.TestPostAssertErrorAsync("timelineop/changename", new HttpTimelineChangeNameRequest { OldName = "ttttt", NewName = "tttttttt" }, errorCode: ErrorCodes.TimelineController.NotExist);
+ await client.TestPatchAssertInvalidModelAsync("timelines/t1", new HttpTimelinePatchRequest { Name = "!!!" });
+ await client.TestPatchAssertErrorAsync("timelines/t1", new HttpTimelinePatchRequest { Name = "t2" }, errorCode: ErrorCodes.TimelineController.NameConflict);
- await client.TestPostAsync("timelineop/changename", new HttpTimelineChangeNameRequest { OldName = "t1", NewName = "newt" });
+ await client.TestPatchAsync("timelines/t1", new HttpTimelinePatchRequest { Name = "newt" });
await client.TestGetAsync("timelines/t1", expectedStatusCode: HttpStatusCode.NotFound);
diff --git a/BackEnd/Timeline/Controllers/TimelineController.cs b/BackEnd/Timeline/Controllers/TimelineController.cs
index 5d484388..06ab8004 100644
--- a/BackEnd/Timeline/Controllers/TimelineController.cs
+++ b/BackEnd/Timeline/Controllers/TimelineController.cs
@@ -6,9 +6,7 @@ using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
-using Timeline.Entities;
using Timeline.Filters;
-using Timeline.Helpers;
using Timeline.Models;
using Timeline.Models.Http;
using Timeline.Models.Mapper;
@@ -22,13 +20,13 @@ namespace Timeline.Controllers
/// Operations about timeline.
/// </summary>
[ApiController]
+ [Route("timelines")]
[CatchTimelineNotExistException]
[ProducesErrorResponseType(typeof(CommonResponse))]
public class TimelineController : Controller
{
private readonly IUserService _userService;
private readonly ITimelineService _service;
- private readonly ITimelinePostService _postService;
private readonly TimelineMapper _timelineMapper;
private readonly IMapper _mapper;
@@ -36,11 +34,10 @@ namespace Timeline.Controllers
/// <summary>
///
/// </summary>
- public TimelineController(IUserService userService, ITimelineService service, ITimelinePostService timelinePostService, TimelineMapper timelineMapper, IMapper mapper)
+ public TimelineController(IUserService userService, ITimelineService service, TimelineMapper timelineMapper, IMapper mapper)
{
_userService = userService;
_service = service;
- _postService = timelinePostService;
_timelineMapper = timelineMapper;
_mapper = mapper;
}
@@ -54,7 +51,7 @@ namespace Timeline.Controllers
/// <param name="relateType">Specify the relation type, may be 'own' or 'join'. If not set, both type will return.</param>
/// <param name="visibility">"Private" or "Register" or "Public". If set, only timelines whose visibility is specified one will return.</param>
/// <returns>The timeline list.</returns>
- [HttpGet("timelines")]
+ [HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult<List<HttpTimeline>>> TimelineList([FromQuery][Username] string? relate, [FromQuery][RegularExpression("(own)|(join)")] string? relateType, [FromQuery] string? visibility)
@@ -117,254 +114,50 @@ namespace Timeline.Controllers
/// Get info of a timeline.
/// </summary>
/// <param name="timeline">The timeline name.</param>
- /// <param name="checkUniqueId">A unique id. If specified and if-modified-since is also specified, the timeline info will return when unique id is not the specified one even if it is not modified.</param>
- /// <param name="queryIfModifiedSince">Same effect as If-Modified-Since header and take precedence than it.</param>
- /// <param name="headerIfModifiedSince">If specified, will return 304 if not modified.</param>
/// <returns>The timeline info.</returns>
- [HttpGet("timelines/{timeline}")]
+ [HttpGet("{timeline}")]
[ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status304NotModified)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task<ActionResult<HttpTimeline>> TimelineGet([FromRoute][GeneralTimelineName] string timeline, [FromQuery] string? checkUniqueId, [FromQuery(Name = "ifModifiedSince")] DateTime? queryIfModifiedSince, [FromHeader(Name = "If-Modified-Since")] DateTime? headerIfModifiedSince)
+ public async Task<ActionResult<HttpTimeline>> TimelineGet([FromRoute][GeneralTimelineName] string timeline)
{
- DateTime? ifModifiedSince = null;
- if (queryIfModifiedSince.HasValue)
- {
- ifModifiedSince = queryIfModifiedSince.Value;
- }
- else if (headerIfModifiedSince is not null)
- {
- ifModifiedSince = headerIfModifiedSince.Value;
- }
-
var timelineId = await _service.GetTimelineIdByName(timeline);
-
- bool returnNotModified = false;
-
- if (ifModifiedSince.HasValue)
- {
- var lastModified = await _service.GetTimelineLastModifiedTime(timelineId);
- if (lastModified < ifModifiedSince.Value)
- {
- if (checkUniqueId != null)
- {
- var uniqueId = await _service.GetTimelineUniqueId(timelineId);
- if (uniqueId == checkUniqueId)
- {
- returnNotModified = true;
- }
- }
- else
- {
- returnNotModified = true;
- }
- }
- }
-
- if (returnNotModified)
- {
- return StatusCode(StatusCodes.Status304NotModified);
- }
- else
- {
- var t = await _service.GetTimeline(timelineId);
- var result = await _timelineMapper.MapToHttp(t, Url, this.GetOptionalUserId());
- return result;
- }
- }
-
- /// <summary>
- /// Get posts of a timeline.
- /// </summary>
- /// <param name="timeline">The name of the timeline.</param>
- /// <param name="modifiedSince">If set, only posts modified since the time will return.</param>
- /// <param name="includeDeleted">If set to true, deleted post will also return.</param>
- /// <returns>The post list.</returns>
- [HttpGet("timelines/{timeline}/posts")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status403Forbidden)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task<ActionResult<List<HttpTimelinePost>>> PostListGet([FromRoute][GeneralTimelineName] string timeline, [FromQuery] DateTime? modifiedSince, [FromQuery] bool? includeDeleted)
- {
- var timelineId = await _service.GetTimelineIdByName(timeline);
-
- if (!UserHasAllTimelineManagementPermission && !await _service.HasReadPermission(timelineId, this.GetOptionalUserId()))
- {
- return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid());
- }
-
- var posts = await _postService.GetPosts(timelineId, modifiedSince, includeDeleted ?? false);
-
- var result = await _timelineMapper.MapToHttp(posts, timeline, Url);
+ var t = await _service.GetTimeline(timelineId);
+ var result = await _timelineMapper.MapToHttp(t, Url, this.GetOptionalUserId());
return result;
}
/// <summary>
- /// Get the data of a post. Usually a image post.
- /// </summary>
- /// <param name="timeline">Timeline name.</param>
- /// <param name="post">The id of the post.</param>
- /// <param name="ifNoneMatch">If-None-Match header.</param>
- /// <returns>The data.</returns>
- [HttpGet("timelines/{timeline}/posts/{post}/data")]
- [Produces("image/png", "image/jpeg", "image/gif", "image/webp", "application/json", "text/json")]
- [ProducesResponseType(typeof(byte[]), StatusCodes.Status200OK)]
- [ProducesResponseType(typeof(void), StatusCodes.Status304NotModified)]
- [ProducesResponseType(StatusCodes.Status400BadRequest)]
- [ProducesResponseType(StatusCodes.Status403Forbidden)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task<IActionResult> PostDataGet([FromRoute][GeneralTimelineName] string timeline, [FromRoute] long post, [FromHeader(Name = "If-None-Match")] string? ifNoneMatch)
- {
- _ = ifNoneMatch;
-
- var timelineId = await _service.GetTimelineIdByName(timeline);
-
- if (!UserHasAllTimelineManagementPermission && !await _service.HasReadPermission(timelineId, this.GetOptionalUserId()))
- {
- return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid());
- }
-
- try
- {
- return await DataCacheHelper.GenerateActionResult(this,
- () => _postService.GetPostDataETag(timelineId, post),
- async () => await _postService.GetPostData(timelineId, post));
- }
- catch (TimelinePostNotExistException)
- {
- return NotFound(ErrorResponse.TimelineController.PostNotExist());
- }
- catch (TimelinePostNoDataException)
- {
- return BadRequest(ErrorResponse.TimelineController.PostNoData());
- }
- }
-
- /// <summary>
- /// Create a new post.
+ /// Change properties of a timeline.
/// </summary>
/// <param name="timeline">Timeline name.</param>
/// <param name="body"></param>
- /// <returns>Info of new post.</returns>
- [HttpPost("timelines/{timeline}/posts")]
+ /// <returns>The new info.</returns>
+ [HttpPatch("{timeline}")]
[Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
- public async Task<ActionResult<HttpTimelinePost>> PostPost([FromRoute][GeneralTimelineName] string timeline, [FromBody] HttpTimelinePostCreateRequest body)
+ public async Task<ActionResult<HttpTimeline>> TimelinePatch([FromRoute][GeneralTimelineName] string timeline, [FromBody] HttpTimelinePatchRequest body)
{
var timelineId = await _service.GetTimelineIdByName(timeline);
- var userId = this.GetUserId();
- if (!UserHasAllTimelineManagementPermission && !await _service.IsMemberOf(timelineId, userId))
+ if (!UserHasAllTimelineManagementPermission && !await _service.HasManagePermission(timelineId, this.GetUserId()))
{
return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid());
}
- var content = body.Content;
-
- TimelinePostEntity post;
-
- if (content.Type == TimelinePostContentTypes.Text)
- {
- var text = content.Text;
- if (text == null)
- {
- return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_TextContentTextRequired));
- }
- post = await _postService.CreateTextPost(timelineId, userId, text, body.Time);
- }
- else if (content.Type == TimelinePostContentTypes.Image)
- {
- var base64Data = content.Data;
- if (base64Data == null)
- {
- return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_ImageContentDataRequired));
- }
- byte[] data;
- try
- {
- data = Convert.FromBase64String(base64Data);
- }
- catch (FormatException)
- {
- return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_ImageContentDataNotBase64));
- }
-
- try
- {
- post = await _postService.CreateImagePost(timelineId, userId, data, body.Time);
- }
- catch (ImageException)
- {
- return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_ImageContentDataNotImage));
- }
- }
- else
- {
- return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_ContentUnknownType));
- }
-
- var result = await _timelineMapper.MapToHttp(post, timeline, Url);
- return result;
- }
-
- /// <summary>
- /// Delete a post.
- /// </summary>
- /// <param name="timeline">Timeline name.</param>
- /// <param name="post">Post id.</param>
- /// <returns>Info of deletion.</returns>
- [HttpDelete("timelines/{timeline}/posts/{post}")]
- [Authorize]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status400BadRequest)]
- [ProducesResponseType(StatusCodes.Status401Unauthorized)]
- [ProducesResponseType(StatusCodes.Status403Forbidden)]
- public async Task<ActionResult> PostDelete([FromRoute][GeneralTimelineName] string timeline, [FromRoute] long post)
- {
- var timelineId = await _service.GetTimelineIdByName(timeline);
-
try
{
- if (!UserHasAllTimelineManagementPermission && !await _postService.HasPostModifyPermission(timelineId, post, this.GetUserId(), true))
- {
- return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid());
- }
- await _postService.DeletePost(timelineId, post);
- return Ok();
- }
- catch (TimelinePostNotExistException)
- {
- return BadRequest(ErrorResponse.TimelineController.PostNotExist());
+ await _service.ChangeProperty(timelineId, _mapper.Map<TimelineChangePropertyParams>(body));
+ var t = await _service.GetTimeline(timelineId);
+ var result = await _timelineMapper.MapToHttp(t, Url, this.GetOptionalUserId());
+ return result;
}
- }
-
- /// <summary>
- /// Change properties of a timeline.
- /// </summary>
- /// <param name="timeline">Timeline name.</param>
- /// <param name="body"></param>
- /// <returns>The new info.</returns>
- [HttpPatch("timelines/{timeline}")]
- [Authorize]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status400BadRequest)]
- [ProducesResponseType(StatusCodes.Status401Unauthorized)]
- [ProducesResponseType(StatusCodes.Status403Forbidden)]
- public async Task<ActionResult<HttpTimeline>> TimelinePatch([FromRoute][GeneralTimelineName] string timeline, [FromBody] HttpTimelinePatchRequest body)
- {
- var timelineId = await _service.GetTimelineIdByName(timeline);
-
- if (!UserHasAllTimelineManagementPermission && !await _service.HasManagePermission(timelineId, this.GetUserId()))
+ catch (EntityAlreadyExistException)
{
- return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid());
+ return BadRequest(ErrorResponse.TimelineController.NameConflict());
}
- await _service.ChangeProperty(timelineId, _mapper.Map<TimelineChangePropertyParams>(body));
- var t = await _service.GetTimeline(timelineId);
- var result = await _timelineMapper.MapToHttp(t, Url, this.GetOptionalUserId());
- return result;
}
/// <summary>
@@ -372,7 +165,7 @@ namespace Timeline.Controllers
/// </summary>
/// <param name="timeline">Timeline name.</param>
/// <param name="member">The new member's username.</param>
- [HttpPut("timelines/{timeline}/members/{member}")]
+ [HttpPut("{timeline}/members/{member}")]
[Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
@@ -404,7 +197,7 @@ namespace Timeline.Controllers
/// </summary>
/// <param name="timeline">Timeline name.</param>
/// <param name="member">The member's username.</param>
- [HttpDelete("timelines/{timeline}/members/{member}")]
+ [HttpDelete("{timeline}/members/{member}")]
[Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
@@ -436,7 +229,7 @@ namespace Timeline.Controllers
/// </summary>
/// <param name="body"></param>
/// <returns>Info of new timeline.</returns>
- [HttpPost("timelines")]
+ [HttpPost]
[Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
@@ -462,7 +255,7 @@ namespace Timeline.Controllers
/// </summary>
/// <param name="timeline">Timeline name.</param>
/// <returns>Info of deletion.</returns>
- [HttpDelete("timelines/{timeline}")]
+ [HttpDelete("{timeline}")]
[Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
@@ -487,32 +280,5 @@ namespace Timeline.Controllers
return BadRequest(ErrorResponse.TimelineController.NotExist());
}
}
-
- [HttpPost("timelineop/changename")]
- [Authorize]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status400BadRequest)]
- [ProducesResponseType(StatusCodes.Status401Unauthorized)]
- [ProducesResponseType(StatusCodes.Status403Forbidden)]
- public async Task<ActionResult<HttpTimeline>> TimelineOpChangeName([FromBody] HttpTimelineChangeNameRequest body)
- {
- var timelineId = await _service.GetTimelineIdByName(body.OldName);
-
- if (!UserHasAllTimelineManagementPermission && !(await _service.HasManagePermission(timelineId, this.GetUserId())))
- {
- return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid());
- }
-
- try
- {
- await _service.ChangeTimelineName(timelineId, body.NewName);
- var timeline = await _service.GetTimeline(timelineId);
- return await _timelineMapper.MapToHttp(timeline, Url, this.GetOptionalUserId());
- }
- catch (EntityAlreadyExistException)
- {
- return BadRequest(ErrorResponse.TimelineController.NameConflict());
- }
- }
}
}
diff --git a/BackEnd/Timeline/Controllers/TimelinePostController.cs b/BackEnd/Timeline/Controllers/TimelinePostController.cs
new file mode 100644
index 00000000..afe9b36f
--- /dev/null
+++ b/BackEnd/Timeline/Controllers/TimelinePostController.cs
@@ -0,0 +1,213 @@
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Timeline.Entities;
+using Timeline.Filters;
+using Timeline.Helpers;
+using Timeline.Models;
+using Timeline.Models.Http;
+using Timeline.Models.Mapper;
+using Timeline.Models.Validation;
+using Timeline.Services;
+using Timeline.Services.Exceptions;
+
+namespace Timeline.Controllers
+{
+ /// <summary>
+ /// Operations about timeline.
+ /// </summary>
+ [ApiController]
+ [Route("timelines/{timeline}/posts")]
+ [CatchTimelineNotExistException]
+ [ProducesErrorResponseType(typeof(CommonResponse))]
+ public class TimelinePostController : Controller
+ {
+ private readonly ITimelineService _timelineService;
+ private readonly ITimelinePostService _postService;
+
+ private readonly TimelineMapper _timelineMapper;
+
+ /// <summary>
+ ///
+ /// </summary>
+ public TimelinePostController(ITimelineService timelineService, ITimelinePostService timelinePostService, TimelineMapper timelineMapper)
+ {
+ _timelineService = timelineService;
+ _postService = timelinePostService;
+ _timelineMapper = timelineMapper;
+ }
+
+ private bool UserHasAllTimelineManagementPermission => this.UserHasPermission(UserPermission.AllTimelineManagement);
+
+ /// <summary>
+ /// Get posts of a timeline.
+ /// </summary>
+ /// <param name="timeline">The name of the timeline.</param>
+ /// <param name="modifiedSince">If set, only posts modified since the time will return.</param>
+ /// <param name="includeDeleted">If set to true, deleted post will also return.</param>
+ /// <returns>The post list.</returns>
+ [HttpGet]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<ActionResult<List<HttpTimelinePost>>> PostList([FromRoute][GeneralTimelineName] string timeline, [FromQuery] DateTime? modifiedSince, [FromQuery] bool? includeDeleted)
+ {
+ var timelineId = await _timelineService.GetTimelineIdByName(timeline);
+
+ if (!UserHasAllTimelineManagementPermission && !await _timelineService.HasReadPermission(timelineId, this.GetOptionalUserId()))
+ {
+ return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid());
+ }
+
+ var posts = await _postService.GetPosts(timelineId, modifiedSince, includeDeleted ?? false);
+
+ var result = await _timelineMapper.MapToHttp(posts, timeline, Url);
+ return result;
+ }
+
+ /// <summary>
+ /// Get the data of a post. Usually a image post.
+ /// </summary>
+ /// <param name="timeline">Timeline name.</param>
+ /// <param name="post">The id of the post.</param>
+ /// <param name="ifNoneMatch">If-None-Match header.</param>
+ /// <returns>The data.</returns>
+ [HttpGet("{post}/data")]
+ [Produces("image/png", "image/jpeg", "image/gif", "image/webp", "application/json", "text/json")]
+ [ProducesResponseType(typeof(byte[]), StatusCodes.Status200OK)]
+ [ProducesResponseType(typeof(void), StatusCodes.Status304NotModified)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<IActionResult> PostDataGet([FromRoute][GeneralTimelineName] string timeline, [FromRoute] long post, [FromHeader(Name = "If-None-Match")] string? ifNoneMatch)
+ {
+ _ = ifNoneMatch;
+
+ var timelineId = await _timelineService.GetTimelineIdByName(timeline);
+
+ if (!UserHasAllTimelineManagementPermission && !await _timelineService.HasReadPermission(timelineId, this.GetOptionalUserId()))
+ {
+ return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid());
+ }
+
+ try
+ {
+ return await DataCacheHelper.GenerateActionResult(this,
+ () => _postService.GetPostDataETag(timelineId, post),
+ async () => await _postService.GetPostData(timelineId, post));
+ }
+ catch (TimelinePostNotExistException)
+ {
+ return NotFound(ErrorResponse.TimelineController.PostNotExist());
+ }
+ catch (TimelinePostNoDataException)
+ {
+ return BadRequest(ErrorResponse.TimelineController.PostNoData());
+ }
+ }
+
+ /// <summary>
+ /// Create a new post.
+ /// </summary>
+ /// <param name="timeline">Timeline name.</param>
+ /// <param name="body"></param>
+ /// <returns>Info of new post.</returns>
+ [HttpPost]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ public async Task<ActionResult<HttpTimelinePost>> PostPost([FromRoute][GeneralTimelineName] string timeline, [FromBody] HttpTimelinePostCreateRequest body)
+ {
+ var timelineId = await _timelineService.GetTimelineIdByName(timeline);
+ var userId = this.GetUserId();
+
+ if (!UserHasAllTimelineManagementPermission && !await _timelineService.IsMemberOf(timelineId, userId))
+ {
+ return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid());
+ }
+
+ var content = body.Content;
+
+ TimelinePostEntity post;
+
+ if (content.Type == TimelinePostContentTypes.Text)
+ {
+ var text = content.Text;
+ if (text == null)
+ {
+ return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_TextContentTextRequired));
+ }
+ post = await _postService.CreateTextPost(timelineId, userId, text, body.Time);
+ }
+ else if (content.Type == TimelinePostContentTypes.Image)
+ {
+ var base64Data = content.Data;
+ if (base64Data == null)
+ {
+ return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_ImageContentDataRequired));
+ }
+ byte[] data;
+ try
+ {
+ data = Convert.FromBase64String(base64Data);
+ }
+ catch (FormatException)
+ {
+ return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_ImageContentDataNotBase64));
+ }
+
+ try
+ {
+ post = await _postService.CreateImagePost(timelineId, userId, data, body.Time);
+ }
+ catch (ImageException)
+ {
+ return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_ImageContentDataNotImage));
+ }
+ }
+ else
+ {
+ return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_ContentUnknownType));
+ }
+
+ var result = await _timelineMapper.MapToHttp(post, timeline, Url);
+ return result;
+ }
+
+ /// <summary>
+ /// Delete a post.
+ /// </summary>
+ /// <param name="timeline">Timeline name.</param>
+ /// <param name="post">Post id.</param>
+ /// <returns>Info of deletion.</returns>
+ [HttpDelete("{post}")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ public async Task<ActionResult> PostDelete([FromRoute][GeneralTimelineName] string timeline, [FromRoute] long post)
+ {
+ var timelineId = await _timelineService.GetTimelineIdByName(timeline);
+
+ try
+ {
+ if (!UserHasAllTimelineManagementPermission && !await _postService.HasPostModifyPermission(timelineId, post, this.GetUserId(), true))
+ {
+ return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid());
+ }
+ await _postService.DeletePost(timelineId, post);
+ return Ok();
+ }
+ catch (TimelinePostNotExistException)
+ {
+ return BadRequest(ErrorResponse.TimelineController.PostNotExist());
+ }
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Models/Http/TimelineController.cs b/BackEnd/Timeline/Models/Http/TimelineController.cs
index 257076f0..79be1826 100644
--- a/BackEnd/Timeline/Models/Http/TimelineController.cs
+++ b/BackEnd/Timeline/Models/Http/TimelineController.cs
@@ -59,6 +59,12 @@ namespace Timeline.Models.Http
public class HttpTimelinePatchRequest
{
/// <summary>
+ /// New name. Null for not change.
+ /// </summary>
+ [TimelineName]
+ public string? Name { get; set; }
+
+ /// <summary>
/// New title. Null for not change.
/// </summary>
public string? Title { get; set; }
@@ -74,25 +80,6 @@ namespace Timeline.Models.Http
public TimelineVisibility? Visibility { get; set; }
}
- /// <summary>
- /// Change timeline name request model.
- /// </summary>
- public class HttpTimelineChangeNameRequest
- {
- /// <summary>
- /// Old name of timeline.
- /// </summary>
- [Required]
- [TimelineName]
- public string OldName { get; set; } = default!;
- /// <summary>
- /// New name of timeline.
- /// </summary>
- [Required]
- [TimelineName]
- public string NewName { get; set; } = default!;
- }
-
public class HttpTimelineControllerAutoMapperProfile : Profile
{
public HttpTimelineControllerAutoMapperProfile()
diff --git a/BackEnd/Timeline/Models/Mapper/TimelineMapper.cs b/BackEnd/Timeline/Models/Mapper/TimelineMapper.cs
index 95418573..79a6fa1d 100644
--- a/BackEnd/Timeline/Models/Mapper/TimelineMapper.cs
+++ b/BackEnd/Timeline/Models/Mapper/TimelineMapper.cs
@@ -48,7 +48,7 @@ namespace Timeline.Models.Mapper
isBookmark: userId is not null && await _bookmarkTimelineService.IsBookmark(userId.Value, entity.Id, false, false),
links: new HttpTimelineLinks(
self: urlHelper.ActionLink(nameof(TimelineController.TimelineGet), nameof(TimelineController)[0..^nameof(Controller).Length], new { timeline = timelineName }),
- posts: urlHelper.ActionLink(nameof(TimelineController.PostListGet), nameof(TimelineController)[0..^nameof(Controller).Length], new { timeline = timelineName })
+ posts: urlHelper.ActionLink(nameof(TimelinePostController.PostList), nameof(TimelinePostController)[0..^nameof(Controller).Length], new { timeline = timelineName })
)
);
}
@@ -83,7 +83,7 @@ namespace Timeline.Models.Mapper
(
type: TimelinePostContentTypes.Image,
text: null,
- url: urlHelper.ActionLink(nameof(TimelineController.PostDataGet), nameof(TimelineController)[0..^nameof(Controller).Length], new { timeline = timelineName, post = entity.LocalId }),
+ url: urlHelper.ActionLink(nameof(TimelinePostController.PostDataGet), nameof(TimelinePostController)[0..^nameof(Controller).Length], new { timeline = timelineName, post = entity.LocalId }),
eTag: $"\"{entity.Content}\""
),
_ => throw new DatabaseCorruptedException(string.Format(CultureInfo.InvariantCulture, "Unknown timeline post type {0}.", entity.ContentType))
diff --git a/BackEnd/Timeline/Properties/launchSettings.json b/BackEnd/Timeline/Properties/launchSettings.json
index db58cd31..851fc6a8 100644
--- a/BackEnd/Timeline/Properties/launchSettings.json
+++ b/BackEnd/Timeline/Properties/launchSettings.json
@@ -2,11 +2,11 @@
"profiles": {
"Dev": {
"commandName": "Project",
- "applicationUrl": "http://0.0.0.0:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"ASPNETCORE_FRONTEND": "Proxy"
- }
+ },
+ "applicationUrl": "http://0.0.0.0:5000"
},
"Dev-Mock": {
"commandName": "Project",
@@ -20,6 +20,14 @@
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Staging"
}
+ },
+ "Dev-Windows": {
+ "commandName": "Project",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "ASPNETCORE_FRONTEND": "Proxy",
+ "TIMELINE_WORKDIR": "D:\\timeline-development"
+ }
}
}
-}
+} \ No newline at end of file
diff --git a/BackEnd/Timeline/Services/TimelineService.cs b/BackEnd/Timeline/Services/TimelineService.cs
index 1d1bb320..f4141752 100644
--- a/BackEnd/Timeline/Services/TimelineService.cs
+++ b/BackEnd/Timeline/Services/TimelineService.cs
@@ -49,6 +49,7 @@ namespace Timeline.Services
public class TimelineChangePropertyParams
{
+ public string? Name { get; set; }
public string? Title { get; set; }
public string? Description { get; set; }
public TimelineVisibility? Visibility { get; set; }
@@ -60,22 +61,6 @@ namespace Timeline.Services
public interface ITimelineService : IBasicTimelineService
{
/// <summary>
- /// Get the timeline last modified time (not include name change).
- /// </summary>
- /// <param name="id">The id of the timeline.</param>
- /// <returns>The timeline modified time.</returns>
- /// <exception cref="TimelineNotExistException">Thrown when timeline does not exist.</exception>
- Task<DateTime> GetTimelineLastModifiedTime(long id);
-
- /// <summary>
- /// Get the timeline unique id.
- /// </summary>
- /// <param name="id">The id of the timeline.</param>
- /// <returns>The timeline unique id.</returns>
- /// <exception cref="TimelineNotExistException">Thrown when timeline does not exist.</exception>
- Task<string> GetTimelineUniqueId(long id);
-
- /// <summary>
/// Get the timeline info.
/// </summary>
/// <param name="id">Id of timeline.</param>
@@ -90,6 +75,7 @@ namespace Timeline.Services
/// <param name="newProperties">The new properties. Null member means not to change.</param>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="newProperties"/> is null.</exception>
/// <exception cref="TimelineNotExistException">Thrown when timeline with given id does not exist.</exception>
+ /// <exception cref="EntityAlreadyExistException">Thrown when a timeline with new name already exists.</exception>
Task ChangeProperty(long id, TimelineChangePropertyParams newProperties);
/// <summary>
@@ -180,20 +166,6 @@ namespace Timeline.Services
/// <param name="id">The id of the timeline to delete.</param>
/// <exception cref="TimelineNotExistException">Thrown when the timeline does not exist.</exception>
Task DeleteTimeline(long id);
-
- /// <summary>
- /// Change name of a timeline.
- /// </summary>
- /// <param name="id">The timeline id.</param>
- /// <param name="newTimelineName">The new timeline name.</param>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="newTimelineName"/> is null.</exception>
- /// <exception cref="ArgumentException">Thrown when <paramref name="newTimelineName"/> is of invalid format.</exception>
- /// <exception cref="TimelineNotExistException">Thrown when timeline does not exist.</exception>
- /// <exception cref="EntityAlreadyExistException">Thrown when a timeline with new name already exists.</exception>
- /// <remarks>
- /// You can only change name of general timeline.
- /// </remarks>
- Task ChangeTimelineName(long id, string newTimelineName);
}
public class TimelineService : BasicTimelineService, ITimelineService
@@ -222,26 +194,6 @@ namespace Timeline.Services
}
}
- public async Task<DateTime> GetTimelineLastModifiedTime(long id)
- {
- var entity = await _database.Timelines.Where(t => t.Id == id).Select(t => new { t.LastModified }).SingleOrDefaultAsync();
-
- if (entity is null)
- throw new TimelineNotExistException(id);
-
- return entity.LastModified;
- }
-
- public async Task<string> GetTimelineUniqueId(long id)
- {
- var entity = await _database.Timelines.Where(t => t.Id == id).Select(t => new { t.UniqueId }).SingleOrDefaultAsync();
-
- if (entity is null)
- throw new TimelineNotExistException(id);
-
- return entity.UniqueId;
- }
-
public async Task<TimelineEntity> GetTimeline(long id)
{
var entity = await _database.Timelines.Where(t => t.Id == id).SingleOrDefaultAsync();
@@ -257,12 +209,29 @@ namespace Timeline.Services
if (newProperties is null)
throw new ArgumentNullException(nameof(newProperties));
+ if (newProperties.Name is not null)
+ ValidateTimelineName(newProperties.Name, nameof(newProperties));
+
var entity = await _database.Timelines.Where(t => t.Id == id).SingleOrDefaultAsync();
if (entity is null)
throw new TimelineNotExistException(id);
var changed = false;
+ var nameChanged = false;
+
+ if (newProperties.Name is not null)
+ {
+ var conflict = await _database.Timelines.AnyAsync(t => t.Name == newProperties.Name);
+
+ if (conflict)
+ throw new EntityAlreadyExistException(EntityNames.Timeline, null, ExceptionTimelineNameConflict);
+
+ entity.Name = newProperties.Name;
+
+ changed = true;
+ nameChanged = true;
+ }
if (newProperties.Title != null)
{
@@ -286,6 +255,8 @@ namespace Timeline.Services
{
var currentTime = _clock.GetCurrentTime();
entity.LastModified = currentTime;
+ if (nameChanged)
+ entity.NameLastModified = currentTime;
}
await _database.SaveChangesAsync();
@@ -447,34 +418,6 @@ namespace Timeline.Services
_database.Timelines.Remove(entity);
await _database.SaveChangesAsync();
}
-
- public async Task ChangeTimelineName(long id, string newTimelineName)
- {
- if (newTimelineName == null)
- throw new ArgumentNullException(nameof(newTimelineName));
-
- ValidateTimelineName(newTimelineName, nameof(newTimelineName));
-
- var entity = await _database.Timelines.Where(t => t.Id == id).SingleOrDefaultAsync();
-
- if (entity is null)
- throw new TimelineNotExistException(id);
-
- if (entity.Name == newTimelineName) return;
-
- var conflict = await _database.Timelines.AnyAsync(t => t.Name == newTimelineName);
-
- if (conflict)
- throw new EntityAlreadyExistException(EntityNames.Timeline, null, ExceptionTimelineNameConflict);
-
- var now = _clock.GetCurrentTime();
-
- entity.Name = newTimelineName;
- entity.NameLastModified = now;
- entity.LastModified = now;
-
- await _database.SaveChangesAsync();
- }
}
public static class TimelineServiceExtensions