aboutsummaryrefslogtreecommitdiff
path: root/Timeline
diff options
context:
space:
mode:
Diffstat (limited to 'Timeline')
-rw-r--r--Timeline/Controllers/PersonalTimelineController.cs131
-rw-r--r--Timeline/Controllers/TimelineController.cs146
-rw-r--r--Timeline/Controllers/UserAvatarController.cs8
-rw-r--r--Timeline/Controllers/UserController.cs1
-rw-r--r--Timeline/Entities/TimelineEntity.cs2
-rw-r--r--Timeline/Entities/TimelinePostEntity.cs6
-rw-r--r--Timeline/Filters/Timeline.cs2
-rw-r--r--Timeline/Helpers/StringLocalizerFactoryExtensions.cs19
-rw-r--r--Timeline/Migrations/20200312112552_AddImagePost.Designer.cs299
-rw-r--r--Timeline/Migrations/20200312112552_AddImagePost.cs38
-rw-r--r--Timeline/Migrations/DatabaseContextModelSnapshot.cs9
-rw-r--r--Timeline/Models/Http/ErrorResponse.cs39
-rw-r--r--Timeline/Models/Http/Timeline.cs130
-rw-r--r--Timeline/Models/Http/TimelineCommon.cs83
-rw-r--r--Timeline/Models/Http/TimelineController.cs12
-rw-r--r--Timeline/Models/Http/UserInfo.cs16
-rw-r--r--Timeline/Models/Timeline.cs87
-rw-r--r--Timeline/Models/User.cs (renamed from Timeline/Services/User.cs)2
-rw-r--r--Timeline/Models/Validation/GeneralTimelineNameValidator.cs33
-rw-r--r--Timeline/Resources/Messages.Designer.cs75
-rw-r--r--Timeline/Resources/Messages.resx27
-rw-r--r--Timeline/Resources/Services/Exception.Designer.cs84
-rw-r--r--Timeline/Resources/Services/Exception.resx30
-rw-r--r--Timeline/Resources/Services/TimelineService.Designer.cs36
-rw-r--r--Timeline/Resources/Services/TimelineService.resx12
-rw-r--r--Timeline/Services/AvatarFormatException.cs51
-rw-r--r--Timeline/Services/ImageException.cs54
-rw-r--r--Timeline/Services/ImageValidator.cs53
-rw-r--r--Timeline/Services/TimelinePostNotExistException.cs11
-rw-r--r--Timeline/Services/TimelineService.cs682
-rw-r--r--Timeline/Services/UserAvatarService.cs47
-rw-r--r--Timeline/Services/UserService.cs1
-rw-r--r--Timeline/Services/UserTokenManager.cs1
-rw-r--r--Timeline/Startup.cs4
-rw-r--r--Timeline/Timeline.csproj8
35 files changed, 1566 insertions, 673 deletions
diff --git a/Timeline/Controllers/PersonalTimelineController.cs b/Timeline/Controllers/PersonalTimelineController.cs
deleted file mode 100644
index cef04a97..00000000
--- a/Timeline/Controllers/PersonalTimelineController.cs
+++ /dev/null
@@ -1,131 +0,0 @@
-using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Mvc;
-using Microsoft.Extensions.Logging;
-using System.Collections.Generic;
-using System.Threading.Tasks;
-using Timeline.Filters;
-using Timeline.Models.Http;
-using Timeline.Models.Validation;
-using Timeline.Services;
-
-namespace Timeline.Controllers
-{
- [ApiController]
- [CatchTimelineNotExistException]
- public class PersonalTimelineController : Controller
- {
- private readonly ILogger<PersonalTimelineController> _logger;
-
- private readonly IPersonalTimelineService _service;
-
- public PersonalTimelineController(ILogger<PersonalTimelineController> logger, IPersonalTimelineService service)
- {
- _logger = logger;
- _service = service;
- }
-
- [HttpGet("users/{username}/timeline")]
- public async Task<ActionResult<TimelineInfo>> TimelineGet([FromRoute][Username] string username)
- {
- return (await _service.GetTimeline(username)).FillLinks(Url);
- }
-
- [HttpGet("users/{username}/timeline/posts")]
- public async Task<ActionResult<IList<TimelinePostInfo>>> PostListGet([FromRoute][Username] string username)
- {
- if (!this.IsAdministrator() && !await _service.HasReadPermission(username, this.GetOptionalUserId()))
- {
- return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid());
- }
-
- return await _service.GetPosts(username);
- }
-
- [HttpPost("users/{username}/timeline/posts")]
- [Authorize]
- public async Task<ActionResult<TimelinePostInfo>> PostPost([FromRoute][Username] string username, [FromBody] TimelinePostCreateRequest body)
- {
- var id = this.GetUserId();
- if (!this.IsAdministrator() && !await _service.IsMemberOf(username, id))
- {
- return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid());
- }
-
- var res = await _service.CreatePost(username, id, body.Content, body.Time);
- return res;
- }
-
- [HttpDelete("users/{username}/timeline/posts/{id}")]
- [Authorize]
- public async Task<ActionResult> PostDelete([FromRoute][Username] string username, [FromRoute] long id)
- {
- try
- {
- if (!this.IsAdministrator() && !await _service.HasPostModifyPermission(username, id, this.GetUserId()))
- {
- return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid());
- }
- await _service.DeletePost(username, id);
- return Ok(CommonDeleteResponse.Delete());
- }
- catch (TimelinePostNotExistException)
- {
- return Ok(CommonDeleteResponse.NotExist());
- }
- }
-
- [HttpPatch("users/{username}/timeline")]
- [Authorize]
- public async Task<ActionResult<TimelineInfo>> TimelinePatch([FromRoute][Username] string username, [FromBody] TimelinePatchRequest body)
- {
- if (!this.IsAdministrator() && !(await _service.HasManagePermission(username, this.GetUserId())))
- {
- return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid());
- }
- await _service.ChangeProperty(username, body);
- var timeline = (await _service.GetTimeline(username)).FillLinks(Url);
- return Ok(timeline);
- }
-
- [HttpPut("users/{username}/timeline/members/{member}")]
- [Authorize]
- public async Task<ActionResult> TimelineMemberPut([FromRoute][Username] string username, [FromRoute][Username] string member)
- {
- if (!this.IsAdministrator() && !(await _service.HasManagePermission(username, this.GetUserId())))
- {
- return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid());
- }
-
- try
- {
- await _service.ChangeMember(username, new List<string> { member }, null);
- return Ok();
- }
- catch (UserNotExistException)
- {
- return BadRequest(ErrorResponse.TimelineCommon.MemberPut_NotExist());
- }
- }
-
- [HttpDelete("users/{username}/timeline/members/{member}")]
- [Authorize]
- public async Task<ActionResult> TimelineMemberDelete([FromRoute][Username] string username, [FromRoute][Username] string member)
- {
- if (!this.IsAdministrator() && !(await _service.HasManagePermission(username, this.GetUserId())))
- {
- return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid());
- }
-
- try
- {
- await _service.ChangeMember(username, null, new List<string> { member });
- return Ok(CommonDeleteResponse.Delete());
- }
- catch (UserNotExistException)
- {
- return Ok(CommonDeleteResponse.NotExist());
- }
- }
- }
-}
diff --git a/Timeline/Controllers/TimelineController.cs b/Timeline/Controllers/TimelineController.cs
index 85ccb5c1..58390c29 100644
--- a/Timeline/Controllers/TimelineController.cs
+++ b/Timeline/Controllers/TimelineController.cs
@@ -1,12 +1,15 @@
-using Microsoft.AspNetCore.Authorization;
+using AutoMapper;
+using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
+using Microsoft.Net.Http.Headers;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using Timeline.Filters;
+using Timeline.Models;
using Timeline.Models.Http;
using Timeline.Models.Validation;
using Timeline.Services;
@@ -22,11 +25,14 @@ namespace Timeline.Controllers
private readonly IUserService _userService;
private readonly ITimelineService _service;
- public TimelineController(ILogger<TimelineController> logger, IUserService userService, ITimelineService service)
+ private readonly IMapper _mapper;
+
+ public TimelineController(ILogger<TimelineController> logger, IUserService userService, ITimelineService service, IMapper mapper)
{
_logger = logger;
_userService = userService;
_service = service;
+ _mapper = mapper;
}
[HttpGet("timelines")]
@@ -81,32 +87,60 @@ namespace Timeline.Controllers
}
}
- var result = await _service.GetTimelines(relationship, visibilityFilter);
- result.ForEach(t => t.FillLinks(Url));
- return Ok(result);
+ var timelines = await _service.GetTimelines(relationship, visibilityFilter);
+ var result = _mapper.Map<List<TimelineInfo>>(timelines);
+ return result;
}
[HttpGet("timelines/{name}")]
- public async Task<ActionResult<TimelineInfo>> TimelineGet([FromRoute][TimelineName] string name)
+ public async Task<ActionResult<TimelineInfo>> TimelineGet([FromRoute][GeneralTimelineName] string name)
{
- var result = (await _service.GetTimeline(name)).FillLinks(Url);
- return Ok(result);
+ var timeline = await _service.GetTimeline(name);
+ var result = _mapper.Map<TimelineInfo>(timeline);
+ return result;
}
[HttpGet("timelines/{name}/posts")]
- public async Task<ActionResult<IList<TimelinePostInfo>>> PostListGet([FromRoute][TimelineName] string name)
+ public async Task<ActionResult<List<TimelinePostInfo>>> PostListGet([FromRoute][GeneralTimelineName] string name)
{
if (!this.IsAdministrator() && !await _service.HasReadPermission(name, this.GetOptionalUserId()))
{
return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid());
}
- return await _service.GetPosts(name);
+ var posts = await _service.GetPosts(name);
+ var result = _mapper.Map<List<TimelinePostInfo>>(posts);
+
+ return result;
+ }
+
+ // TODO: Make cache available.
+ [HttpGet("timelines/{name}/posts/{id}/data")]
+ public async Task<ActionResult<List<TimelinePostInfo>>> PostDataGet([FromRoute][GeneralTimelineName] string name, [FromRoute] long id)
+ {
+ if (!this.IsAdministrator() && !await _service.HasReadPermission(name, this.GetOptionalUserId()))
+ {
+ return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid());
+ }
+
+ try
+ {
+ var data = await _service.GetPostData(name, id);
+ return File(data.Data, data.Type, data.LastModified, new EntityTagHeaderValue($"\"{data.ETag}\""));
+ }
+ catch (TimelinePostNotExistException)
+ {
+ return NotFound(ErrorResponse.TimelineController.PostNotExist());
+ }
+ catch (InvalidOperationException)
+ {
+ return BadRequest(ErrorResponse.TimelineController.PostNoData());
+ }
}
[HttpPost("timelines/{name}/posts")]
[Authorize]
- public async Task<ActionResult<TimelinePostInfo>> PostPost([FromRoute][TimelineName] string name, [FromBody] TimelinePostCreateRequest body)
+ public async Task<ActionResult<TimelinePostInfo>> PostPost([FromRoute][GeneralTimelineName] string name, [FromBody] TimelinePostCreateRequest body)
{
var id = this.GetUserId();
if (!this.IsAdministrator() && !await _service.IsMemberOf(name, id))
@@ -114,45 +148,90 @@ namespace Timeline.Controllers
return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid());
}
- var res = await _service.CreatePost(name, id, body.Content, body.Time);
- return res;
+ var content = body.Content;
+
+ TimelinePost 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 _service.CreateTextPost(name, id, 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 _service.CreateImagePost(name, id, 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 = _mapper.Map<TimelinePostInfo>(post);
+ return result;
}
[HttpDelete("timelines/{name}/posts/{id}")]
[Authorize]
- public async Task<ActionResult> PostDelete([FromRoute][TimelineName] string name, [FromRoute] long id)
+ public async Task<ActionResult<CommonDeleteResponse>> PostDelete([FromRoute][GeneralTimelineName] string name, [FromRoute] long id)
{
+ if (!this.IsAdministrator() && !await _service.HasPostModifyPermission(name, id, this.GetUserId()))
+ {
+ return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid());
+ }
try
{
- if (!this.IsAdministrator() && !await _service.HasPostModifyPermission(name, id, this.GetUserId()))
- {
- return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid());
- }
await _service.DeletePost(name, id);
- return Ok(CommonDeleteResponse.Delete());
+ return CommonDeleteResponse.Delete();
}
catch (TimelinePostNotExistException)
{
- return Ok(CommonDeleteResponse.NotExist());
+ return CommonDeleteResponse.NotExist();
}
}
[HttpPatch("timelines/{name}")]
[Authorize]
- public async Task<ActionResult<TimelineInfo>> TimelinePatch([FromRoute][TimelineName] string name, [FromBody] TimelinePatchRequest body)
+ public async Task<ActionResult<TimelineInfo>> TimelinePatch([FromRoute][GeneralTimelineName] string name, [FromBody] TimelinePatchRequest body)
{
if (!this.IsAdministrator() && !(await _service.HasManagePermission(name, this.GetUserId())))
{
return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid());
}
- await _service.ChangeProperty(name, body);
- var timeline = (await _service.GetTimeline(name)).FillLinks(Url);
- return Ok(timeline);
+ await _service.ChangeProperty(name, _mapper.Map<TimelineChangePropertyRequest>(body));
+ var timeline = await _service.GetTimeline(name);
+ var result = _mapper.Map<TimelineInfo>(timeline);
+ return result;
}
[HttpPut("timelines/{name}/members/{member}")]
[Authorize]
- public async Task<ActionResult> TimelineMemberPut([FromRoute][TimelineName] string name, [FromRoute][Username] string member)
+ public async Task<ActionResult> TimelineMemberPut([FromRoute][GeneralTimelineName] string name, [FromRoute][Username] string member)
{
if (!this.IsAdministrator() && !(await _service.HasManagePermission(name, this.GetUserId())))
{
@@ -166,13 +245,13 @@ namespace Timeline.Controllers
}
catch (UserNotExistException)
{
- return BadRequest(ErrorResponse.TimelineCommon.MemberPut_NotExist());
+ return BadRequest(ErrorResponse.TimelineController.MemberPut_NotExist());
}
}
[HttpDelete("timelines/{name}/members/{member}")]
[Authorize]
- public async Task<ActionResult> TimelineMemberDelete([FromRoute][TimelineName] string name, [FromRoute][Username] string member)
+ public async Task<ActionResult> TimelineMemberDelete([FromRoute][GeneralTimelineName] string name, [FromRoute][Username] string member)
{
if (!this.IsAdministrator() && !(await _service.HasManagePermission(name, this.GetUserId())))
{
@@ -198,18 +277,19 @@ namespace Timeline.Controllers
try
{
- var timelineInfo = (await _service.CreateTimeline(body.Name, userId)).FillLinks(Url);
- return Ok(timelineInfo);
+ var timeline = await _service.CreateTimeline(body.Name, userId);
+ var result = _mapper.Map<TimelineInfo>(timeline);
+ return result;
}
catch (ConflictException)
{
- return BadRequest(ErrorResponse.TimelineCommon.NameConflict());
+ return BadRequest(ErrorResponse.TimelineController.NameConflict());
}
}
[HttpDelete("timelines/{name}")]
[Authorize]
- public async Task<ActionResult<TimelineInfo>> TimelineDelete([FromRoute][TimelineName] string name)
+ public async Task<ActionResult<CommonDeleteResponse>> TimelineDelete([FromRoute][TimelineName] string name)
{
if (!this.IsAdministrator() && !(await _service.HasManagePermission(name, this.GetUserId())))
{
@@ -219,11 +299,11 @@ namespace Timeline.Controllers
try
{
await _service.DeleteTimeline(name);
- return Ok(CommonDeleteResponse.Delete());
+ return CommonDeleteResponse.Delete();
}
catch (TimelineNotExistException)
{
- return Ok(CommonDeleteResponse.NotExist());
+ return CommonDeleteResponse.NotExist();
}
}
}
diff --git a/Timeline/Controllers/UserAvatarController.cs b/Timeline/Controllers/UserAvatarController.cs
index 2dd279a8..f4f3db3e 100644
--- a/Timeline/Controllers/UserAvatarController.cs
+++ b/Timeline/Controllers/UserAvatarController.cs
@@ -126,14 +126,14 @@ namespace Timeline.Controllers
("Username", username), ("Mime Type", Request.ContentType)));
return Ok();
}
- catch (AvatarFormatException e)
+ catch (ImageException e)
{
_logger.LogInformation(e, Log.Format(LogPutUserBadFormat, ("Username", username)));
return BadRequest(e.Error switch
{
- AvatarFormatException.ErrorReason.CantDecode => ErrorResponse.UserAvatar.BadFormat_CantDecode(),
- AvatarFormatException.ErrorReason.UnmatchedFormat => ErrorResponse.UserAvatar.BadFormat_UnmatchedFormat(),
- AvatarFormatException.ErrorReason.BadSize => ErrorResponse.UserAvatar.BadFormat_BadSize(),
+ ImageException.ErrorReason.CantDecode => ErrorResponse.UserAvatar.BadFormat_CantDecode(),
+ ImageException.ErrorReason.UnmatchedFormat => ErrorResponse.UserAvatar.BadFormat_UnmatchedFormat(),
+ ImageException.ErrorReason.NotSquare => ErrorResponse.UserAvatar.BadFormat_BadSize(),
_ =>
throw new Exception(ExceptionUnknownAvatarFormatError)
});
diff --git a/Timeline/Controllers/UserController.cs b/Timeline/Controllers/UserController.cs
index a3e8d816..0bc8bcda 100644
--- a/Timeline/Controllers/UserController.cs
+++ b/Timeline/Controllers/UserController.cs
@@ -7,6 +7,7 @@ using System.Linq;
using System.Threading.Tasks;
using Timeline.Auth;
using Timeline.Helpers;
+using Timeline.Models;
using Timeline.Models.Http;
using Timeline.Models.Validation;
using Timeline.Services;
diff --git a/Timeline/Entities/TimelineEntity.cs b/Timeline/Entities/TimelineEntity.cs
index 56b36d4e..3149d4c2 100644
--- a/Timeline/Entities/TimelineEntity.cs
+++ b/Timeline/Entities/TimelineEntity.cs
@@ -2,7 +2,7 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
-using Timeline.Models.Http;
+using Timeline.Models;
namespace Timeline.Entities
{
diff --git a/Timeline/Entities/TimelinePostEntity.cs b/Timeline/Entities/TimelinePostEntity.cs
index 5805abe0..24bfc7a3 100644
--- a/Timeline/Entities/TimelinePostEntity.cs
+++ b/Timeline/Entities/TimelinePostEntity.cs
@@ -25,9 +25,15 @@ namespace Timeline.Entities
[ForeignKey(nameof(AuthorId))]
public UserEntity Author { get; set; } = default!;
+ [Column("content_type"), Required]
+ public string ContentType { get; set; } = default!;
+
[Column("content")]
public string? Content { get; set; }
+ [Column("extra_content")]
+ public string? ExtraContent { get; set; }
+
[Column("time")]
public DateTime Time { get; set; }
diff --git a/Timeline/Filters/Timeline.cs b/Timeline/Filters/Timeline.cs
index e133c9d6..76e8d751 100644
--- a/Timeline/Filters/Timeline.cs
+++ b/Timeline/Filters/Timeline.cs
@@ -17,7 +17,7 @@ namespace Timeline.Filters
}
else
{
- context.Result = new NotFoundObjectResult(ErrorResponse.TimelineCommon.NotExist());
+ context.Result = new NotFoundObjectResult(ErrorResponse.TimelineController.NotExist());
}
}
}
diff --git a/Timeline/Helpers/StringLocalizerFactoryExtensions.cs b/Timeline/Helpers/StringLocalizerFactoryExtensions.cs
deleted file mode 100644
index c2252b2c..00000000
--- a/Timeline/Helpers/StringLocalizerFactoryExtensions.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-
-using Microsoft.Extensions.Localization;
-using System.Reflection;
-
-namespace Timeline.Helpers
-{
- internal static class StringLocalizerFactoryExtensions
- {
- internal static IStringLocalizer Create(this IStringLocalizerFactory factory, string basename)
- {
- return factory.Create(basename, new AssemblyName(typeof(StringLocalizerFactoryExtensions).Assembly.FullName!).Name);
- }
-
- internal static StringLocalizer<T> Create<T>(this IStringLocalizerFactory factory)
- {
- return new StringLocalizer<T>(factory);
- }
- }
-} \ No newline at end of file
diff --git a/Timeline/Migrations/20200312112552_AddImagePost.Designer.cs b/Timeline/Migrations/20200312112552_AddImagePost.Designer.cs
new file mode 100644
index 00000000..bd75a916
--- /dev/null
+++ b/Timeline/Migrations/20200312112552_AddImagePost.Designer.cs
@@ -0,0 +1,299 @@
+// <auto-generated />
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Timeline.Entities;
+
+namespace Timeline.Migrations
+{
+ [DbContext(typeof(DatabaseContext))]
+ [Migration("20200312112552_AddImagePost")]
+ partial class AddImagePost
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "3.1.2");
+
+ modelBuilder.Entity("Timeline.Entities.DataEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<byte[]>("Data")
+ .IsRequired()
+ .HasColumnName("data")
+ .HasColumnType("BLOB");
+
+ b.Property<int>("Ref")
+ .HasColumnName("ref")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Tag")
+ .IsRequired()
+ .HasColumnName("tag")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Tag")
+ .IsUnique();
+
+ b.ToTable("data");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.JwtTokenEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<byte[]>("Key")
+ .IsRequired()
+ .HasColumnName("key")
+ .HasColumnType("BLOB");
+
+ b.HasKey("Id");
+
+ b.ToTable("jwt_token");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelineEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("CreateTime")
+ .HasColumnName("create_time")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("CurrentPostLocalId")
+ .HasColumnName("current_post_local_id")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Description")
+ .HasColumnName("description")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .HasColumnName("name")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("OwnerId")
+ .HasColumnName("owner")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Visibility")
+ .HasColumnName("visibility")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("OwnerId");
+
+ b.ToTable("timelines");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("TimelineId")
+ .HasColumnName("timeline")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("UserId")
+ .HasColumnName("user")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("TimelineId");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("timeline_members");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("AuthorId")
+ .HasColumnName("author")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Content")
+ .HasColumnName("content")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ContentType")
+ .IsRequired()
+ .HasColumnName("content_type")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ExtraContent")
+ .HasColumnName("extra_content")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("LastUpdated")
+ .HasColumnName("last_updated")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("LocalId")
+ .HasColumnName("local_id")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("Time")
+ .HasColumnName("time")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("TimelineId")
+ .HasColumnName("timeline")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AuthorId");
+
+ b.HasIndex("TimelineId");
+
+ b.ToTable("timeline_posts");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("DataTag")
+ .HasColumnName("data_tag")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("LastModified")
+ .HasColumnName("last_modified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Type")
+ .HasColumnName("type")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("UserId")
+ .HasColumnName("user")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("user_avatars");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.UserEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Nickname")
+ .HasColumnName("nickname")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Password")
+ .IsRequired()
+ .HasColumnName("password")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Roles")
+ .IsRequired()
+ .HasColumnName("roles")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Username")
+ .IsRequired()
+ .HasColumnName("username")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("Version")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("version")
+ .HasColumnType("INTEGER")
+ .HasDefaultValue(0L);
+
+ b.HasKey("Id");
+
+ b.HasIndex("Username")
+ .IsUnique();
+
+ b.ToTable("users");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelineEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.UserEntity", "Owner")
+ .WithMany("Timelines")
+ .HasForeignKey("OwnerId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.TimelineEntity", "Timeline")
+ .WithMany("Members")
+ .HasForeignKey("TimelineId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Timeline.Entities.UserEntity", "User")
+ .WithMany("TimelinesJoined")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.UserEntity", "Author")
+ .WithMany("TimelinePosts")
+ .HasForeignKey("AuthorId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Timeline.Entities.TimelineEntity", "Timeline")
+ .WithMany("Posts")
+ .HasForeignKey("TimelineId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.UserEntity", "User")
+ .WithOne("Avatar")
+ .HasForeignKey("Timeline.Entities.UserAvatarEntity", "UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Timeline/Migrations/20200312112552_AddImagePost.cs b/Timeline/Migrations/20200312112552_AddImagePost.cs
new file mode 100644
index 00000000..d5098ce0
--- /dev/null
+++ b/Timeline/Migrations/20200312112552_AddImagePost.cs
@@ -0,0 +1,38 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+using Timeline.Models;
+
+namespace Timeline.Migrations
+{
+ public partial class AddImagePost : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn<string>(
+ name: "content_type",
+ table: "timeline_posts",
+ nullable: false,
+ defaultValue: "");
+
+ migrationBuilder.AddColumn<string>(
+ name: "extra_content",
+ table: "timeline_posts",
+ nullable: true);
+
+ migrationBuilder.Sql($@"
+UPDATE timeline_posts
+SET content_type = '{TimelinePostContentTypes.Text}';
+ ");
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "content_type",
+ table: "timeline_posts");
+
+ migrationBuilder.DropColumn(
+ name: "extra_content",
+ table: "timeline_posts");
+ }
+ }
+}
diff --git a/Timeline/Migrations/DatabaseContextModelSnapshot.cs b/Timeline/Migrations/DatabaseContextModelSnapshot.cs
index 8170b2f0..4b5b2fa8 100644
--- a/Timeline/Migrations/DatabaseContextModelSnapshot.cs
+++ b/Timeline/Migrations/DatabaseContextModelSnapshot.cs
@@ -139,6 +139,15 @@ namespace Timeline.Migrations
.HasColumnName("content")
.HasColumnType("TEXT");
+ b.Property<string>("ContentType")
+ .IsRequired()
+ .HasColumnName("content_type")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ExtraContent")
+ .HasColumnName("extra_content")
+ .HasColumnType("TEXT");
+
b.Property<DateTime>("LastUpdated")
.HasColumnName("last_updated")
.HasColumnType("TEXT");
diff --git a/Timeline/Models/Http/ErrorResponse.cs b/Timeline/Models/Http/ErrorResponse.cs
index 9f7e70e1..bb9c44df 100644
--- a/Timeline/Models/Http/ErrorResponse.cs
+++ b/Timeline/Models/Http/ErrorResponse.cs
@@ -242,44 +242,39 @@ namespace Timeline.Models.Http
}
- public static class TimelineCommon
+ public static class TimelineController
{
public static CommonResponse NameConflict(params object?[] formatArgs)
{
- return new CommonResponse(ErrorCodes.TimelineCommon.NameConflict, string.Format(TimelineCommon_NameConflict, formatArgs));
+ return new CommonResponse(ErrorCodes.TimelineController.NameConflict, string.Format(TimelineController_NameConflict, formatArgs));
}
public static CommonResponse CustomMessage_NameConflict(string message, params object?[] formatArgs)
{
- return new CommonResponse(ErrorCodes.TimelineCommon.NameConflict, string.Format(message, formatArgs));
+ return new CommonResponse(ErrorCodes.TimelineController.NameConflict, string.Format(message, formatArgs));
}
public static CommonResponse NotExist(params object?[] formatArgs)
{
- return new CommonResponse(ErrorCodes.TimelineCommon.NotExist, string.Format(TimelineCommon_NotExist, formatArgs));
+ return new CommonResponse(ErrorCodes.TimelineController.NotExist, string.Format(TimelineController_NotExist, formatArgs));
}
public static CommonResponse CustomMessage_NotExist(string message, params object?[] formatArgs)
{
- return new CommonResponse(ErrorCodes.TimelineCommon.NotExist, string.Format(message, formatArgs));
+ return new CommonResponse(ErrorCodes.TimelineController.NotExist, string.Format(message, formatArgs));
}
public static CommonResponse MemberPut_NotExist(params object?[] formatArgs)
{
- return new CommonResponse(ErrorCodes.TimelineCommon.MemberPut_NotExist, string.Format(TimelineCommon_MemberPut_NotExist, formatArgs));
+ return new CommonResponse(ErrorCodes.TimelineController.MemberPut_NotExist, string.Format(TimelineController_MemberPut_NotExist, formatArgs));
}
public static CommonResponse CustomMessage_MemberPut_NotExist(string message, params object?[] formatArgs)
{
- return new CommonResponse(ErrorCodes.TimelineCommon.MemberPut_NotExist, string.Format(message, formatArgs));
+ return new CommonResponse(ErrorCodes.TimelineController.MemberPut_NotExist, string.Format(message, formatArgs));
}
- }
-
- public static class TimelineController
- {
-
public static CommonResponse QueryRelateNotExist(params object?[] formatArgs)
{
return new CommonResponse(ErrorCodes.TimelineController.QueryRelateNotExist, string.Format(TimelineController_QueryRelateNotExist, formatArgs));
@@ -290,6 +285,26 @@ namespace Timeline.Models.Http
return new CommonResponse(ErrorCodes.TimelineController.QueryRelateNotExist, string.Format(message, formatArgs));
}
+ public static CommonResponse PostNotExist(params object?[] formatArgs)
+ {
+ return new CommonResponse(ErrorCodes.TimelineController.PostNotExist, string.Format(TimelineController_PostNotExist, formatArgs));
+ }
+
+ public static CommonResponse CustomMessage_PostNotExist(string message, params object?[] formatArgs)
+ {
+ return new CommonResponse(ErrorCodes.TimelineController.PostNotExist, string.Format(message, formatArgs));
+ }
+
+ public static CommonResponse PostNoData(params object?[] formatArgs)
+ {
+ return new CommonResponse(ErrorCodes.TimelineController.PostNoData, string.Format(TimelineController_PostNoData, formatArgs));
+ }
+
+ public static CommonResponse CustomMessage_PostNoData(string message, params object?[] formatArgs)
+ {
+ return new CommonResponse(ErrorCodes.TimelineController.PostNoData, string.Format(message, formatArgs));
+ }
+
}
}
diff --git a/Timeline/Models/Http/Timeline.cs b/Timeline/Models/Http/Timeline.cs
new file mode 100644
index 00000000..9e2aefd0
--- /dev/null
+++ b/Timeline/Models/Http/Timeline.cs
@@ -0,0 +1,130 @@
+using AutoMapper;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Infrastructure;
+using Microsoft.AspNetCore.Mvc.Routing;
+using System;
+using System.Collections.Generic;
+using Timeline.Controllers;
+
+namespace Timeline.Models.Http
+{
+ public class TimelinePostContentInfo
+ {
+ public string Type { get; set; } = default!;
+ public string? Text { get; set; }
+ public string? Url { get; set; }
+ }
+
+ public class TimelinePostInfo
+ {
+ public long Id { get; set; }
+ public TimelinePostContentInfo Content { get; set; } = default!;
+ public DateTime Time { get; set; }
+ public UserInfo Author { get; set; } = default!;
+ public DateTime LastUpdated { get; set; } = default!;
+ }
+
+ public class TimelineInfo
+ {
+ public string? Name { get; set; }
+ public string Description { get; set; } = default!;
+ public UserInfo Owner { get; set; } = default!;
+ public TimelineVisibility Visibility { get; set; }
+#pragma warning disable CA2227 // Collection properties should be read only
+ public List<UserInfo> Members { get; set; } = default!;
+#pragma warning restore CA2227 // Collection properties should be read only
+
+#pragma warning disable CA1707 // Identifiers should not contain underscores
+ public TimelineInfoLinks _links { get; set; } = default!;
+#pragma warning restore CA1707 // Identifiers should not contain underscores
+ }
+
+ public class TimelineInfoLinks
+ {
+ public string Self { get; set; } = default!;
+ public string Posts { get; set; } = default!;
+ }
+
+ public class TimelineInfoLinksValueResolver : IValueResolver<Timeline, TimelineInfo, TimelineInfoLinks>
+ {
+ private readonly IActionContextAccessor _actionContextAccessor;
+ private readonly IUrlHelperFactory _urlHelperFactory;
+
+ public TimelineInfoLinksValueResolver(IActionContextAccessor actionContextAccessor, IUrlHelperFactory urlHelperFactory)
+ {
+ _actionContextAccessor = actionContextAccessor;
+ _urlHelperFactory = urlHelperFactory;
+ }
+
+ public TimelineInfoLinks Resolve(Timeline source, TimelineInfo destination, TimelineInfoLinks destMember, ResolutionContext context)
+ {
+ if (_actionContextAccessor.ActionContext == null)
+ throw new InvalidOperationException("No action context, can't fill urls.");
+
+ var urlHelper = _urlHelperFactory.GetUrlHelper(_actionContextAccessor.ActionContext);
+
+
+ return new TimelineInfoLinks
+ {
+ Self = urlHelper.ActionLink(nameof(TimelineController.TimelineGet), nameof(TimelineController)[0..^nameof(Controller).Length], new { source.Name }),
+ Posts = urlHelper.ActionLink(nameof(TimelineController.PostListGet), nameof(TimelineController)[0..^nameof(Controller).Length], new { source.Name })
+ };
+ }
+ }
+
+ public class TimelinePostContentResolver : IValueResolver<TimelinePost, TimelinePostInfo, TimelinePostContentInfo>
+ {
+ private readonly IActionContextAccessor _actionContextAccessor;
+ private readonly IUrlHelperFactory _urlHelperFactory;
+
+ public TimelinePostContentResolver(IActionContextAccessor actionContextAccessor, IUrlHelperFactory urlHelperFactory)
+ {
+ _actionContextAccessor = actionContextAccessor;
+ _urlHelperFactory = urlHelperFactory;
+ }
+
+ public TimelinePostContentInfo Resolve(TimelinePost source, TimelinePostInfo destination, TimelinePostContentInfo destMember, ResolutionContext context)
+ {
+ if (_actionContextAccessor.ActionContext == null)
+ throw new InvalidOperationException("No action context, can't fill urls.");
+
+ var urlHelper = _urlHelperFactory.GetUrlHelper(_actionContextAccessor.ActionContext);
+
+ var sourceContent = source.Content;
+
+ if (sourceContent is TextTimelinePostContent textContent)
+ {
+ return new TimelinePostContentInfo
+ {
+ Type = TimelinePostContentTypes.Text,
+ Text = textContent.Text
+ };
+ }
+ else if (sourceContent is ImageTimelinePostContent imageContent)
+ {
+ return new TimelinePostContentInfo
+ {
+ Type = TimelinePostContentTypes.Image,
+ Url = urlHelper.ActionLink(
+ action: nameof(TimelineController.PostDataGet),
+ controller: nameof(TimelineController)[0..^nameof(Controller).Length],
+ values: new { Name = source.TimelineName, Id = source.Id })
+ };
+ }
+ else
+ {
+ throw new InvalidOperationException("Unknown content type.");
+ }
+ }
+ }
+
+ public class TimelineInfoAutoMapperProfile : Profile
+ {
+ public TimelineInfoAutoMapperProfile()
+ {
+ CreateMap<Timeline, TimelineInfo>().ForMember(u => u._links, opt => opt.MapFrom<TimelineInfoLinksValueResolver>());
+ CreateMap<TimelinePost, TimelinePostInfo>().ForMember(p => p.Content, opt => opt.MapFrom<TimelinePostContentResolver>());
+ CreateMap<TimelinePatchRequest, TimelineChangePropertyRequest>();
+ }
+ }
+}
diff --git a/Timeline/Models/Http/TimelineCommon.cs b/Timeline/Models/Http/TimelineCommon.cs
deleted file mode 100644
index d0dfd837..00000000
--- a/Timeline/Models/Http/TimelineCommon.cs
+++ /dev/null
@@ -1,83 +0,0 @@
-using Microsoft.AspNetCore.Mvc;
-using System;
-using System.Collections.Generic;
-using Timeline.Controllers;
-
-namespace Timeline.Models.Http
-{
- public enum TimelineVisibility
- {
- /// <summary>
- /// All people including those without accounts.
- /// </summary>
- Public,
- /// <summary>
- /// Only people signed in.
- /// </summary>
- Register,
- /// <summary>
- /// Only member.
- /// </summary>
- Private
- }
-
- public class TimelinePostInfo
- {
- public long Id { get; set; }
- public string Content { get; set; } = default!;
- public DateTime Time { get; set; }
- public UserInfo Author { get; set; } = default!;
- public DateTime LastUpdated { get; set; } = default!;
- }
-
- public class TimelineInfo
- {
- public string? Name { get; set; }
- public string Description { get; set; } = default!;
- public UserInfo Owner { get; set; } = default!;
- public TimelineVisibility Visibility { get; set; }
-#pragma warning disable CA2227 // Collection properties should be read only
- public List<UserInfo> Members { get; set; } = default!;
-#pragma warning restore CA2227 // Collection properties should be read only
-
-#pragma warning disable CA1707 // Identifiers should not contain underscores
- public TimelineInfoLinks? _links { get; set; }
-#pragma warning restore CA1707 // Identifiers should not contain underscores
- }
-
- public class TimelineInfoLinks
- {
- public string Self { get; set; } = default!;
- public string Posts { get; set; } = default!;
- }
-
- public static class TimelineInfoExtensions
- {
- public static TimelineInfo FillLinks(this TimelineInfo info, IUrlHelper urlHelper)
- {
- if (info == null)
- throw new ArgumentNullException(nameof(info));
- if (urlHelper == null)
- throw new ArgumentNullException(nameof(urlHelper));
-
- if (string.IsNullOrEmpty(info.Name))
- {
- info._links = new TimelineInfoLinks
- {
- Self = urlHelper.ActionLink(nameof(PersonalTimelineController.TimelineGet), nameof(PersonalTimelineController)[0..^nameof(Controller).Length], new { info.Owner.Username }),
- Posts = urlHelper.ActionLink(nameof(PersonalTimelineController.PostListGet), nameof(PersonalTimelineController)[0..^nameof(Controller).Length], new { info.Owner.Username })
- };
- }
- else
- {
- info._links = new TimelineInfoLinks
- {
- Self = urlHelper.ActionLink(nameof(TimelineController.TimelineGet), nameof(TimelineController)[0..^nameof(Controller).Length], new { info.Name }),
- Posts = urlHelper.ActionLink(nameof(TimelineController.PostListGet), nameof(TimelineController)[0..^nameof(Controller).Length], new { info.Name })
- };
- }
-
- return info;
- }
- }
-}
diff --git a/Timeline/Models/Http/TimelineController.cs b/Timeline/Models/Http/TimelineController.cs
index 6d461bb9..3e2e6b58 100644
--- a/Timeline/Models/Http/TimelineController.cs
+++ b/Timeline/Models/Http/TimelineController.cs
@@ -4,10 +4,18 @@ using Timeline.Models.Validation;
namespace Timeline.Models.Http
{
+ public class TimelinePostCreateRequestContent
+ {
+ [Required]
+ public string Type { get; set; } = default!;
+ public string? Text { get; set; }
+ public string? Data { get; set; }
+ }
+
public class TimelinePostCreateRequest
{
- [Required(AllowEmptyStrings = true)]
- public string Content { get; set; } = default!;
+ [Required]
+ public TimelinePostCreateRequestContent Content { get; set; } = default!;
public DateTime? Time { get; set; }
}
diff --git a/Timeline/Models/Http/UserInfo.cs b/Timeline/Models/Http/UserInfo.cs
index 68c6d8bd..b4bf14c1 100644
--- a/Timeline/Models/Http/UserInfo.cs
+++ b/Timeline/Models/Http/UserInfo.cs
@@ -2,8 +2,8 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Routing;
+using System;
using Timeline.Controllers;
-using Timeline.Services;
namespace Timeline.Models.Http
{
@@ -12,9 +12,9 @@ namespace Timeline.Models.Http
public string Username { get; set; } = default!;
public string Nickname { get; set; } = default!;
public bool? Administrator { get; set; } = default!;
-#pragma warning disable CA1707
- public UserInfoLinks? _links { get; set; }
-#pragma warning restore CA1707
+#pragma warning disable CA1707 // Identifiers should not contain underscores
+ public UserInfoLinks _links { get; set; } = default!;
+#pragma warning restore CA1707 // Identifiers should not contain underscores
}
public class UserInfoLinks
@@ -24,7 +24,7 @@ namespace Timeline.Models.Http
public string Timeline { get; set; } = default!;
}
- public class UserInfoLinksValueResolver : IValueResolver<User, UserInfo, UserInfoLinks?>
+ public class UserInfoLinksValueResolver : IValueResolver<User, UserInfo, UserInfoLinks>
{
private readonly IActionContextAccessor _actionContextAccessor;
private readonly IUrlHelperFactory _urlHelperFactory;
@@ -35,17 +35,17 @@ namespace Timeline.Models.Http
_urlHelperFactory = urlHelperFactory;
}
- public UserInfoLinks? Resolve(User source, UserInfo destination, UserInfoLinks? destMember, ResolutionContext context)
+ public UserInfoLinks Resolve(User source, UserInfo destination, UserInfoLinks destMember, ResolutionContext context)
{
if (_actionContextAccessor.ActionContext == null)
- return null;
+ throw new InvalidOperationException("No action context, can't fill urls.");
var urlHelper = _urlHelperFactory.GetUrlHelper(_actionContextAccessor.ActionContext);
var result = new UserInfoLinks
{
Self = urlHelper.ActionLink(nameof(UserController.Get), nameof(UserController)[0..^nameof(Controller).Length], new { destination.Username }),
Avatar = urlHelper.ActionLink(nameof(UserAvatarController.Get), nameof(UserAvatarController)[0..^nameof(Controller).Length], new { destination.Username }),
- Timeline = urlHelper.ActionLink(nameof(PersonalTimelineController.TimelineGet), nameof(PersonalTimelineController)[0..^nameof(Controller).Length], new { destination.Username })
+ Timeline = urlHelper.ActionLink(nameof(TimelineController.TimelineGet), nameof(TimelineController)[0..^nameof(Controller).Length], new { Name = "@" + destination.Username })
};
return result;
}
diff --git a/Timeline/Models/Timeline.cs b/Timeline/Models/Timeline.cs
new file mode 100644
index 00000000..803a5c5c
--- /dev/null
+++ b/Timeline/Models/Timeline.cs
@@ -0,0 +1,87 @@
+using System;
+using System.Collections.Generic;
+
+namespace Timeline.Models
+{
+ public enum TimelineVisibility
+ {
+ /// <summary>
+ /// All people including those without accounts.
+ /// </summary>
+ Public,
+ /// <summary>
+ /// Only people signed in.
+ /// </summary>
+ Register,
+ /// <summary>
+ /// Only member.
+ /// </summary>
+ Private
+ }
+
+ public static class TimelinePostContentTypes
+ {
+ public const string Text = "text";
+ public const string Image = "image";
+ }
+
+ public interface ITimelinePostContent
+ {
+ public string Type { get; }
+ }
+
+ public class TextTimelinePostContent : ITimelinePostContent
+ {
+ public TextTimelinePostContent(string text) { Text = text; }
+
+ public string Type { get; } = TimelinePostContentTypes.Text;
+ public string Text { get; set; }
+ }
+
+ public class ImageTimelinePostContent : ITimelinePostContent
+ {
+ public ImageTimelinePostContent(string dataTag) { DataTag = dataTag; }
+
+ public string Type { get; } = TimelinePostContentTypes.Image;
+ public string DataTag { get; set; }
+ }
+
+ public class TimelinePost
+ {
+ public TimelinePost(long id, ITimelinePostContent content, DateTime time, User author, DateTime lastUpdated, string timelineName)
+ {
+ Id = id;
+ Content = content;
+ Time = time;
+ Author = author;
+ LastUpdated = lastUpdated;
+ TimelineName = timelineName;
+ }
+
+ public long Id { get; set; }
+ public ITimelinePostContent Content { get; set; }
+ public DateTime Time { get; set; }
+ public User Author { get; set; }
+ public DateTime LastUpdated { get; set; }
+ public string TimelineName { get; set; }
+ }
+
+#pragma warning disable CA1724 // Type names should not match namespaces
+ public class Timeline
+#pragma warning restore CA1724 // Type names should not match namespaces
+ {
+ public string Name { get; set; } = default!;
+ public string Description { get; set; } = default!;
+ public User Owner { get; set; } = default!;
+ public TimelineVisibility Visibility { get; set; }
+#pragma warning disable CA2227 // Collection properties should be read only
+ public List<User> Members { get; set; } = default!;
+#pragma warning restore CA2227 // Collection properties should be read only
+ }
+
+ public class TimelineChangePropertyRequest
+ {
+ public string? Description { get; set; }
+ public TimelineVisibility? Visibility { get; set; }
+ }
+}
diff --git a/Timeline/Services/User.cs b/Timeline/Models/User.cs
index 09a472e5..37777eba 100644
--- a/Timeline/Services/User.cs
+++ b/Timeline/Models/User.cs
@@ -1,4 +1,4 @@
-namespace Timeline.Services
+namespace Timeline.Models
{
public class User
{
diff --git a/Timeline/Models/Validation/GeneralTimelineNameValidator.cs b/Timeline/Models/Validation/GeneralTimelineNameValidator.cs
new file mode 100644
index 00000000..e1c96fbd
--- /dev/null
+++ b/Timeline/Models/Validation/GeneralTimelineNameValidator.cs
@@ -0,0 +1,33 @@
+using System;
+
+namespace Timeline.Models.Validation
+{
+ public class GeneralTimelineNameValidator : Validator<string>
+ {
+ private readonly UsernameValidator _usernameValidator = new UsernameValidator();
+ private readonly TimelineNameValidator _timelineNameValidator = new TimelineNameValidator();
+
+ protected override (bool, string) DoValidate(string value)
+ {
+ if (value.StartsWith('@'))
+ {
+ return _usernameValidator.Validate(value.Substring(1));
+ }
+ else
+ {
+ return _timelineNameValidator.Validate(value);
+ }
+ }
+ }
+
+ [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter,
+ AllowMultiple = false)]
+ public class GeneralTimelineNameAttribute : ValidateWithAttribute
+ {
+ public GeneralTimelineNameAttribute()
+ : base(typeof(GeneralTimelineNameValidator))
+ {
+
+ }
+ }
+}
diff --git a/Timeline/Resources/Messages.Designer.cs b/Timeline/Resources/Messages.Designer.cs
index 4123cb8b..40c4a1ce 100644
--- a/Timeline/Resources/Messages.Designer.cs
+++ b/Timeline/Resources/Messages.Designer.cs
@@ -151,29 +151,83 @@ namespace Timeline.Resources {
}
/// <summary>
+ /// Looks up a localized string similar to Unknown type of post content..
+ /// </summary>
+ internal static string TimelineController_ContentUnknownType {
+ get {
+ return ResourceManager.GetString("TimelineController_ContentUnknownType", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Data field is not a valid base64 string in image content..
+ /// </summary>
+ internal static string TimelineController_ImageContentDataNotBase64 {
+ get {
+ return ResourceManager.GetString("TimelineController_ImageContentDataNotBase64", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Data field is not a valid image after base64 decoding in image content..
+ /// </summary>
+ internal static string TimelineController_ImageContentDataNotImage {
+ get {
+ return ResourceManager.GetString("TimelineController_ImageContentDataNotImage", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Data field is required for image content..
+ /// </summary>
+ internal static string TimelineController_ImageContentDataRequired {
+ get {
+ return ResourceManager.GetString("TimelineController_ImageContentDataRequired", resourceCulture);
+ }
+ }
+
+ /// <summary>
/// Looks up a localized string similar to The user to set as member does not exist..
/// </summary>
- internal static string TimelineCommon_MemberPut_NotExist {
+ internal static string TimelineController_MemberPut_NotExist {
get {
- return ResourceManager.GetString("TimelineCommon_MemberPut_NotExist", resourceCulture);
+ return ResourceManager.GetString("TimelineController_MemberPut_NotExist", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to A timeline with given name already exists..
/// </summary>
- internal static string TimelineCommon_NameConflict {
+ internal static string TimelineController_NameConflict {
get {
- return ResourceManager.GetString("TimelineCommon_NameConflict", resourceCulture);
+ return ResourceManager.GetString("TimelineController_NameConflict", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The timeline with given name does not exist..
/// </summary>
- internal static string TimelineCommon_NotExist {
+ internal static string TimelineController_NotExist {
get {
- return ResourceManager.GetString("TimelineCommon_NotExist", resourceCulture);
+ return ResourceManager.GetString("TimelineController_NotExist", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The post of that type has no data..
+ /// </summary>
+ internal static string TimelineController_PostNoData {
+ get {
+ return ResourceManager.GetString("TimelineController_PostNoData", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The post to operate on does not exist..
+ /// </summary>
+ internal static string TimelineController_PostNotExist {
+ get {
+ return ResourceManager.GetString("TimelineController_PostNotExist", resourceCulture);
}
}
@@ -196,6 +250,15 @@ namespace Timeline.Resources {
}
/// <summary>
+ /// Looks up a localized string similar to Text field is required for text content..
+ /// </summary>
+ internal static string TimelineController_TextContentTextRequired {
+ get {
+ return ResourceManager.GetString("TimelineController_TextContentTextRequired", resourceCulture);
+ }
+ }
+
+ /// <summary>
/// Looks up a localized string similar to Username or password is invalid..
/// </summary>
internal static string TokenController_Create_BadCredential {
diff --git a/Timeline/Resources/Messages.resx b/Timeline/Resources/Messages.resx
index 865db524..8d5543fe 100644
--- a/Timeline/Resources/Messages.resx
+++ b/Timeline/Resources/Messages.resx
@@ -147,21 +147,42 @@
<data name="Common_InvalidModel" xml:space="preserve">
<value>Model is of bad format.</value>
</data>
- <data name="TimelineCommon_MemberPut_NotExist" xml:space="preserve">
+ <data name="TimelineController_ContentUnknownType" xml:space="preserve">
+ <value>Unknown type of post content.</value>
+ </data>
+ <data name="TimelineController_ImageContentDataNotBase64" xml:space="preserve">
+ <value>Data field is not a valid base64 string in image content.</value>
+ </data>
+ <data name="TimelineController_ImageContentDataNotImage" xml:space="preserve">
+ <value>Data field is not a valid image after base64 decoding in image content.</value>
+ </data>
+ <data name="TimelineController_ImageContentDataRequired" xml:space="preserve">
+ <value>Data field is required for image content.</value>
+ </data>
+ <data name="TimelineController_MemberPut_NotExist" xml:space="preserve">
<value>The user to set as member does not exist.</value>
</data>
- <data name="TimelineCommon_NameConflict" xml:space="preserve">
+ <data name="TimelineController_NameConflict" xml:space="preserve">
<value>A timeline with given name already exists.</value>
</data>
- <data name="TimelineCommon_NotExist" xml:space="preserve">
+ <data name="TimelineController_NotExist" xml:space="preserve">
<value>The timeline with given name does not exist.</value>
</data>
+ <data name="TimelineController_PostNoData" xml:space="preserve">
+ <value>The post of that type has no data.</value>
+ </data>
+ <data name="TimelineController_PostNotExist" xml:space="preserve">
+ <value>The post to operate on does not exist.</value>
+ </data>
<data name="TimelineController_QueryRelateNotExist" xml:space="preserve">
<value>The user specified by query param "relate" does not exist.</value>
</data>
<data name="TimelineController_QueryVisibilityUnknown" xml:space="preserve">
<value>'{0}' is an unkown visibility in the query parameter 'visibility'. </value>
</data>
+ <data name="TimelineController_TextContentTextRequired" xml:space="preserve">
+ <value>Text field is required for text content.</value>
+ </data>
<data name="TokenController_Create_BadCredential" xml:space="preserve">
<value>Username or password is invalid.</value>
</data>
diff --git a/Timeline/Resources/Services/Exception.Designer.cs b/Timeline/Resources/Services/Exception.Designer.cs
index e6806873..0c721d92 100644
--- a/Timeline/Resources/Services/Exception.Designer.cs
+++ b/Timeline/Resources/Services/Exception.Designer.cs
@@ -61,128 +61,128 @@ namespace Timeline.Resources.Services {
}
/// <summary>
- /// Looks up a localized string similar to Avartar is of bad format because {0}..
+ /// Looks up a localized string similar to The password is wrong..
/// </summary>
- internal static string AvatarFormatException {
+ internal static string BadPasswordException {
get {
- return ResourceManager.GetString("AvatarFormatException", resourceCulture);
+ return ResourceManager.GetString("BadPasswordException", resourceCulture);
}
}
/// <summary>
- /// Looks up a localized string similar to image is not a square, aka, width is not equal to height.
+ /// Looks up a localized string similar to A present resource conflicts with the given resource..
/// </summary>
- internal static string AvatarFormatExceptionBadSize {
+ internal static string ConflictException {
get {
- return ResourceManager.GetString("AvatarFormatExceptionBadSize", resourceCulture);
+ return ResourceManager.GetString("ConflictException", resourceCulture);
}
}
/// <summary>
- /// Looks up a localized string similar to failed to decode image, see inner exception.
+ /// Looks up a localized string similar to The hashes password is of bad format. It might not be created by server..
/// </summary>
- internal static string AvatarFormatExceptionCantDecode {
+ internal static string HashedPasswordBadFromatException {
get {
- return ResourceManager.GetString("AvatarFormatExceptionCantDecode", resourceCulture);
+ return ResourceManager.GetString("HashedPasswordBadFromatException", resourceCulture);
}
}
/// <summary>
- /// Looks up a localized string similar to unknown error.
+ /// Looks up a localized string similar to Not of valid base64 format. See inner exception..
/// </summary>
- internal static string AvatarFormatExceptionUnknownError {
+ internal static string HashedPasswordBadFromatExceptionNotBase64 {
get {
- return ResourceManager.GetString("AvatarFormatExceptionUnknownError", resourceCulture);
+ return ResourceManager.GetString("HashedPasswordBadFromatExceptionNotBase64", resourceCulture);
}
}
/// <summary>
- /// Looks up a localized string similar to image&apos;s actual mime type is not the specified one.
+ /// Looks up a localized string similar to Decoded hashed password is of length 0..
/// </summary>
- internal static string AvatarFormatExceptionUnmatchedFormat {
+ internal static string HashedPasswordBadFromatExceptionNotLength0 {
get {
- return ResourceManager.GetString("AvatarFormatExceptionUnmatchedFormat", resourceCulture);
+ return ResourceManager.GetString("HashedPasswordBadFromatExceptionNotLength0", resourceCulture);
}
}
/// <summary>
- /// Looks up a localized string similar to The password is wrong..
+ /// Looks up a localized string similar to See inner exception..
/// </summary>
- internal static string BadPasswordException {
+ internal static string HashedPasswordBadFromatExceptionNotOthers {
get {
- return ResourceManager.GetString("BadPasswordException", resourceCulture);
+ return ResourceManager.GetString("HashedPasswordBadFromatExceptionNotOthers", resourceCulture);
}
}
/// <summary>
- /// Looks up a localized string similar to A present resource conflicts with the given resource..
+ /// Looks up a localized string similar to Salt length &lt; 128 bits..
/// </summary>
- internal static string ConflictException {
+ internal static string HashedPasswordBadFromatExceptionNotSaltTooShort {
get {
- return ResourceManager.GetString("ConflictException", resourceCulture);
+ return ResourceManager.GetString("HashedPasswordBadFromatExceptionNotSaltTooShort", resourceCulture);
}
}
/// <summary>
- /// Looks up a localized string similar to The hashes password is of bad format. It might not be created by server..
+ /// Looks up a localized string similar to Subkey length &lt; 128 bits..
/// </summary>
- internal static string HashedPasswordBadFromatException {
+ internal static string HashedPasswordBadFromatExceptionNotSubkeyTooShort {
get {
- return ResourceManager.GetString("HashedPasswordBadFromatException", resourceCulture);
+ return ResourceManager.GetString("HashedPasswordBadFromatExceptionNotSubkeyTooShort", resourceCulture);
}
}
/// <summary>
- /// Looks up a localized string similar to Not of valid base64 format. See inner exception..
+ /// Looks up a localized string similar to Unknown format marker..
/// </summary>
- internal static string HashedPasswordBadFromatExceptionNotBase64 {
+ internal static string HashedPasswordBadFromatExceptionNotUnknownMarker {
get {
- return ResourceManager.GetString("HashedPasswordBadFromatExceptionNotBase64", resourceCulture);
+ return ResourceManager.GetString("HashedPasswordBadFromatExceptionNotUnknownMarker", resourceCulture);
}
}
/// <summary>
- /// Looks up a localized string similar to Decoded hashed password is of length 0..
+ /// Looks up a localized string similar to Image is in valid because {0}..
/// </summary>
- internal static string HashedPasswordBadFromatExceptionNotLength0 {
+ internal static string ImageException {
get {
- return ResourceManager.GetString("HashedPasswordBadFromatExceptionNotLength0", resourceCulture);
+ return ResourceManager.GetString("ImageException", resourceCulture);
}
}
/// <summary>
- /// Looks up a localized string similar to See inner exception..
+ /// Looks up a localized string similar to image is not a square, aka, width is not equal to height.
/// </summary>
- internal static string HashedPasswordBadFromatExceptionNotOthers {
+ internal static string ImageExceptionBadSize {
get {
- return ResourceManager.GetString("HashedPasswordBadFromatExceptionNotOthers", resourceCulture);
+ return ResourceManager.GetString("ImageExceptionBadSize", resourceCulture);
}
}
/// <summary>
- /// Looks up a localized string similar to Salt length &lt; 128 bits..
+ /// Looks up a localized string similar to failed to decode image, see inner exception.
/// </summary>
- internal static string HashedPasswordBadFromatExceptionNotSaltTooShort {
+ internal static string ImageExceptionCantDecode {
get {
- return ResourceManager.GetString("HashedPasswordBadFromatExceptionNotSaltTooShort", resourceCulture);
+ return ResourceManager.GetString("ImageExceptionCantDecode", resourceCulture);
}
}
/// <summary>
- /// Looks up a localized string similar to Subkey length &lt; 128 bits..
+ /// Looks up a localized string similar to unknown error.
/// </summary>
- internal static string HashedPasswordBadFromatExceptionNotSubkeyTooShort {
+ internal static string ImageExceptionUnknownError {
get {
- return ResourceManager.GetString("HashedPasswordBadFromatExceptionNotSubkeyTooShort", resourceCulture);
+ return ResourceManager.GetString("ImageExceptionUnknownError", resourceCulture);
}
}
/// <summary>
- /// Looks up a localized string similar to Unknown format marker..
+ /// Looks up a localized string similar to image&apos;s actual mime type is not the specified one.
/// </summary>
- internal static string HashedPasswordBadFromatExceptionNotUnknownMarker {
+ internal static string ImageExceptionUnmatchedFormat {
get {
- return ResourceManager.GetString("HashedPasswordBadFromatExceptionNotUnknownMarker", resourceCulture);
+ return ResourceManager.GetString("ImageExceptionUnmatchedFormat", resourceCulture);
}
}
diff --git a/Timeline/Resources/Services/Exception.resx b/Timeline/Resources/Services/Exception.resx
index 11ae5f27..660e5b3d 100644
--- a/Timeline/Resources/Services/Exception.resx
+++ b/Timeline/Resources/Services/Exception.resx
@@ -117,21 +117,6 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
- <data name="AvatarFormatException" xml:space="preserve">
- <value>Avartar is of bad format because {0}.</value>
- </data>
- <data name="AvatarFormatExceptionBadSize" xml:space="preserve">
- <value>image is not a square, aka, width is not equal to height</value>
- </data>
- <data name="AvatarFormatExceptionCantDecode" xml:space="preserve">
- <value>failed to decode image, see inner exception</value>
- </data>
- <data name="AvatarFormatExceptionUnknownError" xml:space="preserve">
- <value>unknown error</value>
- </data>
- <data name="AvatarFormatExceptionUnmatchedFormat" xml:space="preserve">
- <value>image's actual mime type is not the specified one</value>
- </data>
<data name="BadPasswordException" xml:space="preserve">
<value>The password is wrong.</value>
</data>
@@ -159,6 +144,21 @@
<data name="HashedPasswordBadFromatExceptionNotUnknownMarker" xml:space="preserve">
<value>Unknown format marker.</value>
</data>
+ <data name="ImageException" xml:space="preserve">
+ <value>Image is in valid because {0}.</value>
+ </data>
+ <data name="ImageExceptionBadSize" xml:space="preserve">
+ <value>image is not a square, aka, width is not equal to height</value>
+ </data>
+ <data name="ImageExceptionCantDecode" xml:space="preserve">
+ <value>failed to decode image, see inner exception</value>
+ </data>
+ <data name="ImageExceptionUnknownError" xml:space="preserve">
+ <value>unknown error</value>
+ </data>
+ <data name="ImageExceptionUnmatchedFormat" xml:space="preserve">
+ <value>image's actual mime type is not the specified one</value>
+ </data>
<data name="JwtUserTokenBadFormatException" xml:space="preserve">
<value>The token didn't pass verification because {0}.</value>
</data>
diff --git a/Timeline/Resources/Services/TimelineService.Designer.cs b/Timeline/Resources/Services/TimelineService.Designer.cs
index 3ee5959f..4c3de1cd 100644
--- a/Timeline/Resources/Services/TimelineService.Designer.cs
+++ b/Timeline/Resources/Services/TimelineService.Designer.cs
@@ -70,6 +70,15 @@ namespace Timeline.Resources.Services {
}
/// <summary>
+ /// Looks up a localized string similar to Unknown post content type &quot;{0}&quot; is saved in database..
+ /// </summary>
+ internal static string ExceptionDatabaseUnknownContentType {
+ get {
+ return ResourceManager.GetString("ExceptionDatabaseUnknownContentType", resourceCulture);
+ }
+ }
+
+ /// <summary>
/// Looks up a localized string similar to The owner username of personal timeline is of bad format..
/// </summary>
internal static string ExceptionFindTimelineUsernameBadFormat {
@@ -79,6 +88,24 @@ namespace Timeline.Resources.Services {
}
/// <summary>
+ /// Looks up a localized string similar to The data entry of the tag of the image post does not exist..
+ /// </summary>
+ internal static string ExceptionGetDataDataEntryNotExist {
+ get {
+ return ResourceManager.GetString("ExceptionGetDataDataEntryNotExist", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Can&apos;t get data of a non-image post..
+ /// </summary>
+ internal static string ExceptionGetDataNonImagePost {
+ get {
+ return ResourceManager.GetString("ExceptionGetDataNonImagePost", resourceCulture);
+ }
+ }
+
+ /// <summary>
/// Looks up a localized string similar to The timeline name is of bad format because {0}..
/// </summary>
internal static string ExceptionTimelineNameBadFormat {
@@ -95,5 +122,14 @@ namespace Timeline.Resources.Services {
return ResourceManager.GetString("ExceptionTimelineNameConflict", resourceCulture);
}
}
+
+ /// <summary>
+ /// Looks up a localized string similar to Image format type of the post does not exist in column &quot;extra_content&quot;. Normally this couldn&apos;t be possible because it should be saved when post was created. However, we now re-detect the format and save it..
+ /// </summary>
+ internal static string LogGetDataNoFormat {
+ get {
+ return ResourceManager.GetString("LogGetDataNoFormat", resourceCulture);
+ }
+ }
}
}
diff --git a/Timeline/Resources/Services/TimelineService.resx b/Timeline/Resources/Services/TimelineService.resx
index e0d76c9a..97269943 100644
--- a/Timeline/Resources/Services/TimelineService.resx
+++ b/Timeline/Resources/Services/TimelineService.resx
@@ -120,13 +120,25 @@
<data name="ExceptionChangeMemberUsernameBadFormat" xml:space="preserve">
<value>The number {0} username is invalid.</value>
</data>
+ <data name="ExceptionDatabaseUnknownContentType" xml:space="preserve">
+ <value>Unknown post content type "{0}" is saved in database.</value>
+ </data>
<data name="ExceptionFindTimelineUsernameBadFormat" xml:space="preserve">
<value>The owner username of personal timeline is of bad format.</value>
</data>
+ <data name="ExceptionGetDataDataEntryNotExist" xml:space="preserve">
+ <value>The data entry of the tag of the image post does not exist.</value>
+ </data>
+ <data name="ExceptionGetDataNonImagePost" xml:space="preserve">
+ <value>Can't get data of a non-image post.</value>
+ </data>
<data name="ExceptionTimelineNameBadFormat" xml:space="preserve">
<value>The timeline name is of bad format because {0}.</value>
</data>
<data name="ExceptionTimelineNameConflict" xml:space="preserve">
<value>The timeline with given name already exists.</value>
</data>
+ <data name="LogGetDataNoFormat" xml:space="preserve">
+ <value>Image format type of the post does not exist in column "extra_content". Normally this couldn't be possible because it should be saved when post was created. However, we now re-detect the format and save it.</value>
+ </data>
</root> \ No newline at end of file
diff --git a/Timeline/Services/AvatarFormatException.cs b/Timeline/Services/AvatarFormatException.cs
deleted file mode 100644
index 788eabb2..00000000
--- a/Timeline/Services/AvatarFormatException.cs
+++ /dev/null
@@ -1,51 +0,0 @@
-using System;
-using System.Globalization;
-
-namespace Timeline.Services
-{
- /// <summary>
- /// Thrown when avatar is of bad format.
- /// </summary>
- [Serializable]
- public class AvatarFormatException : Exception
- {
- public enum ErrorReason
- {
- /// <summary>
- /// Decoding image failed.
- /// </summary>
- CantDecode,
- /// <summary>
- /// Decoding succeeded but the real type is not the specified type.
- /// </summary>
- UnmatchedFormat,
- /// <summary>
- /// Image is not a square.
- /// </summary>
- BadSize
- }
-
- public AvatarFormatException() : base(MakeMessage(null)) { }
- public AvatarFormatException(string message) : base(message) { }
- public AvatarFormatException(string message, Exception inner) : base(message, inner) { }
-
- public AvatarFormatException(Avatar avatar, ErrorReason error) : base(MakeMessage(error)) { Avatar = avatar; Error = error; }
- public AvatarFormatException(Avatar avatar, ErrorReason error, Exception inner) : base(MakeMessage(error), inner) { Avatar = avatar; Error = error; }
-
- protected AvatarFormatException(
- System.Runtime.Serialization.SerializationInfo info,
- System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
-
- private static string MakeMessage(ErrorReason? reason) =>
- string.Format(CultureInfo.InvariantCulture, Resources.Services.Exception.AvatarFormatException, reason switch
- {
- ErrorReason.CantDecode => Resources.Services.Exception.AvatarFormatExceptionCantDecode,
- ErrorReason.UnmatchedFormat => Resources.Services.Exception.AvatarFormatExceptionUnmatchedFormat,
- ErrorReason.BadSize => Resources.Services.Exception.AvatarFormatExceptionBadSize,
- _ => Resources.Services.Exception.AvatarFormatExceptionUnknownError
- });
-
- public ErrorReason? Error { get; set; }
- public Avatar? Avatar { get; set; }
- }
-}
diff --git a/Timeline/Services/ImageException.cs b/Timeline/Services/ImageException.cs
new file mode 100644
index 00000000..c6126aa3
--- /dev/null
+++ b/Timeline/Services/ImageException.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Globalization;
+
+namespace Timeline.Services
+{
+ [Serializable]
+ public class ImageException : Exception
+ {
+ public enum ErrorReason
+ {
+ /// <summary>
+ /// Decoding image failed.
+ /// </summary>
+ CantDecode,
+ /// <summary>
+ /// Decoding succeeded but the real type is not the specified type.
+ /// </summary>
+ UnmatchedFormat,
+ /// <summary>
+ /// Image is not a square.
+ /// </summary>
+ NotSquare
+ }
+
+ public ImageException() : base(MakeMessage(null)) { }
+ public ImageException(string message) : base(message) { }
+ public ImageException(string message, Exception inner) : base(message, inner) { }
+
+ public ImageException(ErrorReason error, byte[]? data = null, string? requestType = null, string? realType = null) : base(MakeMessage(error)) { Error = error; ImageData = data; RequestType = requestType; RealType = realType; }
+ public ImageException(Exception inner, ErrorReason error, byte[]? data = null, string? requestType = null, string? realType = null) : base(MakeMessage(error), inner) { Error = error; ImageData = data; RequestType = requestType; RealType = realType; }
+
+ protected ImageException(
+ System.Runtime.Serialization.SerializationInfo info,
+ System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
+
+ private static string MakeMessage(ErrorReason? reason) =>
+ string.Format(CultureInfo.InvariantCulture, Resources.Services.Exception.ImageException, reason switch
+ {
+ ErrorReason.CantDecode => Resources.Services.Exception.ImageExceptionCantDecode,
+ ErrorReason.UnmatchedFormat => Resources.Services.Exception.ImageExceptionUnmatchedFormat,
+ ErrorReason.NotSquare => Resources.Services.Exception.ImageExceptionBadSize,
+ _ => Resources.Services.Exception.ImageExceptionUnknownError
+ });
+
+ public ErrorReason? Error { get; }
+#pragma warning disable CA1819 // Properties should not return arrays
+ public byte[]? ImageData { get; }
+#pragma warning restore CA1819 // Properties should not return arrays
+ public string? RequestType { get; }
+
+ // This field will be null if decoding failed.
+ public string? RealType { get; }
+ }
+}
diff --git a/Timeline/Services/ImageValidator.cs b/Timeline/Services/ImageValidator.cs
new file mode 100644
index 00000000..c331d912
--- /dev/null
+++ b/Timeline/Services/ImageValidator.cs
@@ -0,0 +1,53 @@
+using SixLabors.ImageSharp;
+using SixLabors.ImageSharp.Formats;
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Timeline.Services
+{
+ public interface IImageValidator
+ {
+ /// <summary>
+ /// Validate a image data.
+ /// </summary>
+ /// <param name="data">The data of the image. Can't be null.</param>
+ /// <param name="requestType">If not null, the real image format will be check against the requested format and throw if not match. If null, then do not check.</param>
+ /// <param name="square">If true, image must be square.</param>
+ /// <returns>The format.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="data"/> is null.</exception>
+ /// <exception cref="ImageException">Thrown when image data can't be decoded or real type does not match request type or image is not square when required.</exception>
+ Task<IImageFormat> Validate(byte[] data, string? requestType = null, bool square = false);
+ }
+
+ public class ImageValidator : IImageValidator
+ {
+ public ImageValidator()
+ {
+ }
+
+ public async Task<IImageFormat> Validate(byte[] data, string? requestType = null, bool square = false)
+ {
+ if (data == null)
+ throw new ArgumentNullException(nameof(data));
+
+ var format = await Task.Run(() =>
+ {
+ try
+ {
+ using var image = Image.Load(data, out IImageFormat format);
+ if (requestType != null && !format.MimeTypes.Contains(requestType))
+ throw new ImageException(ImageException.ErrorReason.UnmatchedFormat, data, requestType, format.DefaultMimeType);
+ if (square && image.Width != image.Height)
+ throw new ImageException(ImageException.ErrorReason.NotSquare, data, requestType, format.DefaultMimeType);
+ return format;
+ }
+ catch (UnknownImageFormatException e)
+ {
+ throw new ImageException(e, ImageException.ErrorReason.CantDecode, data, requestType, null);
+ }
+ });
+ return format;
+ }
+ }
+}
diff --git a/Timeline/Services/TimelinePostNotExistException.cs b/Timeline/Services/TimelinePostNotExistException.cs
index 97e5d550..c542e63e 100644
--- a/Timeline/Services/TimelinePostNotExistException.cs
+++ b/Timeline/Services/TimelinePostNotExistException.cs
@@ -12,12 +12,17 @@ namespace Timeline.Services
System.Runtime.Serialization.SerializationInfo info,
System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
- public TimelinePostNotExistException(long id) : base(Resources.Services.Exception.TimelinePostNotExistException) { Id = id; }
+ public TimelinePostNotExistException(string timelineName, long id, bool isDelete = false) : base(Resources.Services.Exception.TimelinePostNotExistException) { TimelineName = timelineName; Id = id; IsDelete = isDelete; }
- public TimelinePostNotExistException(long id, string message) : base(message) { Id = id; }
+ public TimelinePostNotExistException(string timelineName, long id, bool isDelete, string message) : base(message) { TimelineName = timelineName; Id = id; IsDelete = isDelete; }
- public TimelinePostNotExistException(long id, string message, Exception inner) : base(message, inner) { Id = id; }
+ public TimelinePostNotExistException(string timelineName, long id, bool isDelete, string message, Exception inner) : base(message, inner) { TimelineName = timelineName; Id = id; IsDelete = isDelete; }
+ public string TimelineName { get; set; } = "";
public long Id { get; set; }
+ /// <summary>
+ /// True if the post is deleted. False if the post does not exist at all.
+ /// </summary>
+ public bool IsDelete { get; set; } = false;
}
}
diff --git a/Timeline/Services/TimelineService.cs b/Timeline/Services/TimelineService.cs
index 379ec8f5..301a1d97 100644
--- a/Timeline/Services/TimelineService.cs
+++ b/Timeline/Services/TimelineService.cs
@@ -1,13 +1,13 @@
-using AutoMapper;
-using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
+using SixLabors.ImageSharp;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Timeline.Entities;
-using Timeline.Models.Http;
+using Timeline.Models;
using Timeline.Models.Validation;
using static Timeline.Resources.Services.TimelineService;
@@ -32,95 +32,118 @@ namespace Timeline.Services
public long UserId { get; set; }
}
+ public class PostData
+ {
+#pragma warning disable CA1819 // Properties should not return arrays
+ public byte[] Data { get; set; } = default!;
+#pragma warning restore CA1819 // Properties should not return arrays
+ public string Type { get; set; } = default!;
+ public string ETag { get; set; } = default!;
+ public DateTime LastModified { get; set; } = default!;
+ }
+
/// <summary>
- /// This define the common interface of both personal timeline
- /// and normal timeline.
+ /// This define the common interface of both personal timeline and normal timeline.
/// </summary>
/// <remarks>
- /// The "name" parameter in method means name of timeline in
- /// <see cref="ITimelineService"/> while username of the owner
- /// of the personal timeline in <see cref="IPersonalTimelineService"/>.
+ /// The "name" parameter in each method has different meaning.
+ /// <see cref="IOrdinaryTimelineService"/> => name of the ordinary timeline
+ /// <see cref="IPersonalTimelineService"/> => username of the owner of the personal timeline
+ /// <see cref="ITimelineService"/> => username if begin with '@' otherwise timeline name
+ ///
+ /// <see cref="ArgumentException"/> is thrown when name is illegal.
+ /// For ordinary timeline, it means the name is not a valid timeline name.
+ /// For personal timeline, it means the name is not a valid username.
+ ///
+ /// <see cref="TimelineNotExistException"> is thrown when timeline does not exist.
+ /// For ordinary timeline, it means the timeline of the name does not exist.
+ /// For personal timeline, it means the user with the username does not exist and the inner exception should be a <see cref="UserNotExistException"/>.
/// </remarks>
public interface IBaseTimelineService
{
/// <summary>
/// Get the timeline info.
/// </summary>
- /// <param name="name">Username or the timeline name. See remarks of <see cref="IBaseTimelineService"/>.</param>
+ /// <param name="name">See remarks of <see cref="IBaseTimelineService"/>.</param>
/// <returns>The timeline info.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> is null.</exception>
- /// <exception cref="ArgumentException">Thrown when <paramref name="name"/> is illegal. It is not a valid timeline name (for normal timeline service) or a valid username (for personal timeline service).</exception>
- /// <exception cref="TimelineNotExistException">
- /// Thrown when timeline does not exist.
- /// For normal timeline, it means the name does not exist.
- /// For personal timeline, it means the user of that username does not exist
- /// and the inner exception should be a <see cref="UserNotExistException"/>.
- /// </exception>
- Task<TimelineInfo> GetTimeline(string name);
+ /// <exception cref="ArgumentException">See remarks of <see cref="IBaseTimelineService"/>.</exception>
+ /// <exception cref="TimelineNotExistException">See remarks of <see cref="IBaseTimelineService"/>.</exception>
+ Task<Models.Timeline> GetTimeline(string name);
/// <summary>
/// Set the properties of a timeline.
/// </summary>
- /// <param name="name">Username or the timeline name. See remarks of <see cref="IBaseTimelineService"/>.</param>
+ /// <param name="name">See remarks of <see cref="IBaseTimelineService"/>.</param>
/// <param name="newProperties">The new properties. Null member means not to change.</param>
- /// <returns>The timeline info.</returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> is null.</exception>
- /// <exception cref="ArgumentException">Thrown when <paramref name="name"/> is illegal. It is not a valid timeline name (for normal timeline service) or a valid username (for personal timeline service).</exception>
- /// <exception cref="TimelineNotExistException">
- /// Thrown when timeline does not exist.
- /// For normal timeline, it means the name does not exist.
- /// For personal timeline, it means the user of that username does not exist
- /// and the inner exception should be a <see cref="UserNotExistException"/>.
- /// </exception>
- Task ChangeProperty(string name, TimelinePatchRequest newProperties);
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> or <paramref name="newProperties"/> is null.</exception>
+ /// <exception cref="ArgumentException">See remarks of <see cref="IBaseTimelineService"/>.</exception>
+ /// <exception cref="TimelineNotExistException">See remarks of <see cref="IBaseTimelineService"/>.</exception>
+ Task ChangeProperty(string name, TimelineChangePropertyRequest newProperties);
/// <summary>
/// Get all the posts in the timeline.
/// </summary>
- /// <param name="name">Username or the timeline name. See remarks of <see cref="IBaseTimelineService"/>.</param>
+ /// <param name="name">See remarks of <see cref="IBaseTimelineService"/>.</param>
/// <returns>A list of all posts.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> is null.</exception>
- /// <exception cref="ArgumentException">Thrown when <paramref name="name"/> is illegal. It is not a valid timeline name (for normal timeline service) or a valid username (for personal timeline service).</exception>
- /// <exception cref="TimelineNotExistException">
- /// Thrown when timeline does not exist.
- /// For normal timeline, it means the name does not exist.
- /// For personal timeline, it means the user of that username does not exist
- /// and the inner exception should be a <see cref="UserNotExistException"/>.
- /// </exception>
- Task<List<TimelinePostInfo>> GetPosts(string name);
+ /// <exception cref="ArgumentException">See remarks of <see cref="IBaseTimelineService"/>.</exception>
+ /// <exception cref="TimelineNotExistException">See remarks of <see cref="IBaseTimelineService"/>.</exception>
+ Task<List<TimelinePost>> GetPosts(string name);
/// <summary>
- /// Create a new post in timeline.
+ /// Get the data of a post.
/// </summary>
- /// <param name="name">Username or the timeline name. See remarks of <ssee cref="IBaseTimelineService"/>.</param>
- /// <param name="authorId">The author's id.</param>
- /// <param name="content">The content.</param>
+ /// <param name="name">See remarks of <see cref="IBaseTimelineService"/>.</param>
+ /// <param name="postId">The id of the post.</param>
+ /// <returns>The data and its type.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> is null.</exception>
+ /// <exception cref="ArgumentException">See remarks of <see cref="IBaseTimelineService"/>.</exception>
+ /// <exception cref="TimelineNotExistException">See remarks of <see cref="IBaseTimelineService"/>.</exception>
+ /// <exception cref="TimelinePostNotExistException">Thrown when post of <paramref name="postId"/> does not exist or has been deleted.</exception>
+ /// <exception cref="InvalidOperationException">Thrown when post has no data. See remarks.</exception>
+ /// <remarks>
+ /// Use this method to retrieve the image of image post.
+ /// </remarks>
+ Task<PostData> GetPostData(string name, long postId);
+
+ /// <summary>
+ /// Create a new text post in timeline.
+ /// </summary>
+ /// <param name="name">See remarks of <see cref="IBaseTimelineService"/>.</param>
+ /// <param name="authorId">The author's user id.</param>
+ /// <param name="text">The content text.</param>
/// <param name="time">The time of the post. If null, then use current time.</param>
/// <returns>The info of the created post.</returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> or <paramref name="content"/> is null.</exception>
- /// <exception cref="ArgumentException">Thrown when <paramref name="name"/> is illegal. It is not a valid timeline name (for normal timeline service) or a valid username (for personal timeline service).</exception>
- /// <exception cref="TimelineNotExistException">
- /// Thrown when timeline does not exist.
- /// For normal timeline, it means the name does not exist.
- /// For personal timeline, it means the user of that username does not exist
- /// and the inner exception should be a <see cref="UserNotExistException"/>.
- /// </exception>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> or <paramref name="text"/> is null.</exception>
+ /// <exception cref="ArgumentException">See remarks of <see cref="IBaseTimelineService"/>.</exception>
+ /// <exception cref="TimelineNotExistException">See remarks of <see cref="IBaseTimelineService"/>.</exception>
/// <exception cref="UserNotExistException">Thrown if user with <paramref name="authorId"/> does not exist.</exception>
- Task<TimelinePostInfo> CreatePost(string name, long authorId, string content, DateTime? time);
+ Task<TimelinePost> CreateTextPost(string name, long authorId, string text, DateTime? time);
/// <summary>
- /// Delete a post
+ /// Create a new image post in timeline.
/// </summary>
- /// <param name="name">Username or the timeline name. See remarks of <see cref="IBaseTimelineService"/>.</param>
+ /// <param name="name">See remarks of <see cref="IBaseTimelineService"/>.</param>
+ /// <param name="authorId">The author's user id.</param>
+ /// <param name="data">The image data.</param>
+ /// <param name="time">The time of the post. If null, then use current time.</param>
+ /// <returns>The info of the created post.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> or <paramref name="data"/> is null.</exception>
+ /// <exception cref="ArgumentException">See remarks of <see cref="IBaseTimelineService"/>.</exception>
+ /// <exception cref="TimelineNotExistException">See remarks of <see cref="IBaseTimelineService"/>.</exception>
+ /// <exception cref="UserNotExistException">Thrown if user with <paramref name="authorId"/> does not exist.</exception>
+ /// <exception cref="ImageException">Thrown if data is not a image. Validated by <see cref="ImageValidator"/>.</exception>
+ Task<TimelinePost> CreateImagePost(string name, long authorId, byte[] data, DateTime? time);
+
+ /// <summary>
+ /// Delete a post.
+ /// </summary>
+ /// <param name="name">See remarks of <see cref="IBaseTimelineService"/>.</param>
/// <param name="id">The id of the post to delete.</param>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> or <paramref name="username"/> is null.</exception>
- /// <exception cref="ArgumentException">Thrown when <paramref name="name"/> is illegal. It is not a valid timeline name (for normal timeline service) or a valid username (for personal timeline service).</exception>
- /// <exception cref="TimelineNotExistException">
- /// Thrown when timeline does not exist.
- /// For normal timeline, it means the name does not exist.
- /// For personal timeline, it means the user of that username does not exist
- /// and the inner exception should be a <see cref="UserNotExistException"/>.
- /// </exception>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> is null.</exception>
+ /// <exception cref="ArgumentException">See remarks of <see cref="IBaseTimelineService"/>.</exception>
+ /// <exception cref="TimelineNotExistException">See remarks of <see cref="IBaseTimelineService"/>.</exception>
/// <exception cref="TimelinePostNotExistException">
/// Thrown when the post with given id does not exist or is deleted already.
/// </exception>
@@ -133,18 +156,13 @@ namespace Timeline.Services
/// <summary>
/// Remove members to a timeline.
/// </summary>
- /// <param name="name">Username or the timeline name. See remarks of <see cref="IBaseTimelineService"/>.</param>
+ /// <param name="name">See remarks of <see cref="IBaseTimelineService"/>.</param>
/// <param name="add">A list of usernames of members to add. May be null.</param>
/// <param name="remove">A list of usernames of members to remove. May be null.</param>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> is null.</exception>
- /// <exception cref="ArgumentException">Thrown when <paramref name="name"/> is illegal. It is not a valid timeline name (for normal timeline service) or a valid username (for personal timeline service).</exception>
+ /// <exception cref="ArgumentException">See remarks of <see cref="IBaseTimelineService"/>.</exception>
+ /// <exception cref="TimelineNotExistException">See remarks of <see cref="IBaseTimelineService"/>.</exception>
/// <exception cref="ArgumentException">Thrown when names in <paramref name="add"/> or <paramref name="remove"/> is not a valid username.</exception>
- /// <exception cref="TimelineNotExistException">
- /// Thrown when timeline does not exist.
- /// For normal timeline, it means the name does not exist.
- /// For personal timeline, it means the user of that username does not exist
- /// and the inner exception should be a <see cref="UserNotExistException"/>.
- /// </exception>
/// <exception cref="UserNotExistException">
/// Thrown when one of the user to change does not exist.
/// </exception>
@@ -160,17 +178,12 @@ namespace Timeline.Services
/// <summary>
/// Check whether a user can manage(change timeline info, member, ...) a timeline.
/// </summary>
- /// <param name="name"></param>
- /// <param name="id"></param>
+ /// <param name="name">See remarks of <see cref="IBaseTimelineService"/>.</param>
+ /// <param name="userId">The user id.</param>
/// <returns>True if the user can manage the timeline, otherwise false.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> is null.</exception>
- /// <exception cref="ArgumentException">Thrown when <paramref name="name"/> is illegal. It is not a valid timeline name (for normal timeline service) or a valid username (for personal timeline service).</exception>
- /// <exception cref="TimelineNotExistException">
- /// Thrown when timeline does not exist.
- /// For normal timeline, it means the name does not exist.
- /// For personal timeline, it means the user of that username does not exist
- /// and the inner exception should be a <see cref="UserNotExistException"/>.
- /// </exception>
+ /// <exception cref="ArgumentException">See remarks of <see cref="IBaseTimelineService"/>.</exception>
+ /// <exception cref="TimelineNotExistException">See remarks of <see cref="IBaseTimelineService"/>.</exception>
/// <remarks>
/// This method does not check whether visitor is administrator.
/// Return false if user with user id does not exist.
@@ -180,17 +193,12 @@ namespace Timeline.Services
/// <summary>
/// Verify whether a visitor has the permission to read a timeline.
/// </summary>
- /// <param name="name">Username or the timeline name. See remarks of <see cref="IBaseTimelineService"/>.</param>
+ /// <param name="name">See remarks of <see cref="IBaseTimelineService"/>.</param>
/// <param name="visitorId">The id of the user to check on. Null means visitor without account.</param>
/// <returns>True if can read, false if can't read.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> is null.</exception>
- /// <exception cref="ArgumentException">Thrown when <paramref name="name"/> is illegal. It is not a valid timeline name (for normal timeline service) or a valid username (for personal timeline service).</exception>
- /// <exception cref="TimelineNotExistException">
- /// Thrown when timeline does not exist.
- /// For normal timeline, it means the name does not exist.
- /// For personal timeline, it means the user of that username does not exist
- /// and the inner exception should be a <see cref="UserNotExistException"/>.
- /// </exception>
+ /// <exception cref="ArgumentException">See remarks of <see cref="IBaseTimelineService"/>.</exception>
+ /// <exception cref="TimelineNotExistException">See remarks of <see cref="IBaseTimelineService"/>.</exception>
/// <remarks>
/// This method does not check whether visitor is administrator.
/// Return false if user with visitor id does not exist.
@@ -200,41 +208,30 @@ namespace Timeline.Services
/// <summary>
/// Verify whether a user has the permission to modify a post.
/// </summary>
- /// <param name="name">Username or the timeline name. See remarks of <see cref="IBaseTimelineService"/>.</param>
+ /// <param name="name">See remarks of <see cref="IBaseTimelineService"/>.</param>
/// <param name="modifierId">The id of the user to check on.</param>
+ /// <param name="throwOnPostNotExist">True if you want it to throw <see cref="TimelinePostNotExistException"/>. Default false.</param>
/// <returns>True if can modify, false if can't modify.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> is null.</exception>
- /// <exception cref="ArgumentException">Thrown when <paramref name="name"/> is illegal. It is not a valid timeline name (for normal timeline service) or a valid username (for personal timeline service).</exception>
- /// <exception cref="TimelineNotExistException">
- /// Thrown when timeline does not exist.
- /// For normal timeline, it means the name does not exist.
- /// For personal timeline, it means the user of that username does not exist
- /// and the inner exception should be a <see cref="UserNotExistException"/>.
- /// </exception>
- /// <exception cref="TimelinePostNotExistException">
- /// Thrown when the post with given id does not exist or is deleted already.
- /// </exception>
+ /// <exception cref="ArgumentException">See remarks of <see cref="IBaseTimelineService"/>.</exception>
+ /// <exception cref="TimelineNotExistException">See remarks of <see cref="IBaseTimelineService"/>.</exception>
+ /// <exception cref="TimelinePostNotExistException">Thrown when the post with given id does not exist or is deleted already and <paramref name="throwOnPostNotExist"/> is true.</exception>
/// <remarks>
/// This method does not check whether the user is administrator.
/// It only checks whether he is the author of the post or the owner of the timeline.
/// Return false when user with modifier id does not exist.
/// </remarks>
- Task<bool> HasPostModifyPermission(string name, long id, long modifierId);
+ Task<bool> HasPostModifyPermission(string name, long id, long modifierId, bool throwOnPostNotExist = false);
/// <summary>
/// Verify whether a user is member of a timeline.
/// </summary>
- /// <param name="name">Username or the timeline name. See remarks of <see cref="IBaseTimelineService"/>.</param>
+ /// <param name="name">See remarks of <see cref="IBaseTimelineService"/>.</param>
/// <param name="userId">The id of user to check on.</param>
/// <returns>True if it is a member, false if not.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> is null.</exception>
- /// <exception cref="ArgumentException">Thrown when <paramref name="name"/> is illegal. It is not a valid timeline name (for normal timeline service) or a valid username (for personal timeline service).</exception>
- /// <exception cref="TimelineNotExistException">
- /// Thrown when timeline does not exist.
- /// For normal timeline, it means the name does not exist.
- /// For personal timeline, it means the user of that username does not exist
- /// and the inner exception should be a <see cref="UserNotExistException"/>.
- /// </exception>
+ /// <exception cref="ArgumentException">See remarks of <see cref="IBaseTimelineService"/>.</exception>
+ /// <exception cref="TimelineNotExistException">See remarks of <see cref="IBaseTimelineService"/>.</exception>
/// <remarks>
/// Timeline owner is also considered as a member.
/// Return false when user with user id does not exist.
@@ -256,7 +253,7 @@ namespace Timeline.Services
/// <remarks>
/// If user with related user id does not exist, empty list will be returned.
/// </remarks>
- Task<List<TimelineInfo>> GetTimelines(TimelineUserRelationship? relate = null, List<TimelineVisibility>? visibility = null);
+ Task<List<Models.Timeline>> GetTimelines(TimelineUserRelationship? relate = null, List<TimelineVisibility>? visibility = null);
/// <summary>
/// Create a timeline.
@@ -268,7 +265,7 @@ namespace Timeline.Services
/// <exception cref="ArgumentException">Thrown when timeline name is invalid.</exception>
/// <exception cref="ConflictException">Thrown when the timeline already exists.</exception>
/// <exception cref="UserNotExistException">Thrown when the owner user does not exist.</exception>
- Task<TimelineInfo> CreateTimeline(string name, long owner);
+ Task<Models.Timeline> CreateTimeline(string name, long owner);
/// <summary>
/// Delete a timeline.
@@ -280,6 +277,11 @@ namespace Timeline.Services
Task DeleteTimeline(string name);
}
+ public interface IOrdinaryTimelineService : IBaseTimelineService
+ {
+
+ }
+
public interface IPersonalTimelineService : IBaseTimelineService
{
@@ -287,36 +289,28 @@ namespace Timeline.Services
public abstract class BaseTimelineService : IBaseTimelineService
{
- protected BaseTimelineService(ILoggerFactory loggerFactory, DatabaseContext database, IUserService userService, IMapper mapper, IClock clock)
+ protected BaseTimelineService(ILoggerFactory loggerFactory, DatabaseContext database, IImageValidator imageValidator, IDataManager dataManager, IUserService userService, IClock clock)
{
+ _logger = loggerFactory.CreateLogger<BaseTimelineService>();
Clock = clock;
Database = database;
+ ImageValidator = imageValidator;
+ DataManager = dataManager;
UserService = userService;
- Mapper = mapper;
}
+ private ILogger<BaseTimelineService> _logger;
+
protected IClock Clock { get; }
protected UsernameValidator UsernameValidator { get; } = new UsernameValidator();
protected DatabaseContext Database { get; }
+ protected IImageValidator ImageValidator { get; }
+ protected IDataManager DataManager { get; }
protected IUserService UserService { get; }
- protected IMapper Mapper { get; }
-
- protected TimelineEntity CreateNewEntity(string? name, long owner)
- {
- return new TimelineEntity
- {
- CurrentPostLocalId = 0,
- Name = name,
- OwnerId = owner,
- Visibility = TimelineVisibility.Register,
- CreateTime = Clock.GetCurrentTime()
- };
- }
-
/// <summary>
/// Find the timeline id by the name.
/// For details, see remarks.
@@ -341,7 +335,9 @@ namespace Timeline.Services
/// </remarks>
protected abstract Task<long> FindTimelineId(string name);
- public async Task<TimelineInfo> GetTimeline(string name)
+ protected abstract string GenerateName(string name);
+
+ public async Task<Models.Timeline> GetTimeline(string name)
{
if (name == null)
throw new ArgumentNullException(nameof(name));
@@ -352,17 +348,17 @@ namespace Timeline.Services
var timelineMemberEntities = await Database.TimelineMembers.Where(m => m.TimelineId == timelineId).Select(m => new { m.UserId }).ToListAsync();
- var owner = Mapper.Map<UserInfo>(await UserService.GetUserById(timelineEntity.OwnerId));
+ var owner = await UserService.GetUserById(timelineEntity.OwnerId);
- var members = new List<UserInfo>();
+ var members = new List<User>();
foreach (var memberEntity in timelineMemberEntities)
{
- members.Add(Mapper.Map<UserInfo>(await UserService.GetUserById(memberEntity.UserId)));
+ members.Add(await UserService.GetUserById(memberEntity.UserId));
}
- return new TimelineInfo
+ return new Models.Timeline
{
- Name = timelineEntity.Name,
+ Name = GenerateName(name),
Description = timelineEntity.Description ?? "",
Owner = owner,
Visibility = timelineEntity.Visibility,
@@ -370,45 +366,100 @@ namespace Timeline.Services
};
}
- public async Task<List<TimelinePostInfo>> GetPosts(string name)
+ public async Task<List<TimelinePost>> GetPosts(string name)
{
if (name == null)
throw new ArgumentNullException(nameof(name));
-
var timelineId = await FindTimelineId(name);
var postEntities = await Database.TimelinePosts.OrderBy(p => p.Time).Where(p => p.TimelineId == timelineId && p.Content != null).ToListAsync();
- var posts = new List<TimelinePostInfo>();
+ var posts = new List<TimelinePost>();
foreach (var entity in postEntities)
{
if (entity.Content != null) // otherwise it is deleted
{
- var author = Mapper.Map<UserInfo>(await UserService.GetUserById(entity.AuthorId));
- posts.Add(new TimelinePostInfo
+ var author = await UserService.GetUserById(entity.AuthorId);
+
+ var type = entity.ContentType;
+
+ ITimelinePostContent content = type switch
{
- Id = entity.LocalId,
- Content = entity.Content,
- Author = author,
- Time = entity.Time,
- LastUpdated = entity.LastUpdated
- });
+ TimelinePostContentTypes.Text => new TextTimelinePostContent(entity.Content),
+ TimelinePostContentTypes.Image => new ImageTimelinePostContent(entity.Content),
+ _ => throw new DatabaseCorruptedException(string.Format(CultureInfo.InvariantCulture, ExceptionDatabaseUnknownContentType, type))
+ };
+
+ posts.Add(new TimelinePost(
+ id: entity.LocalId,
+ content: content,
+ time: entity.Time,
+ author: author,
+ lastUpdated: entity.LastUpdated,
+ timelineName: GenerateName(name)
+ ));
}
}
return posts;
}
+ public async Task<PostData> GetPostData(string name, long postId)
+ {
+ if (name == null)
+ throw new ArgumentNullException(nameof(name));
+
+ var timelineId = await FindTimelineId(name);
+ var postEntity = await Database.TimelinePosts.Where(p => p.LocalId == postId).SingleOrDefaultAsync();
- public async Task<TimelinePostInfo> CreatePost(string name, long authorId, string content, DateTime? time)
+ if (postEntity == null)
+ throw new TimelinePostNotExistException(name, postId);
+
+ if (postEntity.Content == null)
+ throw new TimelinePostNotExistException(name, postId, true);
+
+ if (postEntity.ContentType != TimelinePostContentTypes.Image)
+ throw new InvalidOperationException(ExceptionGetDataNonImagePost);
+
+ var tag = postEntity.Content;
+
+ byte[] data;
+
+ try
+ {
+ data = await DataManager.GetEntry(tag);
+ }
+ catch (InvalidOperationException e)
+ {
+ throw new DatabaseCorruptedException(ExceptionGetDataDataEntryNotExist, e);
+ }
+
+ if (postEntity.ExtraContent == null)
+ {
+ _logger.LogWarning(LogGetDataNoFormat);
+ var format = Image.DetectFormat(data);
+ postEntity.ExtraContent = format.DefaultMimeType;
+ await Database.SaveChangesAsync();
+ }
+
+ return new PostData
+ {
+ Data = data,
+ Type = postEntity.ExtraContent,
+ ETag = tag,
+ LastModified = postEntity.LastUpdated
+ };
+ }
+
+ public async Task<TimelinePost> CreateTextPost(string name, long authorId, string text, DateTime? time)
{
if (name == null)
throw new ArgumentNullException(nameof(name));
- if (content == null)
- throw new ArgumentNullException(nameof(content));
+ if (text == null)
+ throw new ArgumentNullException(nameof(text));
var timelineId = await FindTimelineId(name);
var timelineEntity = await Database.Timelines.Where(t => t.Id == timelineId).SingleAsync();
- var author = Mapper.Map<UserInfo>(await UserService.GetUserById(authorId));
+ var author = await UserService.GetUserById(authorId);
var currentTime = Clock.GetCurrentTime();
var finalTime = time ?? currentTime;
@@ -418,7 +469,8 @@ namespace Timeline.Services
var postEntity = new TimelinePostEntity
{
LocalId = timelineEntity.CurrentPostLocalId,
- Content = content,
+ ContentType = TimelinePostContentTypes.Text,
+ Content = text,
AuthorId = authorId,
TimelineId = timelineId,
Time = finalTime,
@@ -427,14 +479,62 @@ namespace Timeline.Services
Database.TimelinePosts.Add(postEntity);
await Database.SaveChangesAsync();
- return new TimelinePostInfo
+
+ return new TimelinePost(
+ id: postEntity.LocalId,
+ content: new TextTimelinePostContent(text),
+ time: finalTime,
+ author: author,
+ lastUpdated: currentTime,
+ timelineName: GenerateName(name)
+ );
+ }
+
+ public async Task<TimelinePost> CreateImagePost(string name, long authorId, byte[] data, DateTime? time)
+ {
+ if (name == null)
+ throw new ArgumentNullException(nameof(name));
+ if (data == null)
+ throw new ArgumentNullException(nameof(data));
+
+ var timelineId = await FindTimelineId(name);
+ var timelineEntity = await Database.Timelines.Where(t => t.Id == timelineId).SingleAsync();
+
+ var author = await UserService.GetUserById(authorId);
+
+ var imageFormat = await ImageValidator.Validate(data);
+
+ var imageFormatText = imageFormat.DefaultMimeType;
+
+ var tag = await DataManager.RetainEntry(data);
+
+ var currentTime = Clock.GetCurrentTime();
+ var finalTime = time ?? currentTime;
+
+ timelineEntity.CurrentPostLocalId += 1;
+
+ var postEntity = new TimelinePostEntity
{
- Id = postEntity.LocalId,
- Content = content,
- Author = author,
+ LocalId = timelineEntity.CurrentPostLocalId,
+ ContentType = TimelinePostContentTypes.Image,
+ Content = tag,
+ ExtraContent = imageFormatText,
+ AuthorId = authorId,
+ TimelineId = timelineId,
Time = finalTime,
LastUpdated = currentTime
};
+ Database.TimelinePosts.Add(postEntity);
+ await Database.SaveChangesAsync();
+
+ return new TimelinePost(
+ id: postEntity.LocalId,
+ content: new ImageTimelinePostContent(tag),
+ time: finalTime,
+ author: author,
+ lastUpdated: currentTime,
+ timelineName: GenerateName(name)
+ );
}
public async Task DeletePost(string name, long id)
@@ -446,16 +546,28 @@ namespace Timeline.Services
var post = await Database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == id).SingleOrDefaultAsync();
- if (post == null)
- throw new TimelinePostNotExistException(id);
+ if (post == null || post.Content == null)
+ throw new TimelinePostNotExistException(name, id);
+
+ string? dataTag = null;
+
+ if (post.ContentType == TimelinePostContentTypes.Image)
+ {
+ dataTag = post.Content;
+ }
post.Content = null;
post.LastUpdated = Clock.GetCurrentTime();
await Database.SaveChangesAsync();
+
+ if (dataTag != null)
+ {
+ await DataManager.FreeEntry(dataTag);
+ }
}
- public async Task ChangeProperty(string name, TimelinePatchRequest newProperties)
+ public async Task ChangeProperty(string name, TimelineChangePropertyRequest newProperties)
{
if (name == null)
throw new ArgumentNullException(nameof(name));
@@ -592,7 +704,7 @@ namespace Timeline.Services
}
}
- public async Task<bool> HasPostModifyPermission(string name, long id, long modifierId)
+ public async Task<bool> HasPostModifyPermission(string name, long id, long modifierId, bool throwOnPostNotExist = false)
{
if (name == null)
throw new ArgumentNullException(nameof(name));
@@ -603,10 +715,12 @@ namespace Timeline.Services
var postEntity = await Database.TimelinePosts.Where(p => p.Id == id).Select(p => new { p.AuthorId }).SingleOrDefaultAsync();
- if (postEntity == null)
- throw new TimelinePostNotExistException(id);
+ if (postEntity == null && throwOnPostNotExist)
+ {
+ throw new TimelinePostNotExistException(name, id, false);
+ }
- return timelineEntity.OwnerId == modifierId || postEntity.AuthorId == modifierId;
+ return timelineEntity.OwnerId == modifierId || postEntity == null || postEntity.AuthorId == modifierId;
}
public async Task<bool> IsMemberOf(string name, long userId)
@@ -625,7 +739,7 @@ namespace Timeline.Services
}
}
- public class TimelineService : BaseTimelineService, ITimelineService
+ public class OrdinaryTimelineService : BaseTimelineService, IOrdinaryTimelineService
{
private readonly TimelineNameValidator _timelineNameValidator = new TimelineNameValidator();
@@ -637,8 +751,8 @@ namespace Timeline.Services
}
}
- public TimelineService(ILoggerFactory loggerFactory, DatabaseContext database, IUserService userService, IMapper mapper, IClock clock)
- : base(loggerFactory, database, userService, mapper, clock)
+ public OrdinaryTimelineService(ILoggerFactory loggerFactory, DatabaseContext database, IImageValidator imageValidator, IDataManager dataManager, IUserService userService, IClock clock)
+ : base(loggerFactory, database, imageValidator, dataManager, userService, clock)
{
}
@@ -662,7 +776,98 @@ namespace Timeline.Services
}
}
- public async Task<List<TimelineInfo>> GetTimelines(TimelineUserRelationship? relate = null, List<TimelineVisibility>? visibility = null)
+ protected override string GenerateName(string name)
+ {
+ return name;
+ }
+ }
+
+ public class PersonalTimelineService : BaseTimelineService, IPersonalTimelineService
+ {
+ public PersonalTimelineService(ILoggerFactory loggerFactory, DatabaseContext database, IImageValidator imageValidator, IDataManager dataManager, IUserService userService, IClock clock)
+ : base(loggerFactory, database, imageValidator, dataManager, userService, clock)
+ {
+
+ }
+
+ protected override async Task<long> FindTimelineId(string name)
+ {
+ if (name == null)
+ throw new ArgumentNullException(nameof(name));
+
+ long userId;
+ try
+ {
+ userId = await UserService.GetUserIdByUsername(name);
+ }
+ catch (ArgumentException e)
+ {
+ throw new ArgumentException(ExceptionFindTimelineUsernameBadFormat, nameof(name), e);
+ }
+ catch (UserNotExistException e)
+ {
+ throw new TimelineNotExistException(name, e);
+ }
+
+ var timelineEntity = await Database.Timelines.Where(t => t.OwnerId == userId && t.Name == null).Select(t => new { t.Id }).SingleOrDefaultAsync();
+
+ if (timelineEntity != null)
+ {
+ return timelineEntity.Id;
+ }
+ else
+ {
+ var newTimelineEntity = new TimelineEntity
+ {
+ CurrentPostLocalId = 0,
+ Name = null,
+ OwnerId = userId,
+ Visibility = TimelineVisibility.Register,
+ CreateTime = Clock.GetCurrentTime()
+ };
+ Database.Timelines.Add(newTimelineEntity);
+ await Database.SaveChangesAsync();
+
+ return newTimelineEntity.Id;
+ }
+ }
+
+ protected override string GenerateName(string name)
+ {
+ return "@" + name;
+ }
+ }
+
+ public class TimelineService : ITimelineService
+ {
+ private readonly TimelineNameValidator _timelineNameValidator = new TimelineNameValidator();
+
+ private readonly DatabaseContext _database;
+
+ private readonly IUserService _userService;
+ private readonly IClock _clock;
+
+ private readonly IOrdinaryTimelineService _ordinaryTimelineService;
+ private readonly IPersonalTimelineService _personalTimelineService;
+
+ public TimelineService(DatabaseContext database, IUserService userService, IClock clock, IOrdinaryTimelineService ordinaryTimelineService, IPersonalTimelineService personalTimelineService)
+ {
+ _database = database;
+ _userService = userService;
+ _clock = clock;
+ _ordinaryTimelineService = ordinaryTimelineService;
+ _personalTimelineService = personalTimelineService;
+ }
+
+ private void ValidateTimelineName(string name, string paramName)
+ {
+ if (!_timelineNameValidator.Validate(name, out var message))
+ {
+ throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, ExceptionTimelineNameBadFormat, message), paramName);
+ }
+ }
+
+ public async Task<List<Models.Timeline>> GetTimelines(TimelineUserRelationship? relate = null, List<TimelineVisibility>? visibility = null)
{
List<TimelineEntity> entities;
@@ -679,7 +884,7 @@ namespace Timeline.Services
if (relate == null)
{
- entities = await ApplyTimelineVisibilityFilter(Database.Timelines).Include(t => t.Members).ToListAsync();
+ entities = await ApplyTimelineVisibilityFilter(_database.Timelines).Include(t => t.Members).ToListAsync();
}
else
{
@@ -687,31 +892,32 @@ namespace Timeline.Services
if ((relate.Type & TimelineUserRelationshipType.Own) != 0)
{
- entities.AddRange(await ApplyTimelineVisibilityFilter(Database.Timelines.Where(t => t.OwnerId == relate.UserId)).Include(t => t.Members).ToListAsync());
+ entities.AddRange(await ApplyTimelineVisibilityFilter(_database.Timelines.Where(t => t.OwnerId == relate.UserId)).Include(t => t.Members).ToListAsync());
}
if ((relate.Type & TimelineUserRelationshipType.Join) != 0)
{
- entities.AddRange(await ApplyTimelineVisibilityFilter(Database.TimelineMembers.Where(m => m.UserId == relate.UserId).Include(m => m.Timeline).ThenInclude(t => t.Members).Select(m => m.Timeline)).ToListAsync());
+ entities.AddRange(await ApplyTimelineVisibilityFilter(_database.TimelineMembers.Where(m => m.UserId == relate.UserId).Include(m => m.Timeline).ThenInclude(t => t.Members).Select(m => m.Timeline)).ToListAsync());
}
}
- var result = new List<TimelineInfo>();
+ var result = new List<Models.Timeline>();
foreach (var entity in entities)
{
- var timeline = new TimelineInfo
+ var owner = await _userService.GetUserById(entity.OwnerId);
+ var timeline = new Models.Timeline
{
- Name = entity.Name,
+ Name = entity.Name ?? ("@" + owner.Username),
Description = entity.Description ?? "",
- Owner = Mapper.Map<UserInfo>(await UserService.GetUserById(entity.OwnerId)),
+ Owner = owner,
Visibility = entity.Visibility,
- Members = new List<UserInfo>()
+ Members = new List<User>()
};
foreach (var m in entity.Members)
{
- timeline.Members.Add(Mapper.Map<UserInfo>(await UserService.GetUserById(m.UserId)));
+ timeline.Members.Add(await _userService.GetUserById(m.UserId));
}
result.Add(timeline);
@@ -720,31 +926,39 @@ namespace Timeline.Services
return result;
}
- public async Task<TimelineInfo> CreateTimeline(string name, long owner)
+ public async Task<Models.Timeline> CreateTimeline(string name, long owner)
{
if (name == null)
throw new ArgumentNullException(nameof(name));
ValidateTimelineName(name, nameof(name));
- var user = await UserService.GetUserById(owner);
+ var user = await _userService.GetUserById(owner);
- var conflict = await Database.Timelines.AnyAsync(t => t.Name == name);
+ var conflict = await _database.Timelines.AnyAsync(t => t.Name == name);
if (conflict)
throw new ConflictException(ExceptionTimelineNameConflict);
- var newEntity = CreateNewEntity(name, owner);
- Database.Timelines.Add(newEntity);
- await Database.SaveChangesAsync();
+ var newEntity = new TimelineEntity
+ {
+ CurrentPostLocalId = 0,
+ Name = name,
+ OwnerId = owner,
+ Visibility = TimelineVisibility.Register,
+ CreateTime = _clock.GetCurrentTime()
+ };
+
+ _database.Timelines.Add(newEntity);
+ await _database.SaveChangesAsync();
- return new TimelineInfo
+ return new Models.Timeline
{
Name = name,
Description = "",
- Owner = Mapper.Map<UserInfo>(user),
+ Owner = user,
Visibility = newEntity.Visibility,
- Members = new List<UserInfo>()
+ Members = new List<User>()
};
}
@@ -755,57 +969,103 @@ namespace Timeline.Services
ValidateTimelineName(name, nameof(name));
- var entity = await Database.Timelines.Where(t => t.Name == name).SingleOrDefaultAsync();
+ var entity = await _database.Timelines.Where(t => t.Name == name).SingleOrDefaultAsync();
if (entity == null)
throw new TimelineNotExistException(name);
- Database.Timelines.Remove(entity);
- await Database.SaveChangesAsync();
+ _database.Timelines.Remove(entity);
+ await _database.SaveChangesAsync();
}
- }
- public class PersonalTimelineService : BaseTimelineService, IPersonalTimelineService
- {
- public PersonalTimelineService(ILoggerFactory loggerFactory, DatabaseContext database, IUserService userService, IMapper mapper, IClock clock)
- : base(loggerFactory, database, userService, mapper, clock)
- {
- }
-
- protected override async Task<long> FindTimelineId(string name)
+ private IBaseTimelineService BranchName(string name, out string realName)
{
if (name == null)
throw new ArgumentNullException(nameof(name));
- long userId;
- try
+ if (name.StartsWith('@'))
{
- userId = await UserService.GetUserIdByUsername(name);
- }
- catch (ArgumentException e)
- {
- throw new ArgumentException(ExceptionFindTimelineUsernameBadFormat, nameof(name), e);
+ realName = name.Substring(1);
+ return _personalTimelineService;
}
- catch (UserNotExistException e)
+ else
{
- throw new TimelineNotExistException(name, e);
+ realName = name;
+ return _ordinaryTimelineService;
}
+ }
- var timelineEntity = await Database.Timelines.Where(t => t.OwnerId == userId && t.Name == null).Select(t => new { t.Id }).SingleOrDefaultAsync();
+ public Task<Models.Timeline> GetTimeline(string name)
+ {
+ var s = BranchName(name, out var realName);
+ return s.GetTimeline(realName);
+ }
- if (timelineEntity != null)
- {
- return timelineEntity.Id;
- }
- else
- {
- var newTimelineEntity = CreateNewEntity(null, userId);
- Database.Timelines.Add(newTimelineEntity);
- await Database.SaveChangesAsync();
+ public Task ChangeProperty(string name, TimelineChangePropertyRequest newProperties)
+ {
+ var s = BranchName(name, out var realName);
+ return s.ChangeProperty(realName, newProperties);
+ }
- return newTimelineEntity.Id;
- }
+ public Task<List<TimelinePost>> GetPosts(string name)
+ {
+ var s = BranchName(name, out var realName);
+ return s.GetPosts(realName);
+ }
+
+ public Task<PostData> GetPostData(string name, long postId)
+ {
+ var s = BranchName(name, out var realName);
+ return s.GetPostData(realName, postId);
+ }
+
+ public Task<TimelinePost> CreateTextPost(string name, long authorId, string text, DateTime? time)
+ {
+ var s = BranchName(name, out var realName);
+ return s.CreateTextPost(realName, authorId, text, time);
+ }
+
+ public Task<TimelinePost> CreateImagePost(string name, long authorId, byte[] data, DateTime? time)
+ {
+ var s = BranchName(name, out var realName);
+ return s.CreateImagePost(realName, authorId, data, time);
+ }
+
+ public Task DeletePost(string name, long id)
+ {
+ var s = BranchName(name, out var realName);
+ return s.DeletePost(realName, id);
+ }
+
+ public Task ChangeMember(string name, IList<string>? add, IList<string>? remove)
+ {
+ var s = BranchName(name, out var realName);
+ return s.ChangeMember(realName, add, remove);
+ }
+
+ public Task<bool> HasManagePermission(string name, long userId)
+ {
+ var s = BranchName(name, out var realName);
+ return s.HasManagePermission(realName, userId);
+ }
+
+ public Task<bool> HasReadPermission(string name, long? visitorId)
+ {
+ var s = BranchName(name, out var realName);
+ return s.HasReadPermission(realName, visitorId);
+ }
+
+ public Task<bool> HasPostModifyPermission(string name, long id, long modifierId, bool throwOnPostNotExist = false)
+ {
+ var s = BranchName(name, out var realName);
+ return s.HasPostModifyPermission(realName, id, modifierId, throwOnPostNotExist);
+ }
+
+ public Task<bool> IsMemberOf(string name, long userId)
+ {
+ var s = BranchName(name, out var realName);
+ return s.IsMemberOf(realName, userId);
}
}
}
diff --git a/Timeline/Services/UserAvatarService.cs b/Timeline/Services/UserAvatarService.cs
index 52d079a3..1b1be698 100644
--- a/Timeline/Services/UserAvatarService.cs
+++ b/Timeline/Services/UserAvatarService.cs
@@ -1,10 +1,7 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
-using SixLabors.ImageSharp;
-using SixLabors.ImageSharp.Formats;
using System;
using System.IO;
using System.Linq;
@@ -47,16 +44,6 @@ namespace Timeline.Services
Task<AvatarInfo> GetDefaultAvatar();
}
- public interface IUserAvatarValidator
- {
- /// <summary>
- /// Validate a avatar's format and size info.
- /// </summary>
- /// <param name="avatar">The avatar to validate.</param>
- /// <exception cref="AvatarFormatException">Thrown when validation failed.</exception>
- Task Validate(Avatar avatar);
- }
-
public interface IUserAvatarService
{
/// <summary>
@@ -79,7 +66,7 @@ namespace Timeline.Services
/// <param name="id">The id of the user to set avatar for.</param>
/// <param name="avatar">The avatar. Can be null to delete the saved avatar.</param>
/// <exception cref="ArgumentException">Thrown if any field in <paramref name="avatar"/> is null when <paramref name="avatar"/> is not null.</exception>
- /// <exception cref="AvatarFormatException">Thrown if avatar is of bad format.</exception>
+ /// <exception cref="ImageException">Thrown if avatar is of bad format.</exception>
Task SetAvatar(long id, Avatar? avatar);
}
@@ -132,28 +119,6 @@ namespace Timeline.Services
}
}
- public class UserAvatarValidator : IUserAvatarValidator
- {
- public Task Validate(Avatar avatar)
- {
- return Task.Run(() =>
- {
- try
- {
- using var image = Image.Load(avatar.Data, out IImageFormat format);
- if (!format.MimeTypes.Contains(avatar.Type))
- throw new AvatarFormatException(avatar, AvatarFormatException.ErrorReason.UnmatchedFormat);
- if (image.Width != image.Height)
- throw new AvatarFormatException(avatar, AvatarFormatException.ErrorReason.BadSize);
- }
- catch (UnknownImageFormatException e)
- {
- throw new AvatarFormatException(avatar, AvatarFormatException.ErrorReason.CantDecode, e);
- }
- });
- }
- }
-
public class UserAvatarService : IUserAvatarService
{
@@ -162,7 +127,8 @@ namespace Timeline.Services
private readonly DatabaseContext _database;
private readonly IDefaultUserAvatarProvider _defaultUserAvatarProvider;
- private readonly IUserAvatarValidator _avatarValidator;
+
+ private readonly IImageValidator _imageValidator;
private readonly IDataManager _dataManager;
@@ -172,14 +138,14 @@ namespace Timeline.Services
ILogger<UserAvatarService> logger,
DatabaseContext database,
IDefaultUserAvatarProvider defaultUserAvatarProvider,
- IUserAvatarValidator avatarValidator,
+ IImageValidator imageValidator,
IDataManager dataManager,
IClock clock)
{
_logger = logger;
_database = database;
_defaultUserAvatarProvider = defaultUserAvatarProvider;
- _avatarValidator = avatarValidator;
+ _imageValidator = imageValidator;
_dataManager = dataManager;
_clock = clock;
}
@@ -257,7 +223,7 @@ namespace Timeline.Services
}
else
{
- await _avatarValidator.Validate(avatar);
+ await _imageValidator.Validate(avatar.Data, avatar.Type, true);
var tag = await _dataManager.RetainEntry(avatar.Data);
var oldTag = avatarEntity?.DataTag;
var create = avatarEntity == null;
@@ -288,7 +254,6 @@ namespace Timeline.Services
{
services.AddScoped<IUserAvatarService, UserAvatarService>();
services.AddScoped<IDefaultUserAvatarProvider, DefaultUserAvatarProvider>();
- services.AddTransient<IUserAvatarValidator, UserAvatarValidator>();
}
}
}
diff --git a/Timeline/Services/UserService.cs b/Timeline/Services/UserService.cs
index 7dc7159d..e0a5ab50 100644
--- a/Timeline/Services/UserService.cs
+++ b/Timeline/Services/UserService.cs
@@ -6,6 +6,7 @@ using System.Linq;
using System.Threading.Tasks;
using Timeline.Entities;
using Timeline.Helpers;
+using Timeline.Models;
using Timeline.Models.Validation;
using static Timeline.Resources.Services.UserService;
diff --git a/Timeline/Services/UserTokenManager.cs b/Timeline/Services/UserTokenManager.cs
index 4e54c4cd..6decf8f9 100644
--- a/Timeline/Services/UserTokenManager.cs
+++ b/Timeline/Services/UserTokenManager.cs
@@ -1,6 +1,7 @@
using Microsoft.Extensions.Logging;
using System;
using System.Threading.Tasks;
+using Timeline.Models;
namespace Timeline.Services
{
diff --git a/Timeline/Startup.cs b/Timeline/Startup.cs
index 263e6b7a..38bb3164 100644
--- a/Timeline/Startup.cs
+++ b/Timeline/Startup.cs
@@ -47,6 +47,7 @@ namespace Timeline
})
.AddJsonOptions(options =>
{
+ options.JsonSerializerOptions.IgnoreNullValues = true;
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
options.JsonSerializerOptions.Converters.Add(new JsonDateTimeConverter());
})
@@ -97,9 +98,12 @@ namespace Timeline
services.AddScoped<IETagGenerator, ETagGenerator>();
services.AddScoped<IDataManager, DataManager>();
+ services.AddScoped<IImageValidator, ImageValidator>();
+
services.AddUserAvatarService();
services.AddScoped<ITimelineService, TimelineService>();
+ services.AddScoped<IOrdinaryTimelineService, OrdinaryTimelineService>();
services.AddScoped<IPersonalTimelineService, PersonalTimelineService>();
services.TryAddSingleton<IActionContextAccessor, ActionContextAccessor>();
diff --git a/Timeline/Timeline.csproj b/Timeline/Timeline.csproj
index e993c0b3..245ff3e7 100644
--- a/Timeline/Timeline.csproj
+++ b/Timeline/Timeline.csproj
@@ -7,10 +7,6 @@
<LangVersion>8.0</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
- <ItemGroup>
- <Compile Remove="Migrations\20200306101428_AddDataTable.cs" />
- <Compile Remove="Migrations\20200306101428_AddDataTable.Designer.cs" />
- </ItemGroup>
<ItemGroup>
<Content Include="default-avatar.png">
@@ -32,12 +28,12 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
- <PackageReference Include="SixLabors.ImageSharp" Version="1.0.0-dev002868" />
+ <PackageReference Include="SixLabors.ImageSharp" Version="1.0.0-unstable0934" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="5.6.0" />
</ItemGroup>
<ItemGroup>
- <ProjectReference Include="..\Timeline.ErrorCodes\Timeline.ErrorCodes.csproj" />
+ <ProjectReference Include="..\Timeline.ErrorCodes\Timeline.ErrorCodes.csproj" />
</ItemGroup>
<ItemGroup>