aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorcrupest <crupest@outlook.com>2020-03-12 19:56:20 +0800
committerGitHub <noreply@github.com>2020-03-12 19:56:20 +0800
commit904f98bda60b3bd92331aacde3771dedde62d2b5 (patch)
tree70681348ddfc3bc8c3d9a92ae010a02020830573
parenta37874830399c193392cc78367efcecbe8275ceb (diff)
parentf8ff7e20eb5d5673575d36b8964a013765b77bf8 (diff)
downloadtimeline-904f98bda60b3bd92331aacde3771dedde62d2b5.tar.gz
timeline-904f98bda60b3bd92331aacde3771dedde62d2b5.tar.bz2
timeline-904f98bda60b3bd92331aacde3771dedde62d2b5.zip
Merge pull request #69 from crupest/image
Post image feature.
-rw-r--r--Timeline.ErrorCodes/ErrorCodes.cs10
-rw-r--r--Timeline.Tests/Helpers/ResponseAssertions.cs8
-rw-r--r--Timeline.Tests/Helpers/TestApplication.cs31
-rw-r--r--Timeline.Tests/IntegratedTests/IntegratedTestBase.cs84
-rw-r--r--Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs532
-rw-r--r--Timeline.Tests/IntegratedTests/TimelineTest.cs532
-rw-r--r--Timeline.Tests/IntegratedTests/TokenTest.cs1
-rw-r--r--Timeline.Tests/Timeline.Tests.csproj2
-rw-r--r--Timeline.sln4
-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
44 files changed, 2051 insertions, 1392 deletions
diff --git a/Timeline.ErrorCodes/ErrorCodes.cs b/Timeline.ErrorCodes/ErrorCodes.cs
index eca0e18b..0af36383 100644
--- a/Timeline.ErrorCodes/ErrorCodes.cs
+++ b/Timeline.ErrorCodes/ErrorCodes.cs
@@ -56,16 +56,14 @@
public const int BadFormat_BadSize = 1_103_00_03;
}
- public static class TimelineCommon
+ public static class TimelineController
{
public const int NameConflict = 1_104_01_01;
public const int NotExist = 1_104_02_01;
public const int MemberPut_NotExist = 1_104_03_01;
- }
-
- public static class TimelineController
- {
- public const int QueryRelateNotExist = 1_105_01_01;
+ public const int QueryRelateNotExist = 1_104_04_01;
+ public const int PostNotExist = 1_104_05_01;
+ public const int PostNoData = 1_104_05_02;
}
}
}
diff --git a/Timeline.Tests/Helpers/ResponseAssertions.cs b/Timeline.Tests/Helpers/ResponseAssertions.cs
index f01a0677..024732f5 100644
--- a/Timeline.Tests/Helpers/ResponseAssertions.cs
+++ b/Timeline.Tests/Helpers/ResponseAssertions.cs
@@ -33,7 +33,9 @@ namespace Timeline.Tests.Helpers
try
{
- var body = res.Content.ReadAsStringAsync().Result;
+ var task = res.Content.ReadAsStringAsync();
+ task.Wait();
+ var body = task.Result;
if (body.Length > 40)
{
body = body[0..40] + " ...";
@@ -83,7 +85,9 @@ namespace Timeline.Tests.Helpers
string body;
try
{
- body = Subject.Content.ReadAsStringAsync().Result;
+ var task = Subject.Content.ReadAsStringAsync();
+ task.Wait();
+ body = task.Result;
}
catch (AggregateException e)
{
diff --git a/Timeline.Tests/Helpers/TestApplication.cs b/Timeline.Tests/Helpers/TestApplication.cs
index 52c2f2e2..11fe8f87 100644
--- a/Timeline.Tests/Helpers/TestApplication.cs
+++ b/Timeline.Tests/Helpers/TestApplication.cs
@@ -3,29 +3,35 @@ using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
-using System;
using System.Collections.Generic;
using System.IO;
+using System.Threading.Tasks;
using Timeline.Entities;
using Timeline.Migrations;
+using Xunit;
namespace Timeline.Tests.Helpers
{
- public class TestApplication : IDisposable
+ public class TestApplication : IAsyncLifetime
{
- public SqliteConnection DatabaseConnection { get; }
+ public SqliteConnection DatabaseConnection { get; private set; }
- public WebApplicationFactory<Startup> Factory { get; }
+ public WebApplicationFactory<Startup> Factory { get; private set; }
- public string WorkDir { get; }
+ public string WorkDir { get; private set; }
public TestApplication(WebApplicationFactory<Startup> factory)
{
+ Factory = factory;
+ }
+
+ public async Task InitializeAsync()
+ {
WorkDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
Directory.CreateDirectory(WorkDir);
DatabaseConnection = new SqliteConnection("Data Source=:memory:;");
- DatabaseConnection.Open();
+ await DatabaseConnection.OpenAsync();
var options = new DbContextOptionsBuilder<DatabaseContext>()
.UseSqlite(DatabaseConnection)
@@ -33,15 +39,15 @@ namespace Timeline.Tests.Helpers
using (var context = new DatabaseContext(options))
{
- context.Database.EnsureCreated();
+ await context.Database.EnsureCreatedAsync();
context.JwtToken.Add(new JwtTokenEntity
{
Key = JwtTokenGenerateHelper.GenerateKey()
});
- context.SaveChanges();
+ await context.SaveChangesAsync();
}
- Factory = factory.WithWebHostBuilder(builder =>
+ Factory = Factory.WithWebHostBuilder(builder =>
{
builder.ConfigureAppConfiguration((context, config) =>
{
@@ -60,11 +66,10 @@ namespace Timeline.Tests.Helpers
});
}
- public void Dispose()
+ public async Task DisposeAsync()
{
- DatabaseConnection.Close();
- DatabaseConnection.Dispose();
-
+ await DatabaseConnection.CloseAsync();
+ await DatabaseConnection.DisposeAsync();
Directory.Delete(WorkDir, true);
}
}
diff --git a/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs b/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs
index dfde2ea5..a4a7638c 100644
--- a/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs
+++ b/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs
@@ -1,10 +1,13 @@
-using AutoMapper;
-using Microsoft.AspNetCore.Mvc.Testing;
+using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Net.Http;
+using System.Text.Json;
+using System.Text.Json.Serialization;
using System.Threading.Tasks;
+using Timeline.Models;
+using Timeline.Models.Converters;
using Timeline.Models.Http;
using Timeline.Services;
using Timeline.Tests.Helpers;
@@ -12,18 +15,16 @@ using Xunit;
namespace Timeline.Tests.IntegratedTests
{
- public abstract class IntegratedTestBase : IClassFixture<WebApplicationFactory<Startup>>, IDisposable
+ public abstract class IntegratedTestBase : IClassFixture<WebApplicationFactory<Startup>>, IAsyncLifetime
{
- static IntegratedTestBase()
- {
- FluentAssertions.AssertionOptions.AssertEquivalencyUsing(options =>
- options.Excluding(m => m.RuntimeType == typeof(UserInfoLinks)));
- }
-
protected TestApplication TestApp { get; }
protected WebApplicationFactory<Startup> Factory => TestApp.Factory;
+ public IReadOnlyList<UserInfo> UserInfos { get; private set; }
+
+ private readonly int _userCount;
+
public IntegratedTestBase(WebApplicationFactory<Startup> factory) : this(factory, 1)
{
@@ -34,8 +35,30 @@ namespace Timeline.Tests.IntegratedTests
if (userCount < 0)
throw new ArgumentOutOfRangeException(nameof(userCount), userCount, "User count can't be negative.");
- TestApp = new TestApplication(factory);
+ _userCount = userCount;
+ TestApp = new TestApplication(factory);
+ }
+
+ protected virtual Task OnInitializeAsync()
+ {
+ return Task.CompletedTask;
+ }
+
+ protected virtual Task OnDisposeAsync()
+ {
+ return Task.CompletedTask;
+ }
+
+ protected virtual void OnDispose()
+ {
+
+ }
+
+ public async Task InitializeAsync()
+ {
+ await TestApp.InitializeAsync();
+
using (var scope = Factory.Services.CreateScope())
{
var users = new List<User>()
@@ -49,7 +72,7 @@ namespace Timeline.Tests.IntegratedTests
}
};
- for (int i = 1; i <= userCount; i++)
+ for (int i = 1; i <= _userCount; i++)
{
users.Add(new User
{
@@ -63,30 +86,37 @@ namespace Timeline.Tests.IntegratedTests
var userInfoList = new List<UserInfo>();
var userService = scope.ServiceProvider.GetRequiredService<IUserService>();
- var mapper = scope.ServiceProvider.GetRequiredService<IMapper>();
+ foreach (var user in users)
+ {
+ await userService.CreateUser(user);
+ }
+ using var client = await CreateDefaultClient();
+ var options = new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase
+ };
+ options.Converters.Add(new JsonStringEnumConverter());
+ options.Converters.Add(new JsonDateTimeConverter());
foreach (var user in users)
{
- userService.CreateUser(user).Wait();
- userInfoList.Add(mapper.Map<UserInfo>(user));
+ var s = await client.GetStringAsync($"/users/{user.Username}");
+ userInfoList.Add(JsonSerializer.Deserialize<UserInfo>(s, options));
}
UserInfos = userInfoList;
}
+
+ await OnInitializeAsync();
+ }
+
+ public async Task DisposeAsync()
+ {
+ await OnDisposeAsync();
+ OnDispose();
+ await TestApp.DisposeAsync();
}
- protected virtual void OnDispose()
- {
- }
-
- public void Dispose()
- {
- OnDispose();
- TestApp.Dispose();
- }
-
- public IReadOnlyList<UserInfo> UserInfos { get; }
-
public Task<HttpClient> CreateDefaultClient()
{
return Task.FromResult(Factory.CreateDefaultClient());
@@ -121,6 +151,6 @@ namespace Timeline.Tests.IntegratedTests
public Task<HttpClient> CreateClientAsUser()
{
return CreateClientAs(1);
- }
+ }
}
}
diff --git a/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs b/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs
deleted file mode 100644
index 7d0a68e8..00000000
--- a/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs
+++ /dev/null
@@ -1,532 +0,0 @@
-using FluentAssertions;
-using Microsoft.AspNetCore.Mvc.Testing;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Net.Http;
-using System.Threading.Tasks;
-using Timeline.Models.Http;
-using Timeline.Tests.Helpers;
-using Xunit;
-
-namespace Timeline.Tests.IntegratedTests
-{
- public class PersonalTimelineTest : IntegratedTestBase
- {
- public PersonalTimelineTest(WebApplicationFactory<Startup> factory)
- : base(factory, 3)
- {
-
- }
-
- [Fact]
- public async Task TimelineGet_Should_Work()
- {
- using var client = await CreateDefaultClient();
- var res = await client.GetAsync("users/user1/timeline");
- var body = res.Should().HaveStatusCode(200)
- .And.HaveJsonBody<TimelineInfo>().Which;
- body.Owner.Should().BeEquivalentTo(UserInfos[1]);
- body.Visibility.Should().Be(TimelineVisibility.Register);
- body.Description.Should().Be("");
- body.Members.Should().NotBeNull().And.BeEmpty();
- }
-
- [Fact]
- public async Task InvalidModel_BadUsername()
- {
- using var client = await CreateClientAsAdministrator();
- {
- var res = await client.GetAsync("users/user!!!/timeline");
- res.Should().BeInvalidModel();
- }
- {
- var res = await client.PatchAsJsonAsync("users/user!!!/timeline", new TimelinePatchRequest { });
- res.Should().BeInvalidModel();
- }
- {
- var res = await client.PutAsync("users/user!!!/timeline/members/user1", null);
- res.Should().BeInvalidModel();
- }
- {
- var res = await client.DeleteAsync("users/user!!!/timeline/members/user1");
- res.Should().BeInvalidModel();
- }
- {
- var res = await client.GetAsync("users/user!!!/timeline/posts");
- res.Should().BeInvalidModel();
- }
- {
- var res = await client.PostAsJsonAsync("users/user!!!/timeline/posts", new TimelinePostCreateRequest { Content = "aaa" });
- res.Should().BeInvalidModel();
- }
- {
- var res = await client.DeleteAsync("users/user!!!/timeline/posts/123");
- res.Should().BeInvalidModel();
- }
- }
-
- [Fact]
- public async Task NotFound()
- {
- using var client = await CreateClientAsAdministrator();
- {
- var res = await client.GetAsync("users/usernotexist/timeline");
- res.Should().HaveStatusCode(404).And.HaveCommonBody(ErrorCodes.UserCommon.NotExist);
- }
- {
- var res = await client.PatchAsJsonAsync("users/usernotexist/timeline", new TimelinePatchRequest { });
- res.Should().HaveStatusCode(404).And.HaveCommonBody(ErrorCodes.UserCommon.NotExist);
- }
- {
- var res = await client.PutAsync("users/usernotexist/timeline/members/user1", null);
- res.Should().HaveStatusCode(404).And.HaveCommonBody(ErrorCodes.UserCommon.NotExist);
- }
- {
- var res = await client.DeleteAsync("users/usernotexist/timeline/members/user1");
- res.Should().HaveStatusCode(404).And.HaveCommonBody(ErrorCodes.UserCommon.NotExist);
- }
- {
- var res = await client.GetAsync("users/usernotexist/timeline/posts");
- res.Should().HaveStatusCode(404).And.HaveCommonBody(ErrorCodes.UserCommon.NotExist);
- }
- {
- var res = await client.PostAsJsonAsync("users/usernotexist/timeline/posts", new TimelinePostCreateRequest { Content = "aaa" });
- res.Should().HaveStatusCode(404).And.HaveCommonBody(ErrorCodes.UserCommon.NotExist);
- }
- {
- var res = await client.DeleteAsync("users/usernotexist/timeline/posts/123");
- res.Should().HaveStatusCode(404).And.HaveCommonBody(ErrorCodes.UserCommon.NotExist);
- }
- }
-
- [Fact]
- public async Task Description_Should_Work()
- {
- using var client = await CreateClientAsUser();
-
- async Task AssertDescription(string description)
- {
- var res = await client.GetAsync("users/user1/timeline");
- var body = res.Should().HaveStatusCode(200)
- .And.HaveJsonBody<TimelineInfo>()
- .Which.Description.Should().Be(description);
- }
-
- const string mockDescription = "haha";
-
- await AssertDescription("");
- {
- var res = await client.PatchAsJsonAsync("users/user1/timeline",
- new TimelinePatchRequest { Description = mockDescription });
- res.Should().HaveStatusCode(200)
- .And.HaveJsonBody<TimelineInfo>().Which.Description.Should().Be(mockDescription);
- await AssertDescription(mockDescription);
- }
- {
- var res = await client.PatchAsJsonAsync("users/user1/timeline",
- new TimelinePatchRequest { Description = null });
- res.Should().HaveStatusCode(200)
- .And.HaveJsonBody<TimelineInfo>().Which.Description.Should().Be(mockDescription);
- await AssertDescription(mockDescription);
- }
- {
- var res = await client.PatchAsJsonAsync("users/user1/timeline",
- new TimelinePatchRequest { Description = "" });
- res.Should().HaveStatusCode(200)
- .And.HaveJsonBody<TimelineInfo>().Which.Description.Should().Be("");
- await AssertDescription("");
- }
- }
-
- [Fact]
- public async Task Member_Should_Work()
- {
- const string getUrl = "users/user1/timeline";
- using var client = await CreateClientAsUser();
-
- async Task AssertMembers(IList<UserInfo> members)
- {
- var res = await client.GetAsync(getUrl);
- res.Should().HaveStatusCode(200)
- .And.HaveJsonBody<TimelineInfo>()
- .Which.Members.Should().NotBeNull().And.BeEquivalentTo(members);
- }
-
- async Task AssertEmptyMembers()
- {
- var res = await client.GetAsync(getUrl);
- res.Should().HaveStatusCode(200)
- .And.HaveJsonBody<TimelineInfo>()
- .Which.Members.Should().NotBeNull().And.BeEmpty();
- }
-
- await AssertEmptyMembers();
- {
- var res = await client.PutAsync("/users/user1/timeline/members/usernotexist", null);
- res.Should().HaveStatusCode(400)
- .And.HaveCommonBody(ErrorCodes.TimelineCommon.MemberPut_NotExist);
- }
- await AssertEmptyMembers();
- {
- var res = await client.PutAsync("/users/user1/timeline/members/user2", null);
- res.Should().HaveStatusCode(200);
- }
- await AssertMembers(new List<UserInfo> { UserInfos[2] });
- {
- var res = await client.DeleteAsync("/users/user1/timeline/members/user2");
- res.Should().BeDelete(true);
- }
- await AssertEmptyMembers();
- {
- var res = await client.DeleteAsync("/users/user1/timeline/members/users2");
- res.Should().BeDelete(false);
- }
- await AssertEmptyMembers();
- }
-
- [Theory]
- [InlineData(-1, 200, 401, 401, 401, 401)]
- [InlineData(1, 200, 200, 403, 200, 403)]
- [InlineData(0, 200, 200, 200, 200, 200)]
- public async Task Permission_Timeline(int userNumber, int get, int opPatchUser, int opPatchAdmin, int opMemberUser, int opMemberAdmin)
- {
- using var client = await CreateClientAs(userNumber);
- {
- var res = await client.GetAsync("users/user1/timeline");
- res.Should().HaveStatusCode(get);
- }
-
- {
- var res = await client.PatchAsJsonAsync("users/user1/timeline", new TimelinePatchRequest { Description = "hahaha" });
- res.Should().HaveStatusCode(opPatchUser);
- }
-
- {
- var res = await client.PatchAsJsonAsync("users/admin/timeline", new TimelinePatchRequest { Description = "hahaha" });
- res.Should().HaveStatusCode(opPatchAdmin);
- }
-
- {
- var res = await client.PutAsync("users/user1/timeline/members/user2", null);
- res.Should().HaveStatusCode(opMemberUser);
- }
-
- {
- var res = await client.DeleteAsync("users/user1/timeline/members/user2");
- res.Should().HaveStatusCode(opMemberUser);
- }
-
- {
- var res = await client.PutAsync("users/admin/timeline/members/user2", null);
- res.Should().HaveStatusCode(opMemberAdmin);
- }
-
- {
- var res = await client.DeleteAsync("users/admin/timeline/members/user2");
- res.Should().HaveStatusCode(opMemberAdmin);
- }
- }
-
- [Fact]
- public async Task Visibility_Test()
- {
- const string userUrl = "users/user1/timeline/posts";
- const string adminUrl = "users/admin/timeline/posts";
- {
-
- using var client = await CreateClientAsUser();
- using var content = new StringContent(@"{""visibility"":""abcdefg""}", System.Text.Encoding.UTF8, System.Net.Mime.MediaTypeNames.Application.Json);
- var res = await client.PatchAsync("users/user1/timeline", content);
- res.Should().BeInvalidModel();
- }
- { // default visibility is registered
- {
- using var client = await CreateDefaultClient();
- var res = await client.GetAsync(userUrl);
- res.Should().HaveStatusCode(403);
- }
-
- {
- using var client = await CreateClientAsUser();
- var res = await client.GetAsync(adminUrl);
- res.Should().HaveStatusCode(200);
- }
- }
-
- { // change visibility to public
- {
- using var client = await CreateClientAsUser();
- var res = await client.PatchAsJsonAsync("users/user1/timeline",
- new TimelinePatchRequest { Visibility = TimelineVisibility.Public });
- res.Should().HaveStatusCode(200);
- }
- {
- using var client = await CreateDefaultClient();
- var res = await client.GetAsync(userUrl);
- res.Should().HaveStatusCode(200);
- }
- }
-
- { // change visibility to private
- {
- using var client = await CreateClientAsAdministrator();
- {
- var res = await client.PatchAsJsonAsync("users/user1/timeline",
- new TimelinePatchRequest { Visibility = TimelineVisibility.Private });
- res.Should().HaveStatusCode(200);
- }
- {
- var res = await client.PatchAsJsonAsync("users/admin/timeline",
- new TimelinePatchRequest { Visibility = TimelineVisibility.Private });
- res.Should().HaveStatusCode(200);
- }
- }
- {
- using var client = await CreateDefaultClient();
- var res = await client.GetAsync(userUrl);
- res.Should().HaveStatusCode(403);
- }
- { // user can't read admin's
- using var client = await CreateClientAsUser();
- var res = await client.GetAsync(adminUrl);
- res.Should().HaveStatusCode(403);
- }
- { // admin can read user's
- using var client = await CreateClientAsAdministrator();
- var res = await client.GetAsync(userUrl);
- res.Should().HaveStatusCode(200);
- }
- { // add member
- using var client = await CreateClientAsAdministrator();
- var res = await client.PutAsync("/users/admin/timeline/members/user1", null);
- res.Should().HaveStatusCode(200);
- }
- { // now user can read admin's
- using var client = await CreateClientAsUser();
- var res = await client.GetAsync(adminUrl);
- res.Should().HaveStatusCode(200);
- }
- }
- }
-
-
- [Fact]
- public async Task Permission_Post_Create()
- {
- using (var client = await CreateClientAsUser())
- {
- var res = await client.PutAsync("users/user1/timeline/members/user2", null);
- res.Should().HaveStatusCode(200);
- }
-
- using (var client = await CreateDefaultClient())
- {
- { // no auth should get 401
- var res = await client.PostAsJsonAsync("users/user1/timeline/posts",
- new TimelinePostCreateRequest { Content = "aaa" });
- res.Should().HaveStatusCode(401);
- }
- }
-
- using (var client = await CreateClientAsUser())
- {
- { // post self's
- var res = await client.PostAsJsonAsync("users/user1/timeline/posts",
- new TimelinePostCreateRequest { Content = "aaa" });
- res.Should().HaveStatusCode(200);
- }
- { // post other not as a member should get 403
- var res = await client.PostAsJsonAsync("users/admin/timeline/posts",
- new TimelinePostCreateRequest { Content = "aaa" });
- res.Should().HaveStatusCode(403);
- }
- }
-
- using (var client = await CreateClientAsAdministrator())
- {
- { // post as admin
- var res = await client.PostAsJsonAsync("users/user1/timeline/posts",
- new TimelinePostCreateRequest { Content = "aaa" });
- res.Should().HaveStatusCode(200);
- }
- }
-
- using (var client = await CreateClientAs(2))
- {
- { // post as member
- var res = await client.PostAsJsonAsync("users/user1/timeline/posts",
- new TimelinePostCreateRequest { Content = "aaa" });
- res.Should().HaveStatusCode(200);
- }
- }
- }
-
- [Fact]
- public async Task Permission_Post_Delete()
- {
- async Task<long> CreatePost(int userNumber)
- {
- using var client = await CreateClientAs(userNumber);
- var res = await client.PostAsJsonAsync($"users/user1/timeline/posts",
- new TimelinePostCreateRequest { Content = "aaa" });
- return res.Should().HaveStatusCode(200)
- .And.HaveJsonBody<TimelinePostInfo>()
- .Which.Id;
- }
-
- using (var client = await CreateClientAsUser())
- {
- {
- var res = await client.PutAsync("users/user1/timeline/members/user2", null);
- res.Should().HaveStatusCode(200);
- }
- {
- var res = await client.PutAsync("users/user1/timeline/members/user3", null);
- res.Should().HaveStatusCode(200);
- }
- }
-
- { // no auth should get 401
- using var client = await CreateDefaultClient();
- var res = await client.DeleteAsync("users/user1/timeline/posts/12");
- res.Should().HaveStatusCode(401);
- }
-
- { // self can delete self
- var postId = await CreatePost(1);
- using var client = await CreateClientAsUser();
- var res = await client.DeleteAsync($"users/user1/timeline/posts/{postId}");
- res.Should().HaveStatusCode(200);
- }
-
- { // admin can delete any
- var postId = await CreatePost(1);
- using var client = await CreateClientAsAdministrator();
- var res = await client.DeleteAsync($"users/user1/timeline/posts/{postId}");
- res.Should().HaveStatusCode(200);
- }
-
- { // owner can delete other
- var postId = await CreatePost(2);
- using var client = await CreateClientAsUser();
- var res = await client.DeleteAsync($"users/user1/timeline/posts/{postId}");
- res.Should().HaveStatusCode(200);
- }
-
- { // author can delete self
- var postId = await CreatePost(2);
- using var client = await CreateClientAs(2);
- var res = await client.DeleteAsync($"users/user1/timeline/posts/{postId}");
- res.Should().HaveStatusCode(200);
- }
-
- { // otherwise is forbidden
- var postId = await CreatePost(2);
- using var client = await CreateClientAs(3);
- var res = await client.DeleteAsync($"users/user1/timeline/posts/{postId}");
- res.Should().HaveStatusCode(403);
- }
- }
-
- [Fact]
- public async Task Post_Op_Should_Work()
- {
- {
- using var client = await CreateClientAsUser();
- {
- var res = await client.GetAsync("users/user1/timeline/posts");
- res.Should().HaveStatusCode(200)
- .And.HaveJsonBody<TimelinePostInfo[]>()
- .Which.Should().NotBeNull().And.BeEmpty();
- }
- {
- var res = await client.PostAsJsonAsync("users/user1/timeline/posts",
- new TimelinePostCreateRequest { Content = null });
- res.Should().BeInvalidModel();
- }
- const string mockContent = "aaa";
- TimelinePostInfo createRes;
- {
- var res = await client.PostAsJsonAsync("users/user1/timeline/posts",
- new TimelinePostCreateRequest { Content = mockContent });
- var body = res.Should().HaveStatusCode(200)
- .And.HaveJsonBody<TimelinePostInfo>()
- .Which;
- body.Should().NotBeNull();
- body.Content.Should().Be(mockContent);
- body.Author.Should().BeEquivalentTo(UserInfos[1]);
- createRes = body;
- }
- {
- var res = await client.GetAsync("users/user1/timeline/posts");
- res.Should().HaveStatusCode(200)
- .And.HaveJsonBody<TimelinePostInfo[]>()
- .Which.Should().NotBeNull().And.BeEquivalentTo(createRes);
- }
- const string mockContent2 = "bbb";
- var mockTime2 = DateTime.Now.AddDays(-1);
- TimelinePostInfo createRes2;
- {
- var res = await client.PostAsJsonAsync("users/user1/timeline/posts",
- new TimelinePostCreateRequest { Content = mockContent2, Time = mockTime2 });
- var body = res.Should().HaveStatusCode(200)
- .And.HaveJsonBody<TimelinePostInfo>()
- .Which;
- body.Should().NotBeNull();
- body.Content.Should().Be(mockContent2);
- body.Author.Should().BeEquivalentTo(UserInfos[1]);
- body.Time.Should().BeCloseTo(mockTime2, 1000);
- createRes2 = body;
- }
- {
- var res = await client.GetAsync("users/user1/timeline/posts");
- res.Should().HaveStatusCode(200)
- .And.HaveJsonBody<TimelinePostInfo[]>()
- .Which.Should().NotBeNull().And.BeEquivalentTo(createRes, createRes2);
- }
- {
- var res = await client.DeleteAsync($"users/user1/timeline/posts/{createRes.Id}");
- res.Should().BeDelete(true);
- }
- {
- var res = await client.DeleteAsync("users/user1/timeline/posts/30000");
- res.Should().BeDelete(false);
- }
- {
- var res = await client.GetAsync("users/user1/timeline/posts");
- res.Should().HaveStatusCode(200)
- .And.HaveJsonBody<TimelinePostInfo[]>()
- .Which.Should().NotBeNull().And.BeEquivalentTo(createRes2);
- }
- }
- }
-
- [Fact]
- public async Task GetPost_Should_Ordered()
- {
- using var client = await CreateClientAsUser();
-
- async Task<long> CreatePost(DateTime time)
- {
- var res = await client.PostAsJsonAsync("users/user1/timeline/posts",
- new TimelinePostCreateRequest { Content = "aaa", Time = time });
- return res.Should().HaveStatusCode(200)
- .And.HaveJsonBody<TimelinePostInfo>()
- .Which.Id;
- }
-
- var now = DateTime.Now;
- var id0 = await CreatePost(now.AddDays(1));
- var id1 = await CreatePost(now.AddDays(-1));
- var id2 = await CreatePost(now);
-
- {
- var res = await client.GetAsync("users/user1/timeline/posts");
- res.Should().HaveStatusCode(200)
- .And.HaveJsonBody<TimelinePostInfo[]>()
- .Which.Select(p => p.Id).Should().Equal(id1, id2, id0);
- }
- }
- }
-}
diff --git a/Timeline.Tests/IntegratedTests/TimelineTest.cs b/Timeline.Tests/IntegratedTests/TimelineTest.cs
index 14a0a59e..682cfd7c 100644
--- a/Timeline.Tests/IntegratedTests/TimelineTest.cs
+++ b/Timeline.Tests/IntegratedTests/TimelineTest.cs
@@ -1,23 +1,58 @@
using FluentAssertions;
using Microsoft.AspNetCore.Mvc.Testing;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+using SixLabors.ImageSharp;
+using SixLabors.ImageSharp.Formats.Png;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
+using Timeline.Entities;
+using Timeline.Models;
using Timeline.Models.Http;
using Timeline.Tests.Helpers;
using Xunit;
namespace Timeline.Tests.IntegratedTests
{
+ public static class TimelineHelper
+ {
+ public static TimelinePostContentInfo TextPostContent(string text)
+ {
+ return new TimelinePostContentInfo
+ {
+ Type = "text",
+ Text = text
+ };
+ }
+
+ public static TimelinePostCreateRequest TextPostCreateRequest(string text, DateTime? time = null)
+ {
+ return new TimelinePostCreateRequest
+ {
+ Content = new TimelinePostCreateRequestContent
+ {
+ Type = "text",
+ Text = text
+ },
+ Time = time
+ };
+ }
+ }
+
public class TimelineTest : IntegratedTestBase
{
public TimelineTest(WebApplicationFactory<Startup> factory)
: base(factory, 3)
{
+ }
+ protected override async Task OnInitializeAsync()
+ {
+ await CreateTestTimelines();
}
private List<TimelineInfo> _testTimelines;
@@ -35,17 +70,80 @@ namespace Timeline.Tests.IntegratedTests
}
}
+ private static string GeneratePersonalTimelineUrl(int id, string subpath = null)
+ {
+ return $"timelines/@{(id == 0 ? "admin" : ("user" + id))}{(subpath == null ? "" : ("/" + subpath))}";
+ }
+
+ private static string GenerateOrdinaryTimelineUrl(int id, string subpath = null)
+ {
+ return $"timelines/t{id}{(subpath == null ? "" : ("/" + subpath))}";
+ }
+
+ public static IEnumerable<object[]> TimelineUrlGeneratorData()
+ {
+ yield return new[] { new Func<int, string, string>(GeneratePersonalTimelineUrl) };
+ yield return new[] { new Func<int, string, string>(GenerateOrdinaryTimelineUrl) };
+ }
+
+ private static string GeneratePersonalTimelineUrlByName(string name, string subpath = null)
+ {
+ return $"timelines/@{name}{(subpath == null ? "" : ("/" + subpath))}";
+ }
+
+ private static string GenerateOrdinaryTimelineUrlByName(string name, string subpath = null)
+ {
+ return $"timelines/{name}{(subpath == null ? "" : ("/" + subpath))}";
+ }
+
+ public static IEnumerable<object[]> TimelineUrlByNameGeneratorData()
+ {
+ yield return new[] { new Func<string, string, string>(GeneratePersonalTimelineUrlByName) };
+ yield return new[] { new Func<string, string, string>(GenerateOrdinaryTimelineUrlByName) };
+ }
+
[Fact]
- public async Task TimelineList()
+ public async Task TimelineGet_Should_Work()
{
- await CreateTestTimelines();
+ using var client = await CreateDefaultClient();
+ {
+ var res = await client.GetAsync("timelines/@user1");
+ var body = res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelineInfo>().Which;
+ body.Owner.Should().BeEquivalentTo(UserInfos[1]);
+ body.Visibility.Should().Be(TimelineVisibility.Register);
+ body.Description.Should().Be("");
+ body.Members.Should().NotBeNull().And.BeEmpty();
+ var links = body._links;
+ links.Should().NotBeNull();
+ links.Self.Should().EndWith("/timelines/@user1");
+ links.Posts.Should().EndWith("/timelines/@user1/posts");
+ }
+ {
+ var res = await client.GetAsync("timelines/t1");
+ var body = res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelineInfo>().Which;
+ body.Owner.Should().BeEquivalentTo(UserInfos[1]);
+ body.Visibility.Should().Be(TimelineVisibility.Register);
+ body.Description.Should().Be("");
+ body.Members.Should().NotBeNull().And.BeEmpty();
+ var links = body._links;
+ links.Should().NotBeNull();
+ links.Self.Should().EndWith("/timelines/t1");
+ links.Posts.Should().EndWith("/timelines/t1/posts");
+ }
+ }
+
+ [Fact]
+ public async Task TimelineList()
+ {
TimelineInfo user1Timeline;
var client = await CreateDefaultClient();
{
- var res = await client.GetAsync("/users/user1/timeline");
+ var res = await client.GetAsync("/timelines/@user1");
user1Timeline = res.Should().HaveStatusCode(200)
.And.HaveJsonBody<TimelineInfo>().Which;
}
@@ -65,8 +163,6 @@ namespace Timeline.Tests.IntegratedTests
[Fact]
public async Task TimelineList_WithQuery()
{
- await CreateTestTimelines();
-
var testResultRelate = new List<TimelineInfo>();
var testResultOwn = new List<TimelineInfo>();
var testResultJoin = new List<TimelineInfo>();
@@ -80,7 +176,7 @@ namespace Timeline.Tests.IntegratedTests
var client = await CreateClientAsUser();
{
- var res = await client.PutAsync("/users/user1/timeline/members/user3", null);
+ var res = await client.PutAsync("/timelines/@user1/members/user3", null);
res.Should().HaveStatusCode(200);
}
@@ -90,7 +186,7 @@ namespace Timeline.Tests.IntegratedTests
}
{
- var res = await client.PatchAsJsonAsync("/users/user1/timeline", new TimelinePatchRequest { Visibility = TimelineVisibility.Public });
+ var res = await client.PatchAsJsonAsync("/timelines/@user1", new TimelinePatchRequest { Visibility = TimelineVisibility.Public });
res.Should().HaveStatusCode(200);
}
@@ -100,7 +196,7 @@ namespace Timeline.Tests.IntegratedTests
}
{
- var res = await client.GetAsync("/users/user1/timeline");
+ var res = await client.GetAsync("/timelines/@user1");
var timeline = res.Should().HaveStatusCode(200)
.And.HaveJsonBody<TimelineInfo>().Which;
testResultRelate.Add(timeline);
@@ -123,7 +219,7 @@ namespace Timeline.Tests.IntegratedTests
var client = await CreateClientAs(2);
{
- var res = await client.PutAsync("/users/user2/timeline/members/user3", null);
+ var res = await client.PutAsync("/timelines/@user2/members/user3", null);
res.Should().HaveStatusCode(200);
}
@@ -133,7 +229,7 @@ namespace Timeline.Tests.IntegratedTests
}
{
- var res = await client.PatchAsJsonAsync("/users/user2/timeline", new TimelinePatchRequest { Visibility = TimelineVisibility.Register });
+ var res = await client.PatchAsJsonAsync("/timelines/@user2", new TimelinePatchRequest { Visibility = TimelineVisibility.Register });
res.Should().HaveStatusCode(200);
}
@@ -143,7 +239,7 @@ namespace Timeline.Tests.IntegratedTests
}
{
- var res = await client.GetAsync("/users/user2/timeline");
+ var res = await client.GetAsync("/timelines/@user2");
var timeline = res.Should().HaveStatusCode(200)
.And.HaveJsonBody<TimelineInfo>().Which;
testResultRelate.Add(timeline);
@@ -165,7 +261,7 @@ namespace Timeline.Tests.IntegratedTests
var client = await CreateClientAs(3);
{
- var res = await client.PatchAsJsonAsync("/users/user3/timeline", new TimelinePatchRequest { Visibility = TimelineVisibility.Private });
+ var res = await client.PatchAsJsonAsync("/timelines/@user3", new TimelinePatchRequest { Visibility = TimelineVisibility.Private });
res.Should().HaveStatusCode(200);
}
@@ -175,7 +271,7 @@ namespace Timeline.Tests.IntegratedTests
}
{
- var res = await client.GetAsync("/users/user3/timeline");
+ var res = await client.GetAsync("/timelines/@user3");
var timeline = res.Should().HaveStatusCode(200)
.And.HaveJsonBody<TimelineInfo>().Which;
testResultRelate.Add(timeline);
@@ -310,7 +406,7 @@ namespace Timeline.Tests.IntegratedTests
{
var res = await client.PostAsJsonAsync("timelines", new TimelineCreateRequest { Name = "aaa" });
res.Should().HaveStatusCode(400)
- .And.HaveCommonBody(ErrorCodes.TimelineCommon.NameConflict);
+ .And.HaveCommonBody(ErrorCodes.TimelineController.NameConflict);
}
}
}
@@ -318,8 +414,6 @@ namespace Timeline.Tests.IntegratedTests
[Fact]
public async Task TimelineDelete_Should_Work()
{
- await CreateTestTimelines();
-
{
using var client = await CreateDefaultClient();
var res = await client.DeleteAsync("timelines/t1");
@@ -371,84 +465,95 @@ namespace Timeline.Tests.IntegratedTests
}
}
- [Fact]
- public async Task InvalidModel_BadName()
+ [Theory]
+ [MemberData(nameof(TimelineUrlByNameGeneratorData))]
+ public async Task InvalidModel_BadName(Func<string, string, string> generator)
{
using var client = await CreateClientAsAdministrator();
{
- var res = await client.GetAsync("timelines/aaa!!!");
+ var res = await client.GetAsync(generator("aaa!!!", null));
+ res.Should().BeInvalidModel();
+ }
+ {
+ var res = await client.PatchAsJsonAsync(generator("aaa!!!", null), new TimelinePatchRequest { });
res.Should().BeInvalidModel();
}
{
- var res = await client.PatchAsJsonAsync("timelines/aaa!!!", new TimelinePatchRequest { });
+ var res = await client.PutAsync(generator("aaa!!!", "members/user1"), null);
res.Should().BeInvalidModel();
}
{
- var res = await client.PutAsync("timelines/aaa!!!/members/user1", null);
+ var res = await client.DeleteAsync(generator("aaa!!!/members", "user1"));
res.Should().BeInvalidModel();
}
{
- var res = await client.DeleteAsync("timelines/aaa!!!/members/user1");
+ var res = await client.GetAsync(generator("aaa!!!", "posts"));
res.Should().BeInvalidModel();
}
{
- var res = await client.GetAsync("timelines/aaa!!!/posts");
+ var res = await client.PostAsJsonAsync(generator("aaa!!!", "posts"), TimelineHelper.TextPostCreateRequest("aaa"));
res.Should().BeInvalidModel();
}
{
- var res = await client.PostAsJsonAsync("timelines/aaa!!!/posts", new TimelinePostCreateRequest { Content = "aaa" });
+ var res = await client.DeleteAsync(generator("aaa!!!", "posts/123"));
res.Should().BeInvalidModel();
}
{
- var res = await client.DeleteAsync("timelines/aaa!!!/posts/123");
+ var res = await client.GetAsync(generator("aaa!!!", "posts/123/data"));
res.Should().BeInvalidModel();
}
}
- [Fact]
- public async Task NotFound()
+ [Theory]
+ [MemberData(nameof(TimelineUrlByNameGeneratorData))]
+ public async Task Ordinary_NotFound(Func<string, string, string> generator)
{
+ var errorCode = generator == GenerateOrdinaryTimelineUrlByName ? ErrorCodes.TimelineController.NotExist : ErrorCodes.UserCommon.NotExist;
+
using var client = await CreateClientAsAdministrator();
{
- var res = await client.GetAsync("timelines/notexist");
- res.Should().HaveStatusCode(404).And.HaveCommonBody(ErrorCodes.TimelineCommon.NotExist);
+ var res = await client.GetAsync(generator("notexist", null));
+ res.Should().HaveStatusCode(404).And.HaveCommonBody(errorCode);
+ }
+ {
+ var res = await client.PatchAsJsonAsync(generator("notexist", null), new TimelinePatchRequest { });
+ res.Should().HaveStatusCode(404).And.HaveCommonBody(errorCode);
}
{
- var res = await client.PatchAsJsonAsync("timelines/notexist", new TimelinePatchRequest { });
- res.Should().HaveStatusCode(404).And.HaveCommonBody(ErrorCodes.TimelineCommon.NotExist);
+ var res = await client.PutAsync(generator("notexist", "members/user1"), null);
+ res.Should().HaveStatusCode(404).And.HaveCommonBody(errorCode);
}
{
- var res = await client.PutAsync("timelines/notexist/members/user1", null);
- res.Should().HaveStatusCode(404).And.HaveCommonBody(ErrorCodes.TimelineCommon.NotExist);
+ var res = await client.DeleteAsync(generator("notexist", "members/user1"));
+ res.Should().HaveStatusCode(404).And.HaveCommonBody(errorCode);
}
{
- var res = await client.DeleteAsync("timelines/notexist/members/user1");
- res.Should().HaveStatusCode(404).And.HaveCommonBody(ErrorCodes.TimelineCommon.NotExist);
+ var res = await client.GetAsync(generator("notexist", "posts"));
+ res.Should().HaveStatusCode(404).And.HaveCommonBody(errorCode);
}
{
- var res = await client.GetAsync("timelines/notexist/posts");
- res.Should().HaveStatusCode(404).And.HaveCommonBody(ErrorCodes.TimelineCommon.NotExist);
+ var res = await client.PostAsJsonAsync(generator("notexist", "posts"), TimelineHelper.TextPostCreateRequest("aaa"));
+ res.Should().HaveStatusCode(404).And.HaveCommonBody(errorCode);
}
{
- var res = await client.PostAsJsonAsync("timelines/notexist/posts", new TimelinePostCreateRequest { Content = "aaa" });
- res.Should().HaveStatusCode(404).And.HaveCommonBody(ErrorCodes.TimelineCommon.NotExist);
+ var res = await client.DeleteAsync(generator("notexist", "posts/123"));
+ res.Should().HaveStatusCode(404).And.HaveCommonBody(errorCode);
}
{
- var res = await client.DeleteAsync("timelines/notexist/posts/123");
- res.Should().HaveStatusCode(404).And.HaveCommonBody(ErrorCodes.TimelineCommon.NotExist);
+ var res = await client.GetAsync(generator("notexist", "posts/123/data"));
+ res.Should().HaveStatusCode(404).And.HaveCommonBody(errorCode);
}
}
- [Fact]
- public async Task Description_Should_Work()
+ [Theory]
+ [MemberData(nameof(TimelineUrlGeneratorData))]
+ public async Task Description_Should_Work(Func<int, string, string> generator)
{
- await CreateTestTimelines();
-
using var client = await CreateClientAsUser();
async Task AssertDescription(string description)
{
- var res = await client.GetAsync("timelines/t1");
+ var res = await client.GetAsync(generator(1, null));
var body = res.Should().HaveStatusCode(200)
.And.HaveJsonBody<TimelineInfo>()
.Which.Description.Should().Be(description);
@@ -458,21 +563,21 @@ namespace Timeline.Tests.IntegratedTests
await AssertDescription("");
{
- var res = await client.PatchAsJsonAsync("timelines/t1",
+ var res = await client.PatchAsJsonAsync(generator(1, null),
new TimelinePatchRequest { Description = mockDescription });
res.Should().HaveStatusCode(200)
.And.HaveJsonBody<TimelineInfo>().Which.Description.Should().Be(mockDescription);
await AssertDescription(mockDescription);
}
{
- var res = await client.PatchAsJsonAsync("timelines/t1",
+ var res = await client.PatchAsJsonAsync(generator(1, null),
new TimelinePatchRequest { Description = null });
res.Should().HaveStatusCode(200)
.And.HaveJsonBody<TimelineInfo>().Which.Description.Should().Be(mockDescription);
await AssertDescription(mockDescription);
}
{
- var res = await client.PatchAsJsonAsync("timelines/t1",
+ var res = await client.PatchAsJsonAsync(generator(1, null),
new TimelinePatchRequest { Description = "" });
res.Should().HaveStatusCode(200)
.And.HaveJsonBody<TimelineInfo>().Which.Description.Should().Be("");
@@ -480,12 +585,11 @@ namespace Timeline.Tests.IntegratedTests
}
}
- [Fact]
- public async Task Member_Should_Work()
+ [Theory]
+ [MemberData(nameof(TimelineUrlGeneratorData))]
+ public async Task Member_Should_Work(Func<int, string, string> generator)
{
- await CreateTestTimelines();
-
- const string getUrl = "timelines/t1";
+ var getUrl = generator(1, null);
using var client = await CreateClientAsUser();
async Task AssertMembers(IList<UserInfo> members)
@@ -506,35 +610,40 @@ namespace Timeline.Tests.IntegratedTests
await AssertEmptyMembers();
{
- var res = await client.PutAsync("/timelines/t1/members/usernotexist", null);
+ var res = await client.PutAsync(generator(1, "members/usernotexist"), null);
res.Should().HaveStatusCode(400)
- .And.HaveCommonBody(ErrorCodes.TimelineCommon.MemberPut_NotExist);
+ .And.HaveCommonBody(ErrorCodes.TimelineController.MemberPut_NotExist);
}
await AssertEmptyMembers();
{
- var res = await client.PutAsync("/timelines/t1/members/user2", null);
+ var res = await client.PutAsync(generator(1, "members/user2"), null);
res.Should().HaveStatusCode(200);
}
await AssertMembers(new List<UserInfo> { UserInfos[2] });
{
- var res = await client.DeleteAsync("/timelines/t1/members/user2");
+ var res = await client.DeleteAsync(generator(1, "members/user2"));
res.Should().BeDelete(true);
}
await AssertEmptyMembers();
{
- var res = await client.DeleteAsync("/timelines/t1/members/users2");
+ var res = await client.DeleteAsync(generator(1, "members/aaa"));
res.Should().BeDelete(false);
}
await AssertEmptyMembers();
}
[Theory]
- [InlineData(-1, 200, 401, 401, 401, 401)]
- [InlineData(1, 200, 200, 403, 200, 403)]
- [InlineData(0, 200, 200, 200, 200, 200)]
- public async Task Permission_Timeline(int userNumber, int get, int opPatchUser, int opPatchAdmin, int opMemberUser, int opMemberAdmin)
+ [InlineData(nameof(GenerateOrdinaryTimelineUrl), -1, 200, 401, 401, 401, 401)]
+ [InlineData(nameof(GenerateOrdinaryTimelineUrl), 1, 200, 200, 403, 200, 403)]
+ [InlineData(nameof(GenerateOrdinaryTimelineUrl), 0, 200, 200, 200, 200, 200)]
+ [InlineData(nameof(GeneratePersonalTimelineUrl), -1, 200, 401, 401, 401, 401)]
+ [InlineData(nameof(GeneratePersonalTimelineUrl), 1, 200, 200, 403, 200, 403)]
+ [InlineData(nameof(GeneratePersonalTimelineUrl), 0, 200, 200, 200, 200, 200)]
+
+ public async Task Permission_Timeline(string generatorName, int userNumber, int get, int opPatchUser, int opPatchAdmin, int opMemberUser, int opMemberAdmin)
{
- await CreateTestTimelines();
+ var method = GetType().GetMethod(generatorName, System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic);
+ Func<int, string, string> generator = (int id, string subpath) => (string)method.Invoke(null, new object[] { id, subpath });
using var client = await CreateClientAs(userNumber);
{
@@ -543,48 +652,47 @@ namespace Timeline.Tests.IntegratedTests
}
{
- var res = await client.PatchAsJsonAsync("timelines/t1", new TimelinePatchRequest { Description = "hahaha" });
+ var res = await client.PatchAsJsonAsync(generator(1, null), new TimelinePatchRequest { Description = "hahaha" });
res.Should().HaveStatusCode(opPatchUser);
}
{
- var res = await client.PatchAsJsonAsync("timelines/t0", new TimelinePatchRequest { Description = "hahaha" });
+ var res = await client.PatchAsJsonAsync(generator(0, null), new TimelinePatchRequest { Description = "hahaha" });
res.Should().HaveStatusCode(opPatchAdmin);
}
{
- var res = await client.PutAsync("timelines/t1/members/user2", null);
+ var res = await client.PutAsync(generator(1, "members/user2"), null);
res.Should().HaveStatusCode(opMemberUser);
}
{
- var res = await client.DeleteAsync("timelines/t1/members/user2");
+ var res = await client.DeleteAsync(generator(1, "members/user2"));
res.Should().HaveStatusCode(opMemberUser);
}
{
- var res = await client.PutAsync("timelines/t0/members/user2", null);
+ var res = await client.PutAsync(generator(0, "members/user2"), null);
res.Should().HaveStatusCode(opMemberAdmin);
}
{
- var res = await client.DeleteAsync("timelines/t0/members/user2");
+ var res = await client.DeleteAsync(generator(0, "members/user2"));
res.Should().HaveStatusCode(opMemberAdmin);
}
}
- [Fact]
- public async Task Visibility_Test()
+ [Theory]
+ [MemberData(nameof(TimelineUrlGeneratorData))]
+ public async Task Visibility_Test(Func<int, string, string> generator)
{
- await CreateTestTimelines();
-
- const string userUrl = "timelines/t1/posts";
- const string adminUrl = "timelines/t0/posts";
+ var userUrl = generator(1, "posts");
+ var adminUrl = generator(0, "posts");
{
using var client = await CreateClientAsUser();
using var content = new StringContent(@"{""visibility"":""abcdefg""}", System.Text.Encoding.UTF8, System.Net.Mime.MediaTypeNames.Application.Json);
- var res = await client.PatchAsync("timelines/t1", content);
+ var res = await client.PatchAsync(generator(1, null), content);
res.Should().BeInvalidModel();
}
{ // default visibility is registered
@@ -604,7 +712,7 @@ namespace Timeline.Tests.IntegratedTests
{ // change visibility to public
{
using var client = await CreateClientAsUser();
- var res = await client.PatchAsJsonAsync("timelines/t1",
+ var res = await client.PatchAsJsonAsync(generator(1, null),
new TimelinePatchRequest { Visibility = TimelineVisibility.Public });
res.Should().HaveStatusCode(200);
}
@@ -619,12 +727,12 @@ namespace Timeline.Tests.IntegratedTests
{
using var client = await CreateClientAsAdministrator();
{
- var res = await client.PatchAsJsonAsync("timelines/t1",
+ var res = await client.PatchAsJsonAsync(generator(1, null),
new TimelinePatchRequest { Visibility = TimelineVisibility.Private });
res.Should().HaveStatusCode(200);
}
{
- var res = await client.PatchAsJsonAsync("timelines/t0",
+ var res = await client.PatchAsJsonAsync(generator(0, null),
new TimelinePatchRequest { Visibility = TimelineVisibility.Private });
res.Should().HaveStatusCode(200);
}
@@ -646,7 +754,7 @@ namespace Timeline.Tests.IntegratedTests
}
{ // add member
using var client = await CreateClientAsAdministrator();
- var res = await client.PutAsync("/timelines/t0/members/user1", null);
+ var res = await client.PutAsync(generator(0, "members/user1"), null);
res.Should().HaveStatusCode(200);
}
{ // now user can read admin's
@@ -657,23 +765,21 @@ namespace Timeline.Tests.IntegratedTests
}
}
-
- [Fact]
- public async Task Permission_Post_Create()
+ [Theory]
+ [MemberData(nameof(TimelineUrlGeneratorData))]
+ public async Task Permission_Post_Create(Func<int, string, string> generator)
{
- await CreateTestTimelines();
-
using (var client = await CreateClientAsUser())
{
- var res = await client.PutAsync("timelines/t1/members/user2", null);
+ var res = await client.PutAsync(generator(1, "members/user2"), null);
res.Should().HaveStatusCode(200);
}
using (var client = await CreateDefaultClient())
{
{ // no auth should get 401
- var res = await client.PostAsJsonAsync("timelines/t1/posts",
- new TimelinePostCreateRequest { Content = "aaa" });
+ var res = await client.PostAsJsonAsync(generator(1, "posts"),
+ TimelineHelper.TextPostCreateRequest("aaa"));
res.Should().HaveStatusCode(401);
}
}
@@ -681,13 +787,13 @@ namespace Timeline.Tests.IntegratedTests
using (var client = await CreateClientAsUser())
{
{ // post self's
- var res = await client.PostAsJsonAsync("timelines/t1/posts",
- new TimelinePostCreateRequest { Content = "aaa" });
+ var res = await client.PostAsJsonAsync(generator(1, "posts"),
+ TimelineHelper.TextPostCreateRequest("aaa"));
res.Should().HaveStatusCode(200);
}
{ // post other not as a member should get 403
- var res = await client.PostAsJsonAsync("timelines/t0/posts",
- new TimelinePostCreateRequest { Content = "aaa" });
+ var res = await client.PostAsJsonAsync(generator(0, "posts"),
+ TimelineHelper.TextPostCreateRequest("aaa"));
res.Should().HaveStatusCode(403);
}
}
@@ -695,8 +801,8 @@ namespace Timeline.Tests.IntegratedTests
using (var client = await CreateClientAsAdministrator())
{
{ // post as admin
- var res = await client.PostAsJsonAsync("timelines/t1/posts",
- new TimelinePostCreateRequest { Content = "aaa" });
+ var res = await client.PostAsJsonAsync(generator(1, "posts"),
+ TimelineHelper.TextPostCreateRequest("aaa"));
res.Should().HaveStatusCode(200);
}
}
@@ -704,23 +810,22 @@ namespace Timeline.Tests.IntegratedTests
using (var client = await CreateClientAs(2))
{
{ // post as member
- var res = await client.PostAsJsonAsync("timelines/t1/posts",
- new TimelinePostCreateRequest { Content = "aaa" });
+ var res = await client.PostAsJsonAsync(generator(1, "posts"),
+ TimelineHelper.TextPostCreateRequest("aaa"));
res.Should().HaveStatusCode(200);
}
}
}
- [Fact]
- public async Task Permission_Post_Delete()
+ [Theory]
+ [MemberData(nameof(TimelineUrlGeneratorData))]
+ public async Task Permission_Post_Delete(Func<int, string, string> generator)
{
- await CreateTestTimelines();
-
async Task<long> CreatePost(int userNumber)
{
using var client = await CreateClientAs(userNumber);
- var res = await client.PostAsJsonAsync($"timelines/t1/posts",
- new TimelinePostCreateRequest { Content = "aaa" });
+ var res = await client.PostAsJsonAsync(generator(1, "posts"),
+ TimelineHelper.TextPostCreateRequest("aaa"));
return res.Should().HaveStatusCode(200)
.And.HaveJsonBody<TimelinePostInfo>()
.Which.Id;
@@ -729,90 +834,89 @@ namespace Timeline.Tests.IntegratedTests
using (var client = await CreateClientAsUser())
{
{
- var res = await client.PutAsync("timelines/t1/members/user2", null);
+ var res = await client.PutAsync(generator(1, "members/user2"), null);
res.Should().HaveStatusCode(200);
}
{
- var res = await client.PutAsync("timelines/t1/members/user3", null);
+ var res = await client.PutAsync(generator(1, "members/user3"), null);
res.Should().HaveStatusCode(200);
}
}
{ // no auth should get 401
using var client = await CreateDefaultClient();
- var res = await client.DeleteAsync("timelines/t1/posts/12");
+ var res = await client.DeleteAsync(generator(1, "posts/12"));
res.Should().HaveStatusCode(401);
}
{ // self can delete self
var postId = await CreatePost(1);
using var client = await CreateClientAsUser();
- var res = await client.DeleteAsync($"timelines/t1/posts/{postId}");
+ var res = await client.DeleteAsync(generator(1, $"posts/{postId}"));
res.Should().HaveStatusCode(200);
}
{ // admin can delete any
var postId = await CreatePost(1);
using var client = await CreateClientAsAdministrator();
- var res = await client.DeleteAsync($"timelines/t1/posts/{postId}");
+ var res = await client.DeleteAsync(generator(1, $"posts/{postId}"));
res.Should().HaveStatusCode(200);
}
{ // owner can delete other
var postId = await CreatePost(2);
using var client = await CreateClientAsUser();
- var res = await client.DeleteAsync($"timelines/t1/posts/{postId}");
+ var res = await client.DeleteAsync(generator(1, $"posts/{postId}"));
res.Should().HaveStatusCode(200);
}
{ // author can delete self
var postId = await CreatePost(2);
using var client = await CreateClientAs(2);
- var res = await client.DeleteAsync($"timelines/t1/posts/{postId}");
+ var res = await client.DeleteAsync(generator(1, $"posts/{postId}"));
res.Should().HaveStatusCode(200);
}
{ // otherwise is forbidden
var postId = await CreatePost(2);
using var client = await CreateClientAs(3);
- var res = await client.DeleteAsync($"timelines/t1/posts/{postId}");
+ var res = await client.DeleteAsync(generator(1, $"posts/{postId}"));
res.Should().HaveStatusCode(403);
}
}
- [Fact]
- public async Task Post_Op_Should_Work()
+ [Theory]
+ [MemberData(nameof(TimelineUrlGeneratorData))]
+ public async Task TextPost_ShouldWork(Func<int, string, string> generator)
{
- await CreateTestTimelines();
-
{
using var client = await CreateClientAsUser();
{
- var res = await client.GetAsync("timelines/t1/posts");
+ var res = await client.GetAsync(generator(1, "posts"));
res.Should().HaveStatusCode(200)
.And.HaveJsonBody<TimelinePostInfo[]>()
.Which.Should().NotBeNull().And.BeEmpty();
}
{
- var res = await client.PostAsJsonAsync("timelines/t1/posts",
- new TimelinePostCreateRequest { Content = null });
+ var res = await client.PostAsJsonAsync(generator(1, "posts"),
+ TimelineHelper.TextPostCreateRequest(null));
res.Should().BeInvalidModel();
}
const string mockContent = "aaa";
TimelinePostInfo createRes;
{
- var res = await client.PostAsJsonAsync("timelines/t1/posts",
- new TimelinePostCreateRequest { Content = mockContent });
+ var res = await client.PostAsJsonAsync(generator(1, "posts"),
+ TimelineHelper.TextPostCreateRequest(mockContent));
var body = res.Should().HaveStatusCode(200)
.And.HaveJsonBody<TimelinePostInfo>()
.Which;
body.Should().NotBeNull();
- body.Content.Should().Be(mockContent);
+ body.Content.Should().BeEquivalentTo(TimelineHelper.TextPostContent(mockContent));
body.Author.Should().BeEquivalentTo(UserInfos[1]);
createRes = body;
}
{
- var res = await client.GetAsync("timelines/t1/posts");
+ var res = await client.GetAsync(generator(1, "posts"));
res.Should().HaveStatusCode(200)
.And.HaveJsonBody<TimelinePostInfo[]>()
.Which.Should().NotBeNull().And.BeEquivalentTo(createRes);
@@ -821,33 +925,37 @@ namespace Timeline.Tests.IntegratedTests
var mockTime2 = DateTime.Now.AddDays(-1);
TimelinePostInfo createRes2;
{
- var res = await client.PostAsJsonAsync("timelines/t1/posts",
- new TimelinePostCreateRequest { Content = mockContent2, Time = mockTime2 });
+ var res = await client.PostAsJsonAsync(generator(1, "posts"),
+ TimelineHelper.TextPostCreateRequest(mockContent2, mockTime2));
var body = res.Should().HaveStatusCode(200)
.And.HaveJsonBody<TimelinePostInfo>()
.Which;
body.Should().NotBeNull();
- body.Content.Should().Be(mockContent2);
+ body.Content.Should().BeEquivalentTo(TimelineHelper.TextPostContent(mockContent2));
body.Author.Should().BeEquivalentTo(UserInfos[1]);
body.Time.Should().BeCloseTo(mockTime2, 1000);
createRes2 = body;
}
{
- var res = await client.GetAsync("timelines/t1/posts");
+ var res = await client.GetAsync(generator(1, "posts"));
res.Should().HaveStatusCode(200)
.And.HaveJsonBody<TimelinePostInfo[]>()
.Which.Should().NotBeNull().And.BeEquivalentTo(createRes, createRes2);
}
{
- var res = await client.DeleteAsync($"timelines/t1/posts/{createRes.Id}");
+ var res = await client.DeleteAsync(generator(1, $"posts/{createRes.Id}"));
res.Should().BeDelete(true);
}
{
- var res = await client.DeleteAsync("timelines/t1/posts/30000");
+ var res = await client.DeleteAsync(generator(1, $"posts/{createRes.Id}"));
+ res.Should().BeDelete(false);
+ }
+ {
+ var res = await client.DeleteAsync(generator(1, "posts/30000"));
res.Should().BeDelete(false);
}
{
- var res = await client.GetAsync("timelines/t1/posts");
+ var res = await client.GetAsync(generator(1, "posts"));
res.Should().HaveStatusCode(200)
.And.HaveJsonBody<TimelinePostInfo[]>()
.Which.Should().NotBeNull().And.BeEquivalentTo(createRes2);
@@ -855,17 +963,16 @@ namespace Timeline.Tests.IntegratedTests
}
}
- [Fact]
- public async Task GetPost_Should_Ordered()
+ [Theory]
+ [MemberData(nameof(TimelineUrlGeneratorData))]
+ public async Task GetPost_Should_Ordered(Func<int, string, string> generator)
{
- await CreateTestTimelines();
-
using var client = await CreateClientAsUser();
async Task<long> CreatePost(DateTime time)
{
- var res = await client.PostAsJsonAsync("timelines/t1/posts",
- new TimelinePostCreateRequest { Content = "aaa", Time = time });
+ var res = await client.PostAsJsonAsync(generator(1, "posts"),
+ TimelineHelper.TextPostCreateRequest("aaa", time));
return res.Should().HaveStatusCode(200)
.And.HaveJsonBody<TimelinePostInfo>()
.Which.Id;
@@ -877,11 +984,164 @@ namespace Timeline.Tests.IntegratedTests
var id2 = await CreatePost(now);
{
- var res = await client.GetAsync("timelines/t1/posts");
+ var res = await client.GetAsync(generator(1, "posts"));
res.Should().HaveStatusCode(200)
.And.HaveJsonBody<TimelinePostInfo[]>()
.Which.Select(p => p.Id).Should().Equal(id1, id2, id0);
}
}
+
+ [Theory]
+ [MemberData(nameof(TimelineUrlGeneratorData))]
+ public async Task CreatePost_InvalidModel(Func<int, string, string> generator)
+ {
+ using var client = await CreateClientAsUser();
+
+ {
+ var res = await client.PostAsJsonAsync(generator(1, "posts"), new TimelinePostCreateRequest { Content = null });
+ res.Should().BeInvalidModel();
+ }
+
+ {
+ var res = await client.PostAsJsonAsync(generator(1, "posts"), new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Type = null } });
+ res.Should().BeInvalidModel();
+ }
+
+ {
+ var res = await client.PostAsJsonAsync(generator(1, "posts"), new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Type = "hahaha" } });
+ res.Should().BeInvalidModel();
+ }
+
+ {
+ var res = await client.PostAsJsonAsync(generator(1, "posts"), new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Type = "text", Text = null } });
+ res.Should().BeInvalidModel();
+ }
+
+ {
+ var res = await client.PostAsJsonAsync(generator(1, "posts"), new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Type = "image", Data = null } });
+ res.Should().BeInvalidModel();
+ }
+
+ {
+ // image not base64
+ var res = await client.PostAsJsonAsync(generator(1, "posts"), new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Type = "image", Data = "!!!" } });
+ res.Should().BeInvalidModel();
+ }
+
+ {
+ // image base64 not image
+ var res = await client.PostAsJsonAsync(generator(1, "posts"), new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Type = "image", Data = Convert.ToBase64String(new byte[] { 0x01, 0x02, 0x03 }) } });
+ res.Should().BeInvalidModel();
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(TimelineUrlGeneratorData))]
+ public async Task ImagePost_ShouldWork(Func<int, string, string> generator)
+ {
+ var imageData = ImageHelper.CreatePngWithSize(100, 200);
+
+ long postId;
+ string postImageUrl;
+
+ void AssertPostContent(TimelinePostContentInfo content)
+ {
+ content.Type.Should().Be(TimelinePostContentTypes.Image);
+ content.Url.Should().EndWith(generator(1, $"posts/{postId}/data"));
+ content.Text.Should().Be(null);
+ }
+
+ using var client = await CreateClientAsUser();
+
+ {
+ var res = await client.PostAsJsonAsync(generator(1, "posts"),
+ new TimelinePostCreateRequest
+ {
+ Content = new TimelinePostCreateRequestContent
+ {
+ Type = TimelinePostContentTypes.Image,
+ Data = Convert.ToBase64String(imageData)
+ }
+ });
+ var body = res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelinePostInfo>().Which;
+ postId = body.Id;
+ postImageUrl = body.Content.Url;
+ AssertPostContent(body.Content);
+ }
+
+ {
+ var res = await client.GetAsync(generator(1, "posts"));
+ var body = res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelinePostInfo[]>().Which;
+ body.Should().HaveCount(1);
+ var post = body[0];
+ post.Id.Should().Be(postId);
+ AssertPostContent(post.Content);
+ }
+
+ {
+ var res = await client.GetAsync(generator(1, $"posts/{postId}/data"));
+ res.Content.Headers.ContentType.MediaType.Should().Be("image/png");
+ var data = await res.Content.ReadAsByteArrayAsync();
+ var image = Image.Load(data, out var format);
+ image.Width.Should().Be(100);
+ image.Height.Should().Be(200);
+ format.Name.Should().Be(PngFormat.Instance.Name);
+ }
+
+ {
+ var res = await client.DeleteAsync(generator(1, $"posts/{postId}"));
+ res.Should().BeDelete(true);
+ }
+
+ {
+ var res = await client.DeleteAsync(generator(1, $"posts/{postId}"));
+ res.Should().BeDelete(false);
+ }
+
+ {
+ var res = await client.GetAsync(generator(1, "posts"));
+ res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelinePostInfo[]>()
+ .Which.Should().BeEmpty();
+ }
+
+ {
+ using var scope = TestApp.Factory.Services.CreateScope();
+ var database = scope.ServiceProvider.GetRequiredService<DatabaseContext>();
+ var count = await database.Data.CountAsync();
+ count.Should().Be(0);
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(TimelineUrlGeneratorData))]
+ public async Task ImagePost_400(Func<int, string, string> generator)
+ {
+ using var client = await CreateClientAsUser();
+
+ {
+ var res = await client.GetAsync(generator(1, "posts/11234/data"));
+ res.Should().HaveStatusCode(404)
+ .And.HaveCommonBody(ErrorCodes.TimelineController.PostNotExist);
+ }
+
+ long postId;
+ {
+ var res = await client.PostAsJsonAsync(generator(1, "posts"),
+ TimelineHelper.TextPostCreateRequest("aaa"));
+ var body = res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelinePostInfo>()
+ .Which;
+ postId = body.Id;
+ }
+
+ {
+ var res = await client.GetAsync(generator(1, $"posts/{postId}/data"));
+ res.Should().HaveStatusCode(400)
+ .And.HaveCommonBody(ErrorCodes.TimelineController.PostNoData);
+ }
+ }
}
}
diff --git a/Timeline.Tests/IntegratedTests/TokenTest.cs b/Timeline.Tests/IntegratedTests/TokenTest.cs
index 928d546c..7b28746f 100644
--- a/Timeline.Tests/IntegratedTests/TokenTest.cs
+++ b/Timeline.Tests/IntegratedTests/TokenTest.cs
@@ -4,6 +4,7 @@ using Microsoft.Extensions.DependencyInjection;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
+using Timeline.Models;
using Timeline.Models.Http;
using Timeline.Services;
using Timeline.Tests.Helpers;
diff --git a/Timeline.Tests/Timeline.Tests.csproj b/Timeline.Tests/Timeline.Tests.csproj
index 00c1e103..1f00cc6c 100644
--- a/Timeline.Tests/Timeline.Tests.csproj
+++ b/Timeline.Tests/Timeline.Tests.csproj
@@ -28,6 +28,6 @@
</ItemGroup>
<ItemGroup>
- <ProjectReference Include="..\Timeline\Timeline.csproj" />
+ <ProjectReference Include="..\Timeline\Timeline.csproj" />
</ItemGroup>
</Project>
diff --git a/Timeline.sln b/Timeline.sln
index 0e01871a..dfffe385 100644
--- a/Timeline.sln
+++ b/Timeline.sln
@@ -6,9 +6,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Timeline", "Timeline\Timeli
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Timeline.Tests", "Timeline.Tests\Timeline.Tests.csproj", "{3D76D578-37BC-43C2-97BF-9C6DD3825F10}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ErrorResponseCodeGenerator", "ErrorResponseCodeGenerator\ErrorResponseCodeGenerator.csproj", "{F325F802-75DE-4527-A299-F668281B0E4D}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ErrorResponseCodeGenerator", "ErrorResponseCodeGenerator\ErrorResponseCodeGenerator.csproj", "{F325F802-75DE-4527-A299-F668281B0E4D}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Timeline.ErrorCodes", "Timeline.ErrorCodes\Timeline.ErrorCodes.csproj", "{1044E3B0-1010-47CA-956E-B6E8FE87055B}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Timeline.ErrorCodes", "Timeline.ErrorCodes\Timeline.ErrorCodes.csproj", "{1044E3B0-1010-47CA-956E-B6E8FE87055B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
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>