using AutoMapper; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; using Timeline.Entities; using Timeline.Filters; using Timeline.Helpers; using Timeline.Models; using Timeline.Models.Http; using Timeline.Models.Mapper; using Timeline.Models.Validation; using Timeline.Services; using Timeline.Services.Exceptions; namespace Timeline.Controllers { /// /// Operations about timeline. /// [ApiController] [CatchTimelineNotExistException] [ProducesErrorResponseType(typeof(CommonResponse))] public class TimelineController : Controller { private readonly IUserService _userService; private readonly ITimelineService _service; private readonly ITimelinePostService _postService; private readonly IMapper _mapper; /// /// /// public TimelineController(IUserService userService, ITimelineService service, ITimelinePostService timelinePostService, IMapper mapper) { _userService = userService; _service = service; _postService = timelinePostService; _mapper = mapper; } private bool UserHasAllTimelineManagementPermission => this.UserHasPermission(UserPermission.AllTimelineManagement); /// /// List all timelines. /// /// A username. If set, only timelines related to the user will return. /// Specify the relation type, may be 'own' or 'join'. If not set, both type will return. /// "Private" or "Register" or "Public". If set, only timelines whose visibility is specified one will return. /// The timeline list. [HttpGet("timelines")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] public async Task>> TimelineList([FromQuery][Username] string? relate, [FromQuery][RegularExpression("(own)|(join)")] string? relateType, [FromQuery] string? visibility) { List? visibilityFilter = null; if (visibility != null) { visibilityFilter = new List(); var items = visibility.Split('|'); foreach (var item in items) { if (item.Equals(nameof(TimelineVisibility.Private), StringComparison.OrdinalIgnoreCase)) { if (!visibilityFilter.Contains(TimelineVisibility.Private)) visibilityFilter.Add(TimelineVisibility.Private); } else if (item.Equals(nameof(TimelineVisibility.Register), StringComparison.OrdinalIgnoreCase)) { if (!visibilityFilter.Contains(TimelineVisibility.Register)) visibilityFilter.Add(TimelineVisibility.Register); } else if (item.Equals(nameof(TimelineVisibility.Public), StringComparison.OrdinalIgnoreCase)) { if (!visibilityFilter.Contains(TimelineVisibility.Public)) visibilityFilter.Add(TimelineVisibility.Public); } else { return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_QueryVisibilityUnknown, item)); } } } TimelineUserRelationship? relationship = null; if (relate != null) { try { var relatedUserId = await _userService.GetUserIdByUsername(relate); relationship = new TimelineUserRelationship(relateType switch { "own" => TimelineUserRelationshipType.Own, "join" => TimelineUserRelationshipType.Join, _ => TimelineUserRelationshipType.Default }, relatedUserId); } catch (UserNotExistException) { return BadRequest(ErrorResponse.TimelineController.QueryRelateNotExist()); } } var timelines = await _service.GetTimelines(relationship, visibilityFilter); var result = timelines.MapToHttp(Url); return result; } /// /// Get info of a timeline. /// /// The timeline name. /// A unique id. If specified and if-modified-since is also specified, the timeline info will return when unique id is not the specified one even if it is not modified. /// Same effect as If-Modified-Since header and take precedence than it. /// If specified, will return 304 if not modified. /// The timeline info. [HttpGet("timelines/{timeline}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status304NotModified)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> TimelineGet([FromRoute][GeneralTimelineName] string timeline, [FromQuery] string? checkUniqueId, [FromQuery(Name = "ifModifiedSince")] DateTime? queryIfModifiedSince, [FromHeader(Name = "If-Modified-Since")] DateTime? headerIfModifiedSince) { DateTime? ifModifiedSince = null; if (queryIfModifiedSince.HasValue) { ifModifiedSince = queryIfModifiedSince.Value; } else if (headerIfModifiedSince is not null) { ifModifiedSince = headerIfModifiedSince.Value; } var timelineId = await _service.GetTimelineIdByName(timeline); bool returnNotModified = false; if (ifModifiedSince.HasValue) { var lastModified = await _service.GetTimelineLastModifiedTime(timelineId); if (lastModified < ifModifiedSince.Value) { if (checkUniqueId != null) { var uniqueId = await _service.GetTimelineUniqueId(timelineId); if (uniqueId == checkUniqueId) { returnNotModified = true; } } else { returnNotModified = true; } } } if (returnNotModified) { return StatusCode(StatusCodes.Status304NotModified); } else { var t = await _service.GetTimeline(timelineId); var result = t.MapToHttp(Url); return result; } } /// /// Get posts of a timeline. /// /// The name of the timeline. /// If set, only posts modified since the time will return. /// If set to true, deleted post will also return. /// The post list. [HttpGet("timelines/{timeline}/posts")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task>> PostListGet([FromRoute][GeneralTimelineName] string timeline, [FromQuery] DateTime? modifiedSince, [FromQuery] bool? includeDeleted) { var timelineId = await _service.GetTimelineIdByName(timeline); if (!UserHasAllTimelineManagementPermission && !await _service.HasReadPermission(timelineId, this.GetOptionalUserId())) { return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); } var posts = await _postService.GetPosts(timelineId, modifiedSince, includeDeleted ?? false); var result = posts.MapToHttp(timeline, Url); return result; } /// /// Get the data of a post. Usually a image post. /// /// Timeline name. /// The id of the post. /// If-None-Match header. /// The data. [HttpGet("timelines/{timeline}/posts/{post}/data")] [Produces("image/png", "image/jpeg", "image/gif", "image/webp", "application/json", "text/json")] [ProducesResponseType(typeof(byte[]), StatusCodes.Status200OK)] [ProducesResponseType(typeof(void), StatusCodes.Status304NotModified)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task PostDataGet([FromRoute][GeneralTimelineName] string timeline, [FromRoute] long post, [FromHeader(Name = "If-None-Match")] string? ifNoneMatch) { _ = ifNoneMatch; var timelineId = await _service.GetTimelineIdByName(timeline); if (!UserHasAllTimelineManagementPermission && !await _service.HasReadPermission(timelineId, this.GetOptionalUserId())) { return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); } try { return await DataCacheHelper.GenerateActionResult(this, () => _postService.GetPostDataETag(timelineId, post), async () => await _postService.GetPostData(timelineId, post)); } catch (TimelinePostNotExistException) { return NotFound(ErrorResponse.TimelineController.PostNotExist()); } catch (TimelinePostNoDataException) { return BadRequest(ErrorResponse.TimelineController.PostNoData()); } } /// /// Create a new post. /// /// Timeline name. /// /// Info of new post. [HttpPost("timelines/{timeline}/posts")] [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] public async Task> PostPost([FromRoute][GeneralTimelineName] string timeline, [FromBody] HttpTimelinePostCreateRequest body) { var timelineId = await _service.GetTimelineIdByName(timeline); var userId = this.GetUserId(); if (!UserHasAllTimelineManagementPermission && !await _service.IsMemberOf(timelineId, userId)) { return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); } var content = body.Content; TimelinePostEntity post; if (content.Type == TimelinePostContentTypes.Text) { var text = content.Text; if (text == null) { return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_TextContentTextRequired)); } post = await _postService.CreateTextPost(timelineId, userId, text, body.Time); } else if (content.Type == TimelinePostContentTypes.Image) { var base64Data = content.Data; if (base64Data == null) { return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_ImageContentDataRequired)); } byte[] data; try { data = Convert.FromBase64String(base64Data); } catch (FormatException) { return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_ImageContentDataNotBase64)); } try { post = await _postService.CreateImagePost(timelineId, userId, data, body.Time); } catch (ImageException) { return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_ImageContentDataNotImage)); } } else { return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_ContentUnknownType)); } var result = post.MapToHttp(timeline, Url); return result; } /// /// Delete a post. /// /// Timeline name. /// Post id. /// Info of deletion. [HttpDelete("timelines/{timeline}/posts/{post}")] [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] public async Task PostDelete([FromRoute][GeneralTimelineName] string timeline, [FromRoute] long post) { var timelineId = await _service.GetTimelineIdByName(timeline); try { if (!UserHasAllTimelineManagementPermission && !await _postService.HasPostModifyPermission(timelineId, post, this.GetUserId(), true)) { return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); } await _postService.DeletePost(timelineId, post); return Ok(); } catch (TimelinePostNotExistException) { return BadRequest(ErrorResponse.TimelineController.PostNotExist()); } } /// /// Change properties of a timeline. /// /// Timeline name. /// /// The new info. [HttpPatch("timelines/{timeline}")] [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] public async Task> TimelinePatch([FromRoute][GeneralTimelineName] string timeline, [FromBody] HttpTimelinePatchRequest body) { var timelineId = await _service.GetTimelineIdByName(timeline); if (!UserHasAllTimelineManagementPermission && !await _service.HasManagePermission(timelineId, this.GetUserId())) { return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); } await _service.ChangeProperty(timelineId, _mapper.Map(body)); var t = await _service.GetTimeline(timelineId); var result = t.MapToHttp(Url); return result; } /// /// Add a member to timeline. /// /// Timeline name. /// The new member's username. [HttpPut("timelines/{timeline}/members/{member}")] [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] public async Task> TimelineMemberPut([FromRoute][GeneralTimelineName] string timeline, [FromRoute][Username] string member) { var timelineId = await _service.GetTimelineIdByName(timeline); if (!UserHasAllTimelineManagementPermission && !(await _service.HasManagePermission(timelineId, this.GetUserId()))) { return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); } try { var userId = await _userService.GetUserIdByUsername(member); var create = await _service.AddMember(timelineId, userId); return Ok(CommonPutResponse.Create(create)); } catch (UserNotExistException) { return BadRequest(ErrorResponse.UserCommon.NotExist()); } } /// /// Remove a member from timeline. /// /// Timeline name. /// The member's username. [HttpDelete("timelines/{timeline}/members/{member}")] [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] public async Task TimelineMemberDelete([FromRoute][GeneralTimelineName] string timeline, [FromRoute][Username] string member) { var timelineId = await _service.GetTimelineIdByName(timeline); if (!UserHasAllTimelineManagementPermission && !(await _service.HasManagePermission(timelineId, this.GetUserId()))) { return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); } try { var userId = await _userService.GetUserIdByUsername(member); var delete = await _service.RemoveMember(timelineId, userId); return Ok(CommonDeleteResponse.Create(delete)); } catch (UserNotExistException) { return BadRequest(ErrorResponse.UserCommon.NotExist()); } } /// /// Create a timeline. /// /// /// Info of new timeline. [HttpPost("timelines")] [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] public async Task> TimelineCreate([FromBody] TimelineCreateRequest body) { var userId = this.GetUserId(); try { var timeline = await _service.CreateTimeline(body.Name, userId); var result = timeline.MapToHttp(Url); return result; } catch (EntityAlreadyExistException e) when (e.EntityName == EntityNames.Timeline) { return BadRequest(ErrorResponse.TimelineController.NameConflict()); } } /// /// Delete a timeline. /// /// Timeline name. /// Info of deletion. [HttpDelete("timelines/{timeline}")] [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] public async Task TimelineDelete([FromRoute][TimelineName] string timeline) { var timelineId = await _service.GetTimelineIdByName(timeline); if (!UserHasAllTimelineManagementPermission && !(await _service.HasManagePermission(timelineId, this.GetUserId()))) { return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); } try { await _service.DeleteTimeline(timelineId); return Ok(); } catch (TimelineNotExistException) { return BadRequest(ErrorResponse.TimelineController.NotExist()); } } [HttpPost("timelineop/changename")] [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] public async Task> TimelineOpChangeName([FromBody] HttpTimelineChangeNameRequest body) { var timelineId = await _service.GetTimelineIdByName(body.OldName); if (!UserHasAllTimelineManagementPermission && !(await _service.HasManagePermission(timelineId, this.GetUserId()))) { return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); } try { await _service.ChangeTimelineName(timelineId, body.NewName); var timeline = await _service.GetTimeline(timelineId); return Ok(timeline.MapToHttp(Url)); } catch (EntityAlreadyExistException) { return BadRequest(ErrorResponse.TimelineController.NameConflict()); } } } }