using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using System; using System.Collections.Generic; using System.Threading.Tasks; using System.ComponentModel.DataAnnotations; using Timeline.Filters; using Timeline.Helpers.Cache; using Timeline.Models; using Timeline.Models.Http; using Timeline.Models.Mapper; using Timeline.Models.Validation; using Timeline.Services; using Timeline.Entities; namespace Timeline.Controllers { /// /// Operations about timeline. /// [ApiController] [Route("timelines/{timeline}/posts")] [CatchTimelineNotExistException] [CatchTimelinePostNotExistException] [CatchTimelinePostDataNotExistException] [ProducesErrorResponseType(typeof(CommonResponse))] public class TimelinePostController : Controller { private readonly ITimelineService _timelineService; private readonly ITimelinePostService _postService; private readonly TimelineMapper _timelineMapper; /// /// /// public TimelinePostController(ITimelineService timelineService, ITimelinePostService timelinePostService, TimelineMapper timelineMapper) { _timelineService = timelineService; _postService = timelinePostService; _timelineMapper = timelineMapper; } private bool UserHasAllTimelineManagementPermission => this.UserHasPermission(UserPermission.AllTimelineManagement); private Task Map(TimelinePostEntity post, string timelineName) { return _timelineMapper.MapToHttp(post, timelineName, Url, this.GetOptionalUserId(), UserHasAllTimelineManagementPermission); } private Task> Map(List post, string timelineName) { return _timelineMapper.MapToHttp(post, timelineName, Url, this.GetOptionalUserId(), UserHasAllTimelineManagementPermission); } /// /// 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] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task>> List([FromRoute][GeneralTimelineName] string timeline, [FromQuery] DateTime? modifiedSince, [FromQuery] bool? includeDeleted) { var timelineId = await _timelineService.GetTimelineIdByName(timeline); if (!UserHasAllTimelineManagementPermission && !await _timelineService.HasReadPermission(timelineId, this.GetOptionalUserId())) { return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); } var posts = await _postService.GetPosts(timelineId, modifiedSince, includeDeleted ?? false); var result = await Map(posts, timeline); return result; } /// /// Get a post of a timeline. /// /// The name of the timeline. /// The post id. /// The post. [HttpGet("{post}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> Get([FromRoute][GeneralTimelineName] string timeline, [FromRoute(Name = "post")] long postId) { var timelineId = await _timelineService.GetTimelineIdByName(timeline); if (!UserHasAllTimelineManagementPermission && !await _timelineService.HasReadPermission(timelineId, this.GetOptionalUserId())) { return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); } var post = await _postService.GetPost(timelineId, postId); var result = await Map(post, timeline); return result; } /// /// Get the first data of a post. /// /// Timeline name. /// The id of the post. /// The data. [HttpGet("{post}/data")] [Produces(MimeTypes.ImagePng, MimeTypes.ImageJpeg, MimeTypes.ImageGif, MimeTypes.ImageWebp, MimeTypes.TextPlain, MimeTypes.TextMarkdown, MimeTypes.TextPlain, MimeTypes.ApplicationJson)] [ProducesResponseType(typeof(byte[]), StatusCodes.Status200OK)] [ProducesResponseType(typeof(void), StatusCodes.Status304NotModified)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> DataIndexGet([FromRoute][GeneralTimelineName] string timeline, [FromRoute] long post) { return await DataGet(timeline, post, 0); } /// /// Get the data of a post. Usually a image post. /// /// Timeline name. /// The id of the post. /// Index of the data. /// The data. [HttpGet("{post}/data/{data_index}")] [Produces(MimeTypes.ImagePng, MimeTypes.ImageJpeg, MimeTypes.ImageGif, MimeTypes.ImageWebp, MimeTypes.TextPlain, MimeTypes.TextMarkdown, MimeTypes.TextPlain, MimeTypes.ApplicationJson)] [ProducesResponseType(typeof(byte[]), StatusCodes.Status200OK)] [ProducesResponseType(typeof(void), StatusCodes.Status304NotModified)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task DataGet([FromRoute][GeneralTimelineName] string timeline, [FromRoute] long post, [FromRoute(Name = "data_index")][Range(0, 100)] long dataIndex) { var timelineId = await _timelineService.GetTimelineIdByName(timeline); if (!UserHasAllTimelineManagementPermission && !await _timelineService.HasReadPermission(timelineId, this.GetOptionalUserId())) { return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); } return await DataCacheHelper.GenerateActionResult(this, () => _postService.GetPostDataDigest(timelineId, post, dataIndex), () => _postService.GetPostData(timelineId, post, dataIndex) ); } /// /// Create a new post. /// /// Timeline name. /// /// Info of new post. [HttpPost] [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] public async Task> Post([FromRoute][GeneralTimelineName] string timeline, [FromBody] HttpTimelinePostCreateRequest body) { var timelineId = await _timelineService.GetTimelineIdByName(timeline); var userId = this.GetUserId(); if (!UserHasAllTimelineManagementPermission && !await _timelineService.IsMemberOf(timelineId, userId)) { return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); } var createRequest = new TimelinePostCreateRequest() { Time = body.Time, Color = body.Color }; for (int i = 0; i < body.DataList.Count; i++) { var data = body.DataList[i]; if (data is null) return BadRequest(new CommonResponse(ErrorCodes.Common.InvalidModel, $"Data at index {i} is null.")); try { var d = Convert.FromBase64String(data.Data); createRequest.DataList.Add(new TimelinePostCreateRequestData(data.ContentType, d)); } catch (FormatException) { return BadRequest(new CommonResponse(ErrorCodes.Common.InvalidModel, $"Data at index {i} is not a valid base64 string.")); } } try { var post = await _postService.CreatePost(timelineId, userId, createRequest); var result = await Map(post, timeline); return result; } catch (TimelinePostCreateDataException e) { return BadRequest(new CommonResponse(ErrorCodes.Common.InvalidModel, $"Data at index {e.Index} is invalid. {e.Message}")); } } /// /// Update a post except content. /// /// Timeline name. /// Post id. /// Request body. /// New info of post. [HttpPatch("{post}")] [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] public async Task> Patch([FromRoute][GeneralTimelineName] string timeline, [FromRoute] long post, [FromBody] HttpTimelinePostPatchRequest body) { var timelineId = await _timelineService.GetTimelineIdByName(timeline); if (!UserHasAllTimelineManagementPermission && !await _postService.HasPostModifyPermission(timelineId, post, this.GetUserId(), true)) { return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); } var entity = await _postService.PatchPost(timelineId, post, new TimelinePostPatchRequest { Time = body.Time, Color = body.Color }); var result = await Map(entity, timeline); return Ok(result); } /// /// Delete a post. /// /// Timeline name. /// Post id. /// Info of deletion. [HttpDelete("{post}")] [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] public async Task Delete([FromRoute][GeneralTimelineName] string timeline, [FromRoute] long post) { var timelineId = await _timelineService.GetTimelineIdByName(timeline); if (!UserHasAllTimelineManagementPermission && !await _postService.HasPostModifyPermission(timelineId, post, this.GetUserId(), true)) { return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); } await _postService.DeletePost(timelineId, post); return Ok(); } } }