diff options
author | 杨宇千 <crupest@outlook.com> | 2019-11-20 18:21:17 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-11-20 18:21:17 +0800 |
commit | ae5bb5cbef2aec94673e26712d7937fca3996f5b (patch) | |
tree | 788b8acdf1141c757cb3226d3cd5f64594386b8f /Timeline | |
parent | 37a2e6340ab20de1f9e847d795c0cbec9846de97 (diff) | |
parent | ca87f6781a5b0e80989a66be338a699846c40f8d (diff) | |
download | timeline-ae5bb5cbef2aec94673e26712d7937fca3996f5b.tar.gz timeline-ae5bb5cbef2aec94673e26712d7937fca3996f5b.tar.bz2 timeline-ae5bb5cbef2aec94673e26712d7937fca3996f5b.zip |
Merge pull request #54 from crupest/timeline
Add core feature Timeline (currently only personal timeline)
Diffstat (limited to 'Timeline')
28 files changed, 1947 insertions, 138 deletions
diff --git a/Timeline/Controllers/PersonalTimelineController.cs b/Timeline/Controllers/PersonalTimelineController.cs new file mode 100644 index 00000000..c864ed39 --- /dev/null +++ b/Timeline/Controllers/PersonalTimelineController.cs @@ -0,0 +1,168 @@ +using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Threading.Tasks;
+using Timeline.Auth;
+using Timeline.Filters;
+using Timeline.Models;
+using Timeline.Models.Http;
+using Timeline.Models.Validation;
+using Timeline.Services;
+using static Timeline.Resources.Controllers.TimelineController;
+
+namespace Timeline
+{
+ public static partial class ErrorCodes
+ {
+ public static partial class Http
+ {
+ public static class Timeline // ccc = 004
+ {
+ public const int PostListGetForbid = 10040101;
+ public const int PostOperationCreateForbid = 10040102;
+ public const int PostOperationDeleteForbid = 10040103;
+ public const int PostOperationDeleteNotExist = 10040201;
+ public const int ChangeMemberUserNotExist = 10040301;
+ }
+ }
+ }
+}
+
+namespace Timeline.Controllers
+{
+ [ApiController]
+ public class PersonalTimelineController : Controller
+ {
+ private readonly ILogger<PersonalTimelineController> _logger;
+
+ private readonly IPersonalTimelineService _service;
+
+ private bool IsAdmin()
+ {
+ if (User != null)
+ {
+ return User.IsAdministrator();
+ }
+ return false;
+ }
+
+ private string? GetAuthUsername()
+ {
+ if (User == null)
+ {
+ return null;
+ }
+ else
+ {
+ return User.Identity.Name;
+ }
+ }
+
+ public PersonalTimelineController(ILogger<PersonalTimelineController> logger, IPersonalTimelineService service)
+ {
+ _logger = logger;
+ _service = service;
+ }
+
+ [HttpGet("users/{username}/timeline")]
+ [CatchTimelineNotExistException]
+ public async Task<ActionResult<BaseTimelineInfo>> TimelineGet([FromRoute][Username] string username)
+ {
+ return await _service.GetTimeline(username);
+ }
+
+ [HttpGet("users/{username}/timeline/posts")]
+ [CatchTimelineNotExistException]
+ public async Task<ActionResult<IList<TimelinePostInfo>>> PostListGet([FromRoute][Username] string username)
+ {
+ if (!IsAdmin() && !await _service.HasReadPermission(username, GetAuthUsername()))
+ {
+ return StatusCode(StatusCodes.Status403Forbidden,
+ new CommonResponse(ErrorCodes.Http.Timeline.PostListGetForbid, MessagePostListGetForbid));
+ }
+
+ return await _service.GetPosts(username);
+ }
+
+ [HttpPost("users/{username}/timeline/postop/create")]
+ [Authorize]
+ [CatchTimelineNotExistException]
+ public async Task<ActionResult<TimelinePostCreateResponse>> PostOperationCreate([FromRoute][Username] string username, [FromBody] TimelinePostCreateRequest body)
+ {
+ if (!IsAdmin() && !await _service.IsMemberOf(username, GetAuthUsername()!))
+ {
+ return StatusCode(StatusCodes.Status403Forbidden,
+ new CommonResponse(ErrorCodes.Http.Timeline.PostOperationCreateForbid, MessagePostOperationCreateForbid));
+ }
+
+ var res = await _service.CreatePost(username, User.Identity.Name!, body.Content, body.Time);
+ return res;
+ }
+
+ [HttpPost("users/{username}/timeline/postop/delete")]
+ [Authorize]
+ [CatchTimelineNotExistException]
+ public async Task<ActionResult> PostOperationDelete([FromRoute][Username] string username, [FromBody] TimelinePostDeleteRequest body)
+ {
+ try
+ {
+ var postId = body.Id!.Value;
+ if (!IsAdmin() && !await _service.HasPostModifyPermission(username, postId, GetAuthUsername()!))
+ {
+ return StatusCode(StatusCodes.Status403Forbidden,
+ new CommonResponse(ErrorCodes.Http.Timeline.PostOperationDeleteForbid, MessagePostOperationCreateForbid));
+ }
+ await _service.DeletePost(username, postId);
+ }
+ catch (TimelinePostNotExistException)
+ {
+ return BadRequest(new CommonResponse(
+ ErrorCodes.Http.Timeline.PostOperationDeleteNotExist,
+ MessagePostOperationDeleteNotExist));
+ }
+ return Ok();
+ }
+
+ [HttpPost("users/{username}/timeline/op/property")]
+ [Authorize]
+ [SelfOrAdmin]
+ [CatchTimelineNotExistException]
+ public async Task<ActionResult> TimelineChangeProperty([FromRoute][Username] string username, [FromBody] TimelinePropertyChangeRequest body)
+ {
+ await _service.ChangeProperty(username, body);
+ return Ok();
+ }
+
+ [HttpPost("users/{username}/timeline/op/member")]
+ [Authorize]
+ [SelfOrAdmin]
+ [CatchTimelineNotExistException]
+ public async Task<ActionResult> TimelineChangeMember([FromRoute][Username] string username, [FromBody] TimelineMemberChangeRequest body)
+ {
+ try
+ {
+ await _service.ChangeMember(username, body.Add, body.Remove);
+ return Ok();
+ }
+ catch (TimelineMemberOperationUserException e)
+ {
+ if (e.InnerException is UsernameBadFormatException)
+ {
+ return BadRequest(CommonResponse.InvalidModel(
+ string.Format(CultureInfo.CurrentCulture, MessageMemberUsernameBadFormat, e.Index, e.Operation)));
+ }
+ else if (e.InnerException is UserNotExistException)
+ {
+ return BadRequest(new CommonResponse(ErrorCodes.Http.Timeline.ChangeMemberUserNotExist,
+ string.Format(CultureInfo.CurrentCulture, MessageMemberUserNotExist, e.Index, e.Operation)));
+ }
+
+ _logger.LogError(e, LogUnknownTimelineMemberOperationUserException);
+ throw;
+ }
+ }
+ }
+}
diff --git a/Timeline/Entities/DatabaseContext.cs b/Timeline/Entities/DatabaseContext.cs index 6c005b30..123ae0f3 100644 --- a/Timeline/Entities/DatabaseContext.cs +++ b/Timeline/Entities/DatabaseContext.cs @@ -20,5 +20,8 @@ namespace Timeline.Entities public DbSet<User> Users { get; set; } = default!;
public DbSet<UserAvatar> UserAvatars { get; set; } = default!;
public DbSet<UserDetail> UserDetails { get; set; } = default!;
+ public DbSet<TimelineEntity> Timelines { get; set; } = default!;
+ public DbSet<TimelinePostEntity> TimelinePosts { get; set; } = default!;
+ public DbSet<TimelineMemberEntity> TimelineMembers { get; set; } = default!;
}
}
diff --git a/Timeline/Entities/TimelineEntity.cs b/Timeline/Entities/TimelineEntity.cs new file mode 100644 index 00000000..9cacfcae --- /dev/null +++ b/Timeline/Entities/TimelineEntity.cs @@ -0,0 +1,41 @@ +using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Timeline.Models;
+
+namespace Timeline.Entities
+{
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "This is entity object.")]
+ [Table("timelines")]
+ public class TimelineEntity
+ {
+ [Column("id"), Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public long Id { get; set; }
+
+ /// <summary>
+ /// If null, then this timeline is a personal timeline.
+ /// </summary>
+ [Column("name")]
+ public string? Name { get; set; }
+
+ [Column("description")]
+ public string? Description { get; set; }
+
+ [Column("owner")]
+ public long OwnerId { get; set; }
+
+ [ForeignKey(nameof(OwnerId))]
+ public User Owner { get; set; } = default!;
+
+ [Column("visibility")]
+ public TimelineVisibility Visibility { get; set; }
+
+ [Column("create_time")]
+ public DateTime CreateTime { get; set; }
+
+ public List<TimelineMemberEntity> Members { get; set; } = default!;
+
+ public List<TimelinePostEntity> Posts { get; set; } = default!;
+ }
+}
diff --git a/Timeline/Entities/TimelineMemberEntity.cs b/Timeline/Entities/TimelineMemberEntity.cs new file mode 100644 index 00000000..c8961013 --- /dev/null +++ b/Timeline/Entities/TimelineMemberEntity.cs @@ -0,0 +1,23 @@ +using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Timeline.Entities
+{
+ public class TimelineMemberEntity
+ {
+ [Column("id"), Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public long Id { get; set; }
+
+ [Column("user")]
+ public long UserId { get; set; }
+
+ [ForeignKey(nameof(UserId))]
+ public User User { get; set; } = default!;
+
+ [Column("timeline")]
+ public long TimelineId { get; set; }
+
+ [ForeignKey(nameof(TimelineId))]
+ public TimelineEntity Timeline { get; set; } = default!;
+ }
+}
diff --git a/Timeline/Entities/TimelinePostEntity.cs b/Timeline/Entities/TimelinePostEntity.cs new file mode 100644 index 00000000..efef3ab5 --- /dev/null +++ b/Timeline/Entities/TimelinePostEntity.cs @@ -0,0 +1,34 @@ +using System;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Timeline.Entities
+{
+ [Table("timeline_posts")]
+ public class TimelinePostEntity
+ {
+ [Column("id"), Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public long Id { get; set; }
+
+ [Column("timeline")]
+ public long TimelineId { get; set; }
+
+ [ForeignKey(nameof(TimelineId))]
+ public TimelineEntity Timeline { get; set; } = default!;
+
+ [Column("author")]
+ public long AuthorId { get; set; }
+
+ [ForeignKey(nameof(AuthorId))]
+ public User Author { get; set; } = default!;
+
+ [Column("content")]
+ public string? Content { get; set; }
+
+ [Column("time")]
+ public DateTime Time { get; set; }
+
+ [Column("last_updated")]
+ public DateTime LastUpdated { get; set; }
+ }
+}
diff --git a/Timeline/Entities/User.cs b/Timeline/Entities/User.cs index 02352b03..e725a69a 100644 --- a/Timeline/Entities/User.cs +++ b/Timeline/Entities/User.cs @@ -1,4 +1,5 @@ -using System.ComponentModel.DataAnnotations;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Timeline.Entities
@@ -9,6 +10,7 @@ namespace Timeline.Entities public const string User = "user";
}
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "This is an entity class.")]
[Table("users")]
public class User
{
@@ -30,5 +32,11 @@ namespace Timeline.Entities public UserAvatar? Avatar { get; set; }
public UserDetail? Detail { get; set; }
+
+ public List<TimelineEntity> Timelines { get; set; } = default!;
+
+ public List<TimelinePostEntity> TimelinePosts { get; set; } = default!;
+
+ public List<TimelineMemberEntity> TimelinesJoined { get; set; } = default!;
}
}
diff --git a/Timeline/Entities/UserAvatar.cs b/Timeline/Entities/UserAvatar.cs index a5b18b94..114246f3 100644 --- a/Timeline/Entities/UserAvatar.cs +++ b/Timeline/Entities/UserAvatar.cs @@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations.Schema; namespace Timeline.Entities
{
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1819:Properties should not return arrays", Justification = "This is data base entity.")]
[Table("user_avatars")]
public class UserAvatar
{
@@ -11,7 +12,6 @@ namespace Timeline.Entities public long Id { get; set; }
[Column("data")]
- [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1819:Properties should not return arrays", Justification = "This is data base entity.")]
public byte[]? Data { get; set; }
[Column("type")]
diff --git a/Timeline/Filters/Timeline.cs b/Timeline/Filters/Timeline.cs new file mode 100644 index 00000000..7859d409 --- /dev/null +++ b/Timeline/Filters/Timeline.cs @@ -0,0 +1,47 @@ +using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Filters;
+using Timeline.Models.Http;
+using Timeline.Services;
+using static Timeline.Resources.Filters;
+
+namespace Timeline
+{
+ public static partial class ErrorCodes
+ {
+ public static partial class Http
+ {
+ public static partial class Filter // bxx = 1xx
+ {
+ public static class Timeline // bbb = 102
+ {
+ public const int UserNotExist = 11020101;
+ public const int NameNotExist = 11020102;
+ }
+ }
+ }
+ }
+}
+
+namespace Timeline.Filters
+{
+ public class CatchTimelineNotExistExceptionAttribute : ExceptionFilterAttribute
+ {
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods")]
+ public override void OnException(ExceptionContext context)
+ {
+ if (context.Exception is TimelineNotExistException e)
+ {
+ if (e.InnerException is UserNotExistException)
+ {
+ context.Result = new BadRequestObjectResult(
+ new CommonResponse(ErrorCodes.Http.Filter.Timeline.UserNotExist, MessageTimelineNotExistUser));
+ }
+ else
+ {
+ context.Result = new BadRequestObjectResult(
+ new CommonResponse(ErrorCodes.Http.Filter.Timeline.NameNotExist, MessageTimelineNotExist));
+ }
+ }
+ }
+ }
+}
diff --git a/Timeline/Migrations/20191031064541_Initialize.cs b/Timeline/Migrations/20191031064541_Initialize.cs index 416f7c06..73521102 100644 --- a/Timeline/Migrations/20191031064541_Initialize.cs +++ b/Timeline/Migrations/20191031064541_Initialize.cs @@ -85,6 +85,7 @@ namespace Timeline.Migrations column: "name",
unique: true);
+ // Add a init user. Username is "administrator". Password is "crupest".
migrationBuilder.InsertData("users", new string[] { "name", "password", "roles" },
new object[] { "administrator", "AQAAAAEAACcQAAAAENsspZrk8Wo+UuMyg6QuWJsNvRg6gVu4K/TumVod3h9GVLX9zDVuQQds3o7V8QWJ2w==", "user,admin" });
}
diff --git a/Timeline/Models/Http/Timeline.cs b/Timeline/Models/Http/Timeline.cs new file mode 100644 index 00000000..06b88ad1 --- /dev/null +++ b/Timeline/Models/Http/Timeline.cs @@ -0,0 +1,45 @@ +using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using System.Threading.Tasks;
+using Timeline.Entities;
+
+namespace Timeline.Models.Http
+{
+ public class TimelinePostCreateRequest
+ {
+ [Required(AllowEmptyStrings = true)]
+ public string Content { get; set; } = default!;
+
+ public DateTime? Time { get; set; }
+ }
+
+ public class TimelinePostCreateResponse
+ {
+ public long Id { get; set; }
+
+ public DateTime Time { get; set; }
+ }
+
+ public class TimelinePostDeleteRequest
+ {
+ [Required]
+ public long? Id { get; set; }
+ }
+
+ public class TimelinePropertyChangeRequest
+ {
+ public string? Description { get; set; }
+
+ public TimelineVisibility? Visibility { get; set; }
+ }
+
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "This is a DTO class.")]
+ public class TimelineMemberChangeRequest
+ {
+ public List<string>? Add { get; set; }
+
+ public List<string>? Remove { get; set; }
+ }
+}
diff --git a/Timeline/Models/Timeline.cs b/Timeline/Models/Timeline.cs new file mode 100644 index 00000000..752c698d --- /dev/null +++ b/Timeline/Models/Timeline.cs @@ -0,0 +1,55 @@ +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 class TimelinePostInfo
+ {
+ public long Id { get; set; }
+
+ public string? Content { get; set; }
+
+ public DateTime Time { get; set; }
+
+ /// <summary>
+ /// The username of the author.
+ /// </summary>
+ public string Author { get; set; } = default!;
+ }
+
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "This is a DTO class.")]
+ public class BaseTimelineInfo
+ {
+ public string? Description { get; set; }
+
+ /// <summary>
+ /// The username of the owner.
+ /// </summary>
+ public string Owner { get; set; } = default!;
+
+ public TimelineVisibility Visibility { get; set; }
+
+ public List<string> Members { get; set; } = default!;
+ }
+
+ public class TimelineInfo : BaseTimelineInfo
+ {
+ public string Name { get; set; } = default!;
+ }
+}
diff --git a/Timeline/Resources/Controllers/TimelineController.Designer.cs b/Timeline/Resources/Controllers/TimelineController.Designer.cs new file mode 100644 index 00000000..47c43fa2 --- /dev/null +++ b/Timeline/Resources/Controllers/TimelineController.Designer.cs @@ -0,0 +1,135 @@ +//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace Timeline.Resources.Controllers {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class TimelineController {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal TimelineController() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Controllers.TimelineController", typeof(TimelineController).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to An unknown timeline visibility value. Can't convert it..
+ /// </summary>
+ internal static string ExceptionStringToVisibility {
+ get {
+ return ResourceManager.GetString("ExceptionStringToVisibility", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to An unknown TimelineMemberOperationUserException is thrown. Can't recognize its inner exception. It is rethrown..
+ /// </summary>
+ internal static string LogUnknownTimelineMemberOperationUserException {
+ get {
+ return ResourceManager.GetString("LogUnknownTimelineMemberOperationUserException", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The {0}-st username to do operation {1} on is of bad format..
+ /// </summary>
+ internal static string MessageMemberUsernameBadFormat {
+ get {
+ return ResourceManager.GetString("MessageMemberUsernameBadFormat", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The {0}-st user to do operation {1} on does not exist..
+ /// </summary>
+ internal static string MessageMemberUserNotExist {
+ get {
+ return ResourceManager.GetString("MessageMemberUserNotExist", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to You have no permission to read posts of the timeline..
+ /// </summary>
+ internal static string MessagePostListGetForbid {
+ get {
+ return ResourceManager.GetString("MessagePostListGetForbid", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to You have no permission to create posts in the timeline..
+ /// </summary>
+ internal static string MessagePostOperationCreateForbid {
+ get {
+ return ResourceManager.GetString("MessagePostOperationCreateForbid", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to You have no permission to delete posts in the timeline..
+ /// </summary>
+ internal static string MessagePostOperationDeleteForbid {
+ get {
+ return ResourceManager.GetString("MessagePostOperationDeleteForbid", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The post to delete does not exist..
+ /// </summary>
+ internal static string MessagePostOperationDeleteNotExist {
+ get {
+ return ResourceManager.GetString("MessagePostOperationDeleteNotExist", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/Timeline/Resources/Controllers/TimelineController.resx b/Timeline/Resources/Controllers/TimelineController.resx new file mode 100644 index 00000000..0cf7e881 --- /dev/null +++ b/Timeline/Resources/Controllers/TimelineController.resx @@ -0,0 +1,144 @@ +<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="ExceptionStringToVisibility" xml:space="preserve">
+ <value>An unknown timeline visibility value. Can't convert it.</value>
+ </data>
+ <data name="LogUnknownTimelineMemberOperationUserException" xml:space="preserve">
+ <value>An unknown TimelineMemberOperationUserException is thrown. Can't recognize its inner exception. It is rethrown.</value>
+ </data>
+ <data name="MessageMemberUsernameBadFormat" xml:space="preserve">
+ <value>The {0}-st username to do operation {1} on is of bad format.</value>
+ </data>
+ <data name="MessageMemberUserNotExist" xml:space="preserve">
+ <value>The {0}-st user to do operation {1} on does not exist.</value>
+ </data>
+ <data name="MessagePostListGetForbid" xml:space="preserve">
+ <value>You have no permission to read posts of the timeline.</value>
+ </data>
+ <data name="MessagePostOperationCreateForbid" xml:space="preserve">
+ <value>You have no permission to create posts in the timeline.</value>
+ </data>
+ <data name="MessagePostOperationDeleteForbid" xml:space="preserve">
+ <value>You have no permission to delete posts in the timeline.</value>
+ </data>
+ <data name="MessagePostOperationDeleteNotExist" xml:space="preserve">
+ <value>The post to delete does not exist.</value>
+ </data>
+</root>
\ No newline at end of file diff --git a/Timeline/Resources/Controllers/TimelineController.zh.resx b/Timeline/Resources/Controllers/TimelineController.zh.resx new file mode 100644 index 00000000..170ab4cd --- /dev/null +++ b/Timeline/Resources/Controllers/TimelineController.zh.resx @@ -0,0 +1,138 @@ +<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="MessageMemberUsernameBadFormat" xml:space="preserve">
+ <value>第{0}个做{1}操作的用户名格式错误。</value>
+ </data>
+ <data name="MessageMemberUserNotExist" xml:space="preserve">
+ <value>第{0}个做{1}操作的用户不存在。</value>
+ </data>
+ <data name="MessagePostListGetForbid" xml:space="preserve">
+ <value>你没有权限读取这个时间线消息。</value>
+ </data>
+ <data name="MessagePostOperationCreateForbid" xml:space="preserve">
+ <value>你没有权限在这个时间线中创建消息。</value>
+ </data>
+ <data name="MessagePostOperationDeleteForbid" xml:space="preserve">
+ <value>你没有权限在这个时间线中删除消息。</value>
+ </data>
+ <data name="MessagePostOperationDeleteNotExist" xml:space="preserve">
+ <value>要删除的消息不存在。</value>
+ </data>
+</root>
\ No newline at end of file diff --git a/Timeline/Resources/Filters.Designer.cs b/Timeline/Resources/Filters.Designer.cs index 3481e4ae..5576190d 100644 --- a/Timeline/Resources/Filters.Designer.cs +++ b/Timeline/Resources/Filters.Designer.cs @@ -124,6 +124,24 @@ namespace Timeline.Resources { }
/// <summary>
+ /// Looks up a localized string similar to The requested timeline does not exist..
+ /// </summary>
+ internal static string MessageTimelineNotExist {
+ get {
+ return ResourceManager.GetString("MessageTimelineNotExist", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The requested personal timeline does not exist because the user does not exist..
+ /// </summary>
+ internal static string MessageTimelineNotExistUser {
+ get {
+ return ResourceManager.GetString("MessageTimelineNotExistUser", resourceCulture);
+ }
+ }
+
+ /// <summary>
/// Looks up a localized string similar to The user does not exist..
/// </summary>
internal static string MessageUserNotExist {
diff --git a/Timeline/Resources/Filters.resx b/Timeline/Resources/Filters.resx index b91d4612..7bfbc703 100644 --- a/Timeline/Resources/Filters.resx +++ b/Timeline/Resources/Filters.resx @@ -138,6 +138,12 @@ <data name="MessageSelfOrAdminForbid" xml:space="preserve">
<value>You can't access the resource unless you are the owner or administrator.</value>
</data>
+ <data name="MessageTimelineNotExist" xml:space="preserve">
+ <value>The requested timeline does not exist.</value>
+ </data>
+ <data name="MessageTimelineNotExistUser" xml:space="preserve">
+ <value>The requested personal timeline does not exist because the user does not exist.</value>
+ </data>
<data name="MessageUserNotExist" xml:space="preserve">
<value>The user does not exist.</value>
</data>
diff --git a/Timeline/Resources/Filters.zh.resx b/Timeline/Resources/Filters.zh.resx index 159ac04a..36aac788 100644 --- a/Timeline/Resources/Filters.zh.resx +++ b/Timeline/Resources/Filters.zh.resx @@ -129,6 +129,12 @@ <data name="MessageSelfOrAdminForbid" xml:space="preserve">
<value>你无权访问该资源除非你是资源的拥有者或者管理员。</value>
</data>
+ <data name="MessageTimelineNotExist" xml:space="preserve">
+ <value>请求的时间线不存在。</value>
+ </data>
+ <data name="MessageTimelineNotExistUser" xml:space="preserve">
+ <value>请求的个人时间线不存在因为该用户不存在。</value>
+ </data>
<data name="MessageUserNotExist" xml:space="preserve">
<value>用户不存在。</value>
</data>
diff --git a/Timeline/Resources/Services/Exception.Designer.cs b/Timeline/Resources/Services/Exception.Designer.cs index ddf60f45..1b46f9e9 100644 --- a/Timeline/Resources/Services/Exception.Designer.cs +++ b/Timeline/Resources/Services/Exception.Designer.cs @@ -268,6 +268,69 @@ namespace Timeline.Resources.Services { }
/// <summary>
+ /// Looks up a localized string similar to The timeline with that name already exists..
+ /// </summary>
+ internal static string TimelineAlreadyExistException {
+ get {
+ return ResourceManager.GetString("TimelineAlreadyExistException", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to An exception happened when add or remove member on timeline..
+ /// </summary>
+ internal static string TimelineMemberOperationException {
+ get {
+ return ResourceManager.GetString("TimelineMemberOperationException", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to An exception happened when do operation {0} on the {1} member on timeline..
+ /// </summary>
+ internal static string TimelineMemberOperationExceptionDetail {
+ get {
+ return ResourceManager.GetString("TimelineMemberOperationExceptionDetail", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Timeline name is of bad format. If this is a personal timeline, it means the username is of bad format and inner exception should be a UsernameBadFormatException..
+ /// </summary>
+ internal static string TimelineNameBadFormatException {
+ get {
+ return ResourceManager.GetString("TimelineNameBadFormatException", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Timeline does not exist. If this is a personal timeline, it means the user does not exist and inner exception should be a UserNotExistException..
+ /// </summary>
+ internal static string TimelineNotExistException {
+ get {
+ return ResourceManager.GetString("TimelineNotExistException", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The timeline post does not exist. You can't do operation on it..
+ /// </summary>
+ internal static string TimelinePostNotExistException {
+ get {
+ return ResourceManager.GetString("TimelinePostNotExistException", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The use is not a member of the timeline..
+ /// </summary>
+ internal static string TimelineUserNotMemberException {
+ get {
+ return ResourceManager.GetString("TimelineUserNotMemberException", resourceCulture);
+ }
+ }
+
+ /// <summary>
/// Looks up a localized string similar to The username is of bad format..
/// </summary>
internal static string UsernameBadFormatException {
diff --git a/Timeline/Resources/Services/Exception.resx b/Timeline/Resources/Services/Exception.resx index 12bf9afb..1d9c0037 100644 --- a/Timeline/Resources/Services/Exception.resx +++ b/Timeline/Resources/Services/Exception.resx @@ -186,6 +186,27 @@ <data name="JwtVerifyExceptionVersionClaimBadFormat" xml:space="preserve">
<value>version claim is not a number.</value>
</data>
+ <data name="TimelineAlreadyExistException" xml:space="preserve">
+ <value>The timeline with that name already exists.</value>
+ </data>
+ <data name="TimelineMemberOperationException" xml:space="preserve">
+ <value>An exception happened when add or remove member on timeline.</value>
+ </data>
+ <data name="TimelineMemberOperationExceptionDetail" xml:space="preserve">
+ <value>An exception happened when do operation {0} on the {1} member on timeline.</value>
+ </data>
+ <data name="TimelineNameBadFormatException" xml:space="preserve">
+ <value>Timeline name is of bad format. If this is a personal timeline, it means the username is of bad format and inner exception should be a UsernameBadFormatException.</value>
+ </data>
+ <data name="TimelineNotExistException" xml:space="preserve">
+ <value>Timeline does not exist. If this is a personal timeline, it means the user does not exist and inner exception should be a UserNotExistException.</value>
+ </data>
+ <data name="TimelinePostNotExistException" xml:space="preserve">
+ <value>The timeline post does not exist. You can't do operation on it.</value>
+ </data>
+ <data name="TimelineUserNotMemberException" xml:space="preserve">
+ <value>The use is not a member of the timeline.</value>
+ </data>
<data name="UsernameBadFormatException" xml:space="preserve">
<value>The username is of bad format.</value>
</data>
diff --git a/Timeline/Services/TimelineAlreadyExistException.cs b/Timeline/Services/TimelineAlreadyExistException.cs new file mode 100644 index 00000000..c2dea1f9 --- /dev/null +++ b/Timeline/Services/TimelineAlreadyExistException.cs @@ -0,0 +1,17 @@ +using System;
+
+namespace Timeline.Services
+{
+ [Serializable]
+ public class TimelineAlreadyExistException : Exception
+ {
+ public TimelineAlreadyExistException() : base(Resources.Services.Exception.TimelineAlreadyExistException) { }
+ public TimelineAlreadyExistException(string name) : base(Resources.Services.Exception.TimelineAlreadyExistException) { Name = name; }
+ public TimelineAlreadyExistException(string name, Exception inner) : base(Resources.Services.Exception.TimelineAlreadyExistException, inner) { Name = name; }
+ protected TimelineAlreadyExistException(
+ System.Runtime.Serialization.SerializationInfo info,
+ System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
+
+ public string? Name { get; set; }
+ }
+}
diff --git a/Timeline/Services/TimelineMemberOperationUserException.cs b/Timeline/Services/TimelineMemberOperationUserException.cs new file mode 100644 index 00000000..543ee160 --- /dev/null +++ b/Timeline/Services/TimelineMemberOperationUserException.cs @@ -0,0 +1,37 @@ +using System;
+using System.Globalization;
+
+namespace Timeline.Services
+{
+ [Serializable]
+ public class TimelineMemberOperationUserException : Exception
+ {
+ public enum MemberOperation
+ {
+ Add,
+ Remove
+ }
+
+ public TimelineMemberOperationUserException() : base(Resources.Services.Exception.TimelineMemberOperationException) { }
+ public TimelineMemberOperationUserException(string message) : base(message) { }
+ public TimelineMemberOperationUserException(string message, Exception inner) : base(message, inner) { }
+ protected TimelineMemberOperationUserException(
+ System.Runtime.Serialization.SerializationInfo info,
+ System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
+
+ public TimelineMemberOperationUserException(int index, MemberOperation operation, string username, Exception inner)
+ : base(MakeMessage(operation, index), inner) { Operation = operation; Index = index; Username = username; }
+
+ private static string MakeMessage(MemberOperation operation, int index) => string.Format(CultureInfo.CurrentCulture,
+ Resources.Services.Exception.TimelineMemberOperationExceptionDetail, operation, index);
+
+ public MemberOperation? Operation { get; set; }
+
+ /// <summary>
+ /// The index of the member on which the operation failed.
+ /// </summary>
+ public int? Index { get; set; }
+
+ public string? Username { get; set; }
+ }
+}
diff --git a/Timeline/Services/TimelineNameBadFormatException.cs b/Timeline/Services/TimelineNameBadFormatException.cs new file mode 100644 index 00000000..5120a175 --- /dev/null +++ b/Timeline/Services/TimelineNameBadFormatException.cs @@ -0,0 +1,21 @@ +using System;
+
+namespace Timeline.Services
+{
+ [Serializable]
+ public class TimelineNameBadFormatException : Exception
+ {
+ public TimelineNameBadFormatException()
+ : base(Resources.Services.Exception.TimelineNameBadFormatException) { }
+ public TimelineNameBadFormatException(string name)
+ : base(Resources.Services.Exception.TimelineNameBadFormatException) { Name = name; }
+ public TimelineNameBadFormatException(string name, Exception inner)
+ : base(Resources.Services.Exception.TimelineNameBadFormatException, inner) { Name = name; }
+
+ protected TimelineNameBadFormatException(
+ System.Runtime.Serialization.SerializationInfo info,
+ System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
+
+ public string? Name { get; set; }
+ }
+}
diff --git a/Timeline/Services/TimelineNotExistException.cs b/Timeline/Services/TimelineNotExistException.cs new file mode 100644 index 00000000..6dfd0bab --- /dev/null +++ b/Timeline/Services/TimelineNotExistException.cs @@ -0,0 +1,19 @@ +using System;
+
+namespace Timeline.Services
+{
+ [Serializable]
+ public class TimelineNotExistException : Exception
+ {
+ public TimelineNotExistException() : base(Resources.Services.Exception.TimelineNotExistException) { }
+ public TimelineNotExistException(string name)
+ : base(Resources.Services.Exception.TimelineNotExistException) { Name = name; }
+ public TimelineNotExistException(string name, Exception inner)
+ : base(Resources.Services.Exception.TimelineNotExistException, inner) { Name = name; }
+ protected TimelineNotExistException(
+ System.Runtime.Serialization.SerializationInfo info,
+ System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
+
+ public string? Name { get; set; }
+ }
+}
diff --git a/Timeline/Services/TimelinePostNotExistException.cs b/Timeline/Services/TimelinePostNotExistException.cs new file mode 100644 index 00000000..97e5d550 --- /dev/null +++ b/Timeline/Services/TimelinePostNotExistException.cs @@ -0,0 +1,23 @@ +using System;
+
+namespace Timeline.Services
+{
+ [Serializable]
+ public class TimelinePostNotExistException : Exception
+ {
+ public TimelinePostNotExistException() { }
+ public TimelinePostNotExistException(string message) : base(message) { }
+ public TimelinePostNotExistException(string message, Exception inner) : base(message, inner) { }
+ protected TimelinePostNotExistException(
+ 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(long id, string message) : base(message) { Id = id; }
+
+ public TimelinePostNotExistException(long id, string message, Exception inner) : base(message, inner) { Id = id; }
+
+ public long Id { get; set; }
+ }
+}
diff --git a/Timeline/Services/TimelineService.cs b/Timeline/Services/TimelineService.cs new file mode 100644 index 00000000..affcff2e --- /dev/null +++ b/Timeline/Services/TimelineService.cs @@ -0,0 +1,730 @@ +using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Timeline.Entities;
+using Timeline.Models;
+using Timeline.Models.Http;
+using Timeline.Models.Validation;
+
+namespace Timeline.Services
+{
+ /// <summary>
+ /// 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"/>.
+ /// </remarks>
+ public interface IBaseTimelineService
+ {
+ /// <summary>
+ /// Get all the posts in the timeline.
+ /// </summary>
+ /// <param name="name">Username or the timeline 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="TimelineNameBadFormatException">
+ /// Thrown when timeline name is of bad format.
+ /// For normal timeline, it means name is an empty string.
+ /// For personal timeline, it means the username is of bad format,
+ /// the inner exception should be a <see cref="UsernameBadFormatException"/>.
+ /// </exception>
+ /// <exception cref="TimelineNotExistException">
+ /// Thrown when timeline does not exist.
+ /// For normal timeline, it means the name does not exist.
+ /// For personal timeline, it means the user of that username does not exist
+ /// and the inner exception should be a <see cref="UserNotExistException"/>.
+ /// </exception>
+ Task<List<TimelinePostInfo>> GetPosts(string name);
+
+ /// <summary>
+ /// Create a new post in timeline.
+ /// </summary>
+ /// <param name="name">Username or the timeline name. See remarks of <ssee cref="IBaseTimelineService"/>.</param>
+ /// <param name="author">The author's username.</param>
+ /// <param name="content">The content.</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="author"/> or <paramref name="content"/> is null.</exception>
+ /// <exception cref="TimelineNameBadFormatException">
+ /// Thrown when timeline name is of bad format.
+ /// For normal timeline, it means name is an empty string.
+ /// For personal timeline, it means the username is of bad format,
+ /// the inner exception should be a <see cref="UsernameBadFormatException"/>.
+ /// </exception>
+ /// <exception cref="TimelineNotExistException">
+ /// Thrown when timeline does not exist.
+ /// For normal timeline, it means the name does not exist.
+ /// For personal timeline, it means the user of that username does not exist
+ /// and the inner exception should be a <see cref="UserNotExistException"/>.
+ /// </exception>
+ /// <exception cref="UsernameBadFormatException">Thrown if <paramref name="author"/> is of bad format.</exception>
+ /// <exception cref="UserNotExistException">Thrown if <paramref name="author"/> does not exist.</exception>
+ Task<TimelinePostCreateResponse> CreatePost(string name, string author, string content, DateTime? time);
+
+ /// <summary>
+ /// Delete a post
+ /// </summary>
+ /// <param name="name">Username or the timeline 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="TimelineNameBadFormatException">
+ /// Thrown when timeline name is of bad format.
+ /// For normal timeline, it means name is an empty string.
+ /// For personal timeline, it means the username is of bad format,
+ /// the inner exception should be a <see cref="UsernameBadFormatException"/>.
+ /// </exception>
+ /// <exception cref="TimelineNotExistException">
+ /// Thrown when timeline does not exist.
+ /// For normal timeline, it means the name does not exist.
+ /// For personal timeline, it means the user of that username does not exist
+ /// and the inner exception should be a <see cref="UserNotExistException"/>.
+ /// </exception>
+ /// <exception cref="TimelinePostNotExistException">
+ /// Thrown when the post with given id does not exist or is deleted already.
+ /// </exception>
+ /// <remarks>
+ /// First use <see cref="IBaseTimelineService.HasPostModifyPermission(string, long, string)"/>
+ /// to check the permission.
+ /// </remarks>
+ Task DeletePost(string name, long id);
+
+ /// <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="newProperties">The new properties. Null member means not to change.</param>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> or <paramref name="newProperties"/> is null.</exception>
+ /// <exception cref="TimelineNameBadFormatException">
+ /// Thrown when timeline name is of bad format.
+ /// For normal timeline, it means name is an empty string.
+ /// For personal timeline, it means the username is of bad format,
+ /// the inner exception should be a <see cref="UsernameBadFormatException"/>.
+ /// </exception>
+ /// <exception cref="TimelineNotExistException">
+ /// Thrown when timeline does not exist.
+ /// For normal timeline, it means the name does not exist.
+ /// For personal timeline, it means the user of that username does not exist
+ /// and the inner exception should be a <see cref="UserNotExistException"/>.
+ /// </exception>
+ Task ChangeProperty(string name, TimelinePropertyChangeRequest newProperties);
+
+ /// <summary>
+ /// Remove members to a timeline.
+ /// </summary>
+ /// <param name="name">Username or the timeline 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="TimelineNameBadFormatException">
+ /// Thrown when timeline name is of bad format.
+ /// For normal timeline, it means name is an empty string.
+ /// For personal timeline, it means the username is of bad format,
+ /// the inner exception should be a <see cref="UsernameBadFormatException"/>.
+ /// </exception>
+ /// <exception cref="TimelineNotExistException">
+ /// Thrown when timeline does not exist.
+ /// For normal timeline, it means the name does not exist.
+ /// For personal timeline, it means the user of that username does not exist
+ /// and the inner exception should be a <see cref="UserNotExistException"/>.
+ /// </exception>
+ /// <exception cref="TimelineMemberOperationUserException">
+ /// Thrown when an exception occurs on the user list.
+ /// The inner exception is <see cref="UsernameBadFormatException"/>
+ /// when one of the username is invalid.
+ /// The inner exception is <see cref="UserNotExistException"/>
+ /// when one of the user to change does not exist.
+ /// </exception>
+ /// <remarks>
+ /// Operating on a username that is of bad format or does not exist always throws.
+ /// Add a user that already is a member has no effects.
+ /// Remove a user that is not a member also has not effects.
+ /// Add and remove an identical user results in no effects.
+ /// More than one same usernames are regarded as one.
+ /// </remarks>
+ Task ChangeMember(string name, IList<string>? add, IList<string>? remove);
+
+ /// <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="username">The user to check on. Null means visitor without account.</param>
+ /// <returns>True if can read, false if can't read.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> is null.</exception>
+ /// <exception cref="TimelineNameBadFormatException">
+ /// Thrown when timeline name is of bad format.
+ /// For normal timeline, it means name is an empty string.
+ /// For personal timeline, it means the username is of bad format,
+ /// the inner exception should be a <see cref="UsernameBadFormatException"/>.
+ /// </exception>
+ /// <exception cref="TimelineNotExistException">
+ /// Thrown when timeline does not exist.
+ /// For normal timeline, it means the name does not exist.
+ /// For personal timeline, it means the user of that username does not exist
+ /// and the inner exception should be a <see cref="UserNotExistException"/>.
+ /// </exception>
+ /// <exception cref="UsernameBadFormatException">
+ /// Thrown when <paramref name="username"/> is of bad format.
+ /// </exception>
+ /// <exception cref="UserNotExistException">
+ /// Thrown when <paramref name="username"/> does not exist.
+ /// </exception>
+ Task<bool> HasReadPermission(string name, string? username);
+
+ /// <summary>
+ /// 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="username">The user to check on.</param>
+ /// <returns>True if can modify, false if can't modify.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> or <paramref name="username"/> is null.</exception>
+ /// <exception cref="TimelineNameBadFormatException">
+ /// Thrown when timeline name is of bad format.
+ /// For normal timeline, it means name is an empty string.
+ /// For personal timeline, it means the username is of bad format,
+ /// the inner exception should be a <see cref="UsernameBadFormatException"/>.
+ /// </exception>
+ /// <exception cref="TimelineNotExistException">
+ /// Thrown when timeline does not exist.
+ /// For normal timeline, it means the name does not exist.
+ /// For personal timeline, it means the user of that username does not exist
+ /// and the inner exception should be a <see cref="UserNotExistException"/>.
+ /// </exception>
+ /// <exception cref="TimelinePostNotExistException">
+ /// Thrown when the post with given id does not exist or is deleted already.
+ /// </exception>
+ /// <exception cref="UsernameBadFormatException">
+ /// Thrown when <paramref name="username"/> is of bad format.
+ /// </exception>
+ /// <exception cref="UserNotExistException">
+ /// Thrown when <paramref name="username"/> does not exist.
+ /// </exception>
+ /// <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.
+ /// </remarks>
+ Task<bool> HasPostModifyPermission(string name, long id, string username);
+
+ /// <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="username">The user to check on.</param>
+ /// <returns>True if it is a member, false if not.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> or <paramref name="username"/> is null.</exception>
+ /// <exception cref="TimelineNameBadFormatException">
+ /// Thrown when timeline name is of bad format.
+ /// For normal timeline, it means name is an empty string.
+ /// For personal timeline, it means the username is of bad format,
+ /// the inner exception should be a <see cref="UsernameBadFormatException"/>.
+ /// </exception>
+ /// <exception cref="TimelineNotExistException">
+ /// Thrown when timeline does not exist.
+ /// For normal timeline, it means the name does not exist.
+ /// For personal timeline, it means the user of that username does not exist
+ /// and the inner exception should be a <see cref="UserNotExistException"/>.
+ /// </exception>
+ /// <exception cref="UsernameBadFormatException">
+ /// Thrown when <paramref name="username"/> is not a valid username.
+ /// </exception>
+ /// <exception cref="UserNotExistException">
+ /// Thrown when user <paramref name="username"/> does not exist.
+ /// </exception>
+ /// <remarks>
+ /// Timeline owner is also considered as a member.
+ /// </remarks>
+ Task<bool> IsMemberOf(string name, string username);
+ }
+
+ /// <summary>
+ /// Service for normal timeline.
+ /// </summary>
+ public interface ITimelineService : IBaseTimelineService
+ {
+ /// <summary>
+ /// Get the timeline info.
+ /// </summary>
+ /// <param name="name">The name of the timeline.</param>
+ /// <returns>The timeline info.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> is null.</exception>
+ /// <exception cref="TimelineNameBadFormatException">
+ /// Thrown when timeline name is invalid. Currently it means it is an empty string.
+ /// </exception>
+ /// <exception cref="TimelineNotExistException">
+ /// Thrown when timeline with the name does not exist.
+ /// </exception>
+ Task<TimelineInfo> GetTimeline(string name);
+
+ /// <summary>
+ /// Create a timeline.
+ /// </summary>
+ /// <param name="name">The name of the timeline.</param>
+ /// <param name="owner">The owner of the timeline.</param>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> or <paramref name="owner"/> is null.</exception>
+ /// <exception cref="TimelineNameBadFormatException">
+ /// Thrown when timeline name is invalid. Currently it means it is an empty string.
+ /// </exception>
+ /// <exception cref="TimelineAlreadyExistException">
+ /// Thrown when the timeline already exists.
+ /// </exception>
+ /// <exception cref="UsernameBadFormatException">
+ /// Thrown when the username of the owner is not valid.
+ /// </exception>
+ /// <exception cref="UserNotExistException">
+ /// Thrown when the owner user does not exist.</exception>
+ Task CreateTimeline(string name, string owner);
+ }
+
+ public interface IPersonalTimelineService : IBaseTimelineService
+ {
+ /// <summary>
+ /// Get the timeline info.
+ /// </summary>
+ /// <param name="username">The username of the owner of the personal timeline.</param>
+ /// <returns>The timeline info.</returns>
+ /// <exception cref="ArgumentNullException">
+ /// Thrown when <paramref name="username"/> is null.
+ /// </exception>
+ /// <exception cref="TimelineNameBadFormatException">
+ /// Thrown when <paramref name="username"/> is of bad format. Inner exception MUST be <see cref="UsernameBadFormatException"/>.
+ /// </exception>
+ /// <exception cref="TimelineNotExistException">
+ /// Thrown when the user does not exist. Inner exception MUST be <see cref="UserNotExistException"/>.
+ /// </exception>
+ Task<BaseTimelineInfo> GetTimeline(string username);
+ }
+
+ public abstract class BaseTimelineService : IBaseTimelineService
+ {
+ protected BaseTimelineService(ILoggerFactory loggerFactory, DatabaseContext database, IClock clock)
+ {
+ Clock = clock;
+ Database = database;
+ }
+
+ protected IClock Clock { get; }
+
+ protected UsernameValidator UsernameValidator { get; } = new UsernameValidator();
+
+ protected DatabaseContext Database { get; }
+
+ /// <summary>
+ /// Find the timeline id by the name.
+ /// For details, see remarks.
+ /// </summary>
+ /// <param name="name">The username or the timeline name. See remarks.</param>
+ /// <returns>The id of the timeline entity.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> is null.</exception>
+ /// <exception cref="TimelineNameBadFormatException">
+ /// Thrown when timeline name is of bad format.
+ /// For normal timeline, it means name is an empty string.
+ /// For personal timeline, it means the username is of bad format,
+ /// the inner exception should be a <see cref="UsernameBadFormatException"/>.
+ /// </exception>
+ /// <exception cref="TimelineNotExistException">
+ /// Thrown when timeline does not exist.
+ /// For normal timeline, it means the name does not exist.
+ /// For personal timeline, it means the user of that username does not exist
+ /// and the inner exception should be a <see cref="UserNotExistException"/>.
+ /// </exception>
+ /// <remarks>
+ /// This is the common but different part for both types of timeline service.
+ /// For class that implements <see cref="IPersonalTimelineService"/>, this method should
+ /// find the timeline entity id by the given <paramref name="name"/> as the username of the owner.
+ /// For class that implements <see cref="ITimelineService"/>, this method should
+ /// find the timeline entity id by the given <paramref name="name"/> as the timeline name.
+ /// This method should be called by many other method that follows the contract.
+ /// </remarks>
+ protected abstract Task<long> FindTimelineId(string name);
+
+ public async Task<List<TimelinePostInfo>> GetPosts(string name)
+ {
+ if (name == null)
+ throw new ArgumentNullException(nameof(name));
+
+ var timelineId = await FindTimelineId(name);
+ var postEntities = await Database.TimelinePosts.OrderBy(p => p.Time).Where(p => p.TimelineId == timelineId && p.Content != null).ToListAsync();
+ var posts = new List<TimelinePostInfo>(await Task.WhenAll(postEntities.Select(async p => new TimelinePostInfo
+ {
+ Id = p.Id,
+ Content = p.Content,
+ Author = (await Database.Users.Where(u => u.Id == p.AuthorId).Select(u => new { u.Name }).SingleAsync()).Name,
+ Time = p.Time
+ })));
+ return posts;
+ }
+
+ public async Task<TimelinePostCreateResponse> CreatePost(string name, string author, string content, DateTime? time)
+ {
+ if (name == null)
+ throw new ArgumentNullException(nameof(name));
+ if (author == null)
+ throw new ArgumentNullException(nameof(author));
+ if (content == null)
+ throw new ArgumentNullException(nameof(content));
+
+ {
+ var (result, message) = UsernameValidator.Validate(author);
+ if (!result)
+ {
+ throw new UsernameBadFormatException(author, message);
+ }
+ }
+
+ var timelineId = await FindTimelineId(name);
+
+ var authorEntity = Database.Users.Where(u => u.Name == author).Select(u => new { u.Id }).SingleOrDefault();
+ if (authorEntity == null)
+ {
+ throw new UserNotExistException(author);
+ }
+ var authorId = authorEntity.Id;
+
+ var currentTime = Clock.GetCurrentTime();
+
+ var postEntity = new TimelinePostEntity
+ {
+ Content = content,
+ AuthorId = authorId,
+ TimelineId = timelineId,
+ Time = time ?? currentTime,
+ LastUpdated = currentTime
+ };
+
+ Database.TimelinePosts.Add(postEntity);
+ await Database.SaveChangesAsync();
+
+ return new TimelinePostCreateResponse
+ {
+ Id = postEntity.Id,
+ Time = postEntity.Time
+ };
+ }
+
+ public async Task DeletePost(string name, long id)
+ {
+ if (name == null)
+ throw new ArgumentNullException(nameof(name));
+
+ var timelineId = FindTimelineId(name);
+
+ var post = await Database.TimelinePosts.Where(p => p.Id == id).SingleOrDefaultAsync();
+
+ if (post == null)
+ throw new TimelinePostNotExistException(id);
+
+ post.Content = null;
+ post.LastUpdated = Clock.GetCurrentTime();
+
+ await Database.SaveChangesAsync();
+ }
+
+ public async Task ChangeProperty(string name, TimelinePropertyChangeRequest newProperties)
+ {
+ if (name == null)
+ throw new ArgumentNullException(nameof(name));
+ if (newProperties == null)
+ throw new ArgumentNullException(nameof(newProperties));
+
+ var timelineId = await FindTimelineId(name);
+
+ var timelineEntity = await Database.Timelines.Where(t => t.Id == timelineId).SingleAsync();
+
+ if (newProperties.Description != null)
+ {
+ timelineEntity.Description = newProperties.Description;
+ }
+
+ if (newProperties.Visibility.HasValue)
+ {
+ timelineEntity.Visibility = newProperties.Visibility.Value;
+ }
+
+ await Database.SaveChangesAsync();
+ }
+
+ public async Task ChangeMember(string name, IList<string>? add, IList<string>? remove)
+ {
+ if (name == null)
+ throw new ArgumentNullException(nameof(name));
+
+ // remove duplication and check the format of each username.
+ // Return a username->index map.
+ Dictionary<string, int>? RemoveDuplicateAndCheckFormat(IList<string>? list, TimelineMemberOperationUserException.MemberOperation operation)
+ {
+ if (list != null)
+ {
+ Dictionary<string, int> result = new Dictionary<string, int>();
+ var count = list.Count;
+ for (var index = 0; index < count; index++)
+ {
+ var username = list[index];
+ if (result.ContainsKey(username))
+ {
+ continue;
+ }
+ var (validationResult, message) = UsernameValidator.Validate(username);
+ if (!validationResult)
+ throw new TimelineMemberOperationUserException(
+ index, operation, username,
+ new UsernameBadFormatException(username, message));
+ result.Add(username, index);
+ }
+ return result;
+ }
+ else
+ {
+ return null;
+ }
+ }
+ var simplifiedAdd = RemoveDuplicateAndCheckFormat(add, TimelineMemberOperationUserException.MemberOperation.Add);
+ var simplifiedRemove = RemoveDuplicateAndCheckFormat(remove, TimelineMemberOperationUserException.MemberOperation.Remove);
+
+ // remove those both in add and remove
+ if (simplifiedAdd != null && simplifiedRemove != null)
+ {
+ var usersToClean = simplifiedRemove.Keys.Where(u => simplifiedAdd.ContainsKey(u));
+ foreach (var u in usersToClean)
+ {
+ simplifiedAdd.Remove(u);
+ simplifiedRemove.Remove(u);
+ }
+ }
+
+ var timelineId = await FindTimelineId(name);
+
+ async Task<List<long>?> CheckExistenceAndGetId(Dictionary<string, int>? map, TimelineMemberOperationUserException.MemberOperation operation)
+ {
+ if (map == null)
+ return null;
+
+ List<long> result = new List<long>();
+ foreach (var (username, index) in map)
+ {
+ var user = await Database.Users.Where(u => u.Name == username).Select(u => new { u.Id }).SingleOrDefaultAsync();
+ if (user == null)
+ {
+ throw new TimelineMemberOperationUserException(index, operation, username,
+ new UserNotExistException(username));
+ }
+ result.Add(user.Id);
+ }
+ return result;
+ }
+ var userIdsAdd = await CheckExistenceAndGetId(simplifiedAdd, TimelineMemberOperationUserException.MemberOperation.Add);
+ var userIdsRemove = await CheckExistenceAndGetId(simplifiedRemove, TimelineMemberOperationUserException.MemberOperation.Remove);
+
+ if (userIdsAdd != null)
+ {
+ var membersToAdd = userIdsAdd.Select(id => new TimelineMemberEntity { UserId = id, TimelineId = timelineId }).ToList();
+ Database.TimelineMembers.AddRange(membersToAdd);
+ }
+
+ if (userIdsRemove != null)
+ {
+ var membersToRemove = await Database.TimelineMembers.Where(m => m.TimelineId == timelineId && userIdsRemove.Contains(m.UserId)).ToListAsync();
+ Database.TimelineMembers.RemoveRange(membersToRemove);
+ }
+
+ await Database.SaveChangesAsync();
+ }
+
+ public async Task<bool> HasReadPermission(string name, string? username)
+ {
+ if (name == null)
+ throw new ArgumentNullException(nameof(name));
+
+ long? userId = null;
+ if (username != null)
+ {
+ var (result, message) = UsernameValidator.Validate(username);
+ if (!result)
+ {
+ throw new UsernameBadFormatException(username);
+ }
+
+ var user = await Database.Users.Where(u => u.Name == username).Select(u => new { u.Id }).SingleOrDefaultAsync();
+
+ if (user == null)
+ {
+ throw new UserNotExistException(username);
+ }
+
+ userId = user.Id;
+ }
+
+ var timelineId = await FindTimelineId(name);
+
+ var timelineEntity = await Database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.Visibility }).SingleAsync();
+
+ if (timelineEntity.Visibility == TimelineVisibility.Public)
+ return true;
+
+ if (timelineEntity.Visibility == TimelineVisibility.Register && username != null)
+ return true;
+
+ if (userId == null)
+ {
+ return false;
+ }
+ else
+ {
+ var memberEntity = await Database.TimelineMembers.Where(m => m.UserId == userId && m.TimelineId == timelineId).SingleOrDefaultAsync();
+ return memberEntity != null;
+ }
+ }
+
+ public async Task<bool> HasPostModifyPermission(string name, long id, string username)
+ {
+ if (name == null)
+ throw new ArgumentNullException(nameof(name));
+ if (username == null)
+ throw new ArgumentNullException(nameof(username));
+
+ {
+ var (result, message) = UsernameValidator.Validate(username);
+ if (!result)
+ {
+ throw new UsernameBadFormatException(username);
+ }
+ }
+
+ var user = await Database.Users.Where(u => u.Name == username).Select(u => new { u.Id }).SingleOrDefaultAsync();
+
+ if (user == null)
+ {
+ throw new UserNotExistException(username);
+ }
+
+ var userId = user.Id;
+
+ var timelineId = await FindTimelineId(name);
+
+ var timelineEntity = await Database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.OwnerId }).SingleAsync();
+
+ var postEntity = await Database.TimelinePosts.Where(p => p.Id == id).Select(p => new { p.AuthorId }).SingleOrDefaultAsync();
+
+ if (postEntity == null)
+ throw new TimelinePostNotExistException(id);
+
+ return timelineEntity.OwnerId == userId || postEntity.AuthorId == userId;
+ }
+
+ public async Task<bool> IsMemberOf(string name, string username)
+ {
+ if (name == null)
+ throw new ArgumentNullException(nameof(name));
+ if (username == null)
+ throw new ArgumentNullException(nameof(username));
+
+ {
+ var (result, message) = UsernameValidator.Validate(username);
+ if (!result)
+ {
+ throw new UsernameBadFormatException(username);
+ }
+ }
+
+ var user = await Database.Users.Where(u => u.Name == username).Select(u => new { u.Id }).SingleOrDefaultAsync();
+
+ if (user == null)
+ {
+ throw new UserNotExistException(username);
+ }
+
+ var userId = user.Id;
+
+ var timelineId = await FindTimelineId(name);
+
+ var timelineEntity = await Database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.OwnerId }).SingleAsync();
+
+ if (userId == timelineEntity.OwnerId)
+ return true;
+
+ var timelineMemberEntity = await Database.TimelineMembers.Where(m => m.TimelineId == timelineId && m.UserId == userId).SingleOrDefaultAsync();
+
+ return timelineMemberEntity != null;
+ }
+ }
+
+ public class PersonalTimelineService : BaseTimelineService, IPersonalTimelineService
+ {
+ public PersonalTimelineService(ILoggerFactory loggerFactory, DatabaseContext database, IClock clock)
+ : base(loggerFactory, database, clock)
+ {
+
+ }
+
+ protected override async Task<long> FindTimelineId(string name)
+ {
+ {
+ var (result, message) = UsernameValidator.Validate(name);
+ if (!result)
+ {
+ throw new TimelineNameBadFormatException(name, new UsernameBadFormatException(name, message));
+ }
+ }
+
+ var userEntity = await Database.Users.Where(u => u.Name == name).Select(u => new { u.Id }).SingleOrDefaultAsync();
+
+ if (userEntity == null)
+ {
+ throw new TimelineNotExistException(name, new UserNotExistException(name));
+ }
+
+ var userId = userEntity.Id;
+
+ 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
+ {
+ Name = null,
+ Description = null,
+ OwnerId = userId,
+ Visibility = TimelineVisibility.Register,
+ CreateTime = Clock.GetCurrentTime(),
+ };
+ Database.Timelines.Add(newTimelineEntity);
+ await Database.SaveChangesAsync();
+
+ return newTimelineEntity.Id;
+ }
+ }
+
+ public async Task<BaseTimelineInfo> GetTimeline(string username)
+ {
+ if (username == null)
+ throw new ArgumentNullException(nameof(username));
+
+ var timelineId = await FindTimelineId(username);
+
+ var timelineEntity = await Database.Timelines.Where(t => t.Id == timelineId).SingleAsync();
+
+ var timelineMemberEntities = await Database.TimelineMembers.Where(m => m.TimelineId == timelineId).Select(m => new { m.UserId }).ToListAsync();
+
+ var memberUsernameTasks = timelineMemberEntities.Select(m => Database.Users.Where(u => u.Id == m.UserId).Select(u => u.Name).SingleAsync()).ToArray();
+
+ var memberUsernames = await Task.WhenAll(memberUsernameTasks);
+
+ return new BaseTimelineInfo
+ {
+ Description = timelineEntity.Description ?? "",
+ Owner = username,
+ Visibility = timelineEntity.Visibility,
+ Members = memberUsernames.ToList()
+ };
+ }
+
+ }
+}
diff --git a/Timeline/Services/UsernameBadFormatException.cs b/Timeline/Services/UsernameBadFormatException.cs index 04354d22..d82bf962 100644 --- a/Timeline/Services/UsernameBadFormatException.cs +++ b/Timeline/Services/UsernameBadFormatException.cs @@ -9,8 +9,8 @@ namespace Timeline.Services public class UsernameBadFormatException : Exception
{
public UsernameBadFormatException() : base(Resources.Services.Exception.UsernameBadFormatException) { }
- public UsernameBadFormatException(string message) : base(message) { }
- public UsernameBadFormatException(string message, Exception inner) : base(message, inner) { }
+ public UsernameBadFormatException(string username) : this() { Username = username; }
+ public UsernameBadFormatException(string username, Exception inner) : base(Resources.Services.Exception.UsernameBadFormatException, inner) { Username = username; }
public UsernameBadFormatException(string username, string message) : base(message) { Username = username; }
public UsernameBadFormatException(string username, string message, Exception inner) : base(message, inner) { Username = username; }
diff --git a/Timeline/Startup.cs b/Timeline/Startup.cs index f6abf36d..b2e958f9 100644 --- a/Timeline/Startup.cs +++ b/Timeline/Startup.cs @@ -71,6 +71,8 @@ namespace Timeline services.AddUserAvatarService();
services.AddScoped<IUserDetailService, UserDetailService>();
+ services.AddScoped<IPersonalTimelineService, PersonalTimelineService>();
+
var databaseConfig = Configuration.GetSection(nameof(DatabaseConfig)).Get<DatabaseConfig>();
services.AddDbContext<DatabaseContext>(options =>
diff --git a/Timeline/Timeline.csproj b/Timeline/Timeline.csproj index bd195475..f3589c04 100644 --- a/Timeline/Timeline.csproj +++ b/Timeline/Timeline.csproj @@ -1,7 +1,6 @@ <Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
- <IsPackable>false</IsPackable>
<UserSecretsId>1f6fb74d-4277-4bc0-aeea-b1fc5ffb0b43</UserSecretsId>
<Authors>crupest</Authors>
@@ -24,8 +23,8 @@ <PackageReference Include="Microsoft.EntityFrameworkCore" Version="3.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Analyzers" Version="3.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.0.0">
- <PrivateAssets>all</PrivateAssets>
- <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="3.0.0-rc1.final" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql.Design" Version="1.1.2" />
@@ -34,139 +33,144 @@ </ItemGroup>
<ItemGroup>
- <Compile Update="Resources\Authentication\AuthHandler.Designer.cs">
- <DesignTime>True</DesignTime>
- <AutoGen>True</AutoGen>
- <DependentUpon>AuthHandler.resx</DependentUpon>
- </Compile>
- <Compile Update="Resources\Common.Designer.cs">
- <DesignTime>True</DesignTime>
- <AutoGen>True</AutoGen>
- <DependentUpon>Common.resx</DependentUpon>
- </Compile>
- <Compile Update="Resources\Controllers\Testing\TestingI18nController.Designer.cs">
- <DesignTime>True</DesignTime>
- <AutoGen>True</AutoGen>
- <DependentUpon>TestingI18nController.resx</DependentUpon>
- </Compile>
- <Compile Update="Resources\Controllers\TokenController.Designer.cs">
- <DesignTime>True</DesignTime>
- <AutoGen>True</AutoGen>
- <DependentUpon>TokenController.resx</DependentUpon>
- </Compile>
- <Compile Update="Resources\Controllers\UserAvatarController.Designer.cs">
- <DesignTime>True</DesignTime>
- <AutoGen>True</AutoGen>
- <DependentUpon>UserAvatarController.resx</DependentUpon>
- </Compile>
- <Compile Update="Resources\Controllers\UserController.Designer.cs">
- <DesignTime>True</DesignTime>
- <AutoGen>True</AutoGen>
- <DependentUpon>UserController.resx</DependentUpon>
- </Compile>
- <Compile Update="Resources\Filters.Designer.cs">
- <DesignTime>True</DesignTime>
- <AutoGen>True</AutoGen>
- <DependentUpon>Filters.resx</DependentUpon>
- </Compile>
- <Compile Update="Resources\Models\Http\Common.Designer.cs">
- <DesignTime>True</DesignTime>
- <AutoGen>True</AutoGen>
- <DependentUpon>Common.resx</DependentUpon>
- </Compile>
- <Compile Update="Resources\Models\Validation\UsernameValidator.Designer.cs">
- <DesignTime>True</DesignTime>
- <AutoGen>True</AutoGen>
- <DependentUpon>UsernameValidator.resx</DependentUpon>
- </Compile>
- <Compile Update="Resources\Models\Validation\Validator.Designer.cs">
- <DesignTime>True</DesignTime>
- <AutoGen>True</AutoGen>
- <DependentUpon>Validator.resx</DependentUpon>
- </Compile>
- <Compile Update="Resources\Services\Exception.Designer.cs">
- <DesignTime>True</DesignTime>
- <AutoGen>True</AutoGen>
- <DependentUpon>Exception.resx</DependentUpon>
- </Compile>
- <Compile Update="Resources\Services\UserAvatarService.Designer.cs">
- <DesignTime>True</DesignTime>
- <AutoGen>True</AutoGen>
- <DependentUpon>UserAvatarService.resx</DependentUpon>
- </Compile>
- <Compile Update="Resources\Services\UserDetailService.Designer.cs">
- <DesignTime>True</DesignTime>
- <AutoGen>True</AutoGen>
- <DependentUpon>UserDetailService.resx</DependentUpon>
- </Compile>
- <Compile Update="Resources\Services\UserService.Designer.cs">
- <DesignTime>True</DesignTime>
- <AutoGen>True</AutoGen>
- <DependentUpon>UserService.resx</DependentUpon>
- </Compile>
- </ItemGroup>
-
- <ItemGroup>
- <EmbeddedResource Update="Resources\Authentication\AuthHandler.resx">
- <Generator>ResXFileCodeGenerator</Generator>
- <LastGenOutput>AuthHandler.Designer.cs</LastGenOutput>
- </EmbeddedResource>
- <EmbeddedResource Update="Resources\Common.resx">
- <Generator>ResXFileCodeGenerator</Generator>
- <LastGenOutput>Common.Designer.cs</LastGenOutput>
- </EmbeddedResource>
- <EmbeddedResource Update="Resources\Controllers\Testing\TestingI18nController.resx">
- <Generator>ResXFileCodeGenerator</Generator>
- <LastGenOutput>TestingI18nController.Designer.cs</LastGenOutput>
- </EmbeddedResource>
- <EmbeddedResource Update="Resources\Controllers\TokenController.resx">
- <SubType>Designer</SubType>
- <Generator>ResXFileCodeGenerator</Generator>
- <LastGenOutput>TokenController.Designer.cs</LastGenOutput>
- </EmbeddedResource>
- <EmbeddedResource Update="Resources\Controllers\UserAvatarController.resx">
- <Generator>ResXFileCodeGenerator</Generator>
- <LastGenOutput>UserAvatarController.Designer.cs</LastGenOutput>
- </EmbeddedResource>
- <EmbeddedResource Update="Resources\Controllers\UserController.resx">
- <Generator>ResXFileCodeGenerator</Generator>
- <LastGenOutput>UserController.Designer.cs</LastGenOutput>
- </EmbeddedResource>
- <EmbeddedResource Update="Resources\Filters.resx">
- <Generator>ResXFileCodeGenerator</Generator>
- <LastGenOutput>Filters.Designer.cs</LastGenOutput>
- </EmbeddedResource>
- <EmbeddedResource Update="Resources\Models\Http\Common.resx">
- <Generator>ResXFileCodeGenerator</Generator>
- <LastGenOutput>Common.Designer.cs</LastGenOutput>
- </EmbeddedResource>
- <EmbeddedResource Update="Resources\Models\Validation\UsernameValidator.resx">
- <Generator>ResXFileCodeGenerator</Generator>
- <LastGenOutput>UsernameValidator.Designer.cs</LastGenOutput>
- </EmbeddedResource>
- <EmbeddedResource Update="Resources\Models\Validation\Validator.resx">
- <Generator>ResXFileCodeGenerator</Generator>
- <LastGenOutput>Validator.Designer.cs</LastGenOutput>
- </EmbeddedResource>
- <EmbeddedResource Update="Resources\Services\Exception.resx">
- <Generator>ResXFileCodeGenerator</Generator>
- <LastGenOutput>Exception.Designer.cs</LastGenOutput>
- </EmbeddedResource>
- <EmbeddedResource Update="Resources\Services\UserAvatarService.resx">
- <Generator>ResXFileCodeGenerator</Generator>
- <LastGenOutput>UserAvatarService.Designer.cs</LastGenOutput>
- </EmbeddedResource>
- <EmbeddedResource Update="Resources\Services\UserDetailService.resx">
- <Generator>ResXFileCodeGenerator</Generator>
- <LastGenOutput>UserDetailService.Designer.cs</LastGenOutput>
- </EmbeddedResource>
- <EmbeddedResource Update="Resources\Services\UserService.resx">
- <Generator>ResXFileCodeGenerator</Generator>
- <LastGenOutput>UserService.Designer.cs</LastGenOutput>
- </EmbeddedResource>
+ <Compile Update="Resources\Authentication\AuthHandler.Designer.cs">
+ <DesignTime>True</DesignTime>
+ <AutoGen>True</AutoGen>
+ <DependentUpon>AuthHandler.resx</DependentUpon>
+ </Compile>
+ <Compile Update="Resources\Common.Designer.cs">
+ <DesignTime>True</DesignTime>
+ <AutoGen>True</AutoGen>
+ <DependentUpon>Common.resx</DependentUpon>
+ </Compile>
+ <Compile Update="Resources\Controllers\Testing\TestingI18nController.Designer.cs">
+ <DesignTime>True</DesignTime>
+ <AutoGen>True</AutoGen>
+ <DependentUpon>TestingI18nController.resx</DependentUpon>
+ </Compile>
+ <Compile Update="Resources\Controllers\TimelineController.Designer.cs">
+ <DesignTime>True</DesignTime>
+ <AutoGen>True</AutoGen>
+ <DependentUpon>TimelineController.resx</DependentUpon>
+ </Compile>
+ <Compile Update="Resources\Controllers\TokenController.Designer.cs">
+ <DesignTime>True</DesignTime>
+ <AutoGen>True</AutoGen>
+ <DependentUpon>TokenController.resx</DependentUpon>
+ </Compile>
+ <Compile Update="Resources\Controllers\UserAvatarController.Designer.cs">
+ <DesignTime>True</DesignTime>
+ <AutoGen>True</AutoGen>
+ <DependentUpon>UserAvatarController.resx</DependentUpon>
+ </Compile>
+ <Compile Update="Resources\Controllers\UserController.Designer.cs">
+ <DesignTime>True</DesignTime>
+ <AutoGen>True</AutoGen>
+ <DependentUpon>UserController.resx</DependentUpon>
+ </Compile>
+ <Compile Update="Resources\Filters.Designer.cs">
+ <DesignTime>True</DesignTime>
+ <AutoGen>True</AutoGen>
+ <DependentUpon>Filters.resx</DependentUpon>
+ </Compile>
+ <Compile Update="Resources\Models\Http\Common.Designer.cs">
+ <DesignTime>True</DesignTime>
+ <AutoGen>True</AutoGen>
+ <DependentUpon>Common.resx</DependentUpon>
+ </Compile>
+ <Compile Update="Resources\Models\Validation\UsernameValidator.Designer.cs">
+ <DesignTime>True</DesignTime>
+ <AutoGen>True</AutoGen>
+ <DependentUpon>UsernameValidator.resx</DependentUpon>
+ </Compile>
+ <Compile Update="Resources\Models\Validation\Validator.Designer.cs">
+ <DesignTime>True</DesignTime>
+ <AutoGen>True</AutoGen>
+ <DependentUpon>Validator.resx</DependentUpon>
+ </Compile>
+ <Compile Update="Resources\Services\Exception.Designer.cs">
+ <DesignTime>True</DesignTime>
+ <AutoGen>True</AutoGen>
+ <DependentUpon>Exception.resx</DependentUpon>
+ </Compile>
+ <Compile Update="Resources\Services\UserAvatarService.Designer.cs">
+ <DesignTime>True</DesignTime>
+ <AutoGen>True</AutoGen>
+ <DependentUpon>UserAvatarService.resx</DependentUpon>
+ </Compile>
+ <Compile Update="Resources\Services\UserDetailService.Designer.cs">
+ <DesignTime>True</DesignTime>
+ <AutoGen>True</AutoGen>
+ <DependentUpon>UserDetailService.resx</DependentUpon>
+ </Compile>
+ <Compile Update="Resources\Services\UserService.Designer.cs">
+ <DesignTime>True</DesignTime>
+ <AutoGen>True</AutoGen>
+ <DependentUpon>UserService.resx</DependentUpon>
+ </Compile>
</ItemGroup>
<ItemGroup>
- <Folder Include="Resources\Filters\" />
+ <EmbeddedResource Update="Resources\Authentication\AuthHandler.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>AuthHandler.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ <EmbeddedResource Update="Resources\Common.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>Common.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ <EmbeddedResource Update="Resources\Controllers\Testing\TestingI18nController.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>TestingI18nController.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ <EmbeddedResource Update="Resources\Controllers\TimelineController.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>TimelineController.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ <EmbeddedResource Update="Resources\Controllers\TokenController.resx">
+ <SubType>Designer</SubType>
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>TokenController.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ <EmbeddedResource Update="Resources\Controllers\UserAvatarController.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>UserAvatarController.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ <EmbeddedResource Update="Resources\Controllers\UserController.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>UserController.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ <EmbeddedResource Update="Resources\Filters.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>Filters.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ <EmbeddedResource Update="Resources\Models\Http\Common.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>Common.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ <EmbeddedResource Update="Resources\Models\Validation\UsernameValidator.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>UsernameValidator.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ <EmbeddedResource Update="Resources\Models\Validation\Validator.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>Validator.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ <EmbeddedResource Update="Resources\Services\Exception.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>Exception.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ <EmbeddedResource Update="Resources\Services\UserAvatarService.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>UserAvatarService.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ <EmbeddedResource Update="Resources\Services\UserDetailService.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>UserDetailService.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ <EmbeddedResource Update="Resources\Services\UserService.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>UserService.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
</ItemGroup>
</Project>
|