using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; using Timeline.Entities; using Timeline.Filters; using Timeline.Helpers.Cache; using Timeline.Models; using Timeline.Models.Http; using Timeline.Models.Validation; using Timeline.Services.Mapper; using Timeline.Services.Timeline; using Timeline.Services.User; using Timeline.SignalRHub; namespace Timeline.Controllers { /// /// Operations about timeline. /// [ApiController] [Route("timelines/{timeline}/posts")] [CatchMultipleTimelineException] [ProducesErrorResponseType(typeof(CommonResponse))] public class TimelinePostController : MyControllerBase { private readonly ILogger _logger; private readonly ITimelineService _timelineService; private readonly ITimelinePostService _postService; private readonly IGenericMapper _mapper; private readonly MarkdownProcessor _markdownProcessor; private readonly IHubContext _timelineHubContext; public TimelinePostController(ILogger logger, ITimelineService timelineService, ITimelinePostService timelinePostService, IGenericMapper mapper, MarkdownProcessor markdownProcessor, IHubContext timelineHubContext) { _logger = logger; _timelineService = timelineService; _postService = timelinePostService; _mapper = mapper; _markdownProcessor = markdownProcessor; _timelineHubContext = timelineHubContext; } private bool UserHasAllTimelineManagementPermission => UserHasPermission(UserPermission.AllTimelineManagement); private Task Map(TimelinePostEntity post) { return _mapper.MapAsync(post, Url, User); } private Task> Map(List posts) { return _mapper.MapListAsync(posts, Url, User); } /// /// 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. /// Page number, starting from 0. Null to get all. /// Post number per page. Default is 20. /// 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, [FromQuery][Range(0, int.MaxValue)] int? page, [FromQuery][Range(1, int.MaxValue)] int? numberPerPage) { var timelineId = await _timelineService.GetTimelineIdByNameAsync(timeline); if (!UserHasAllTimelineManagementPermission && !await _timelineService.HasReadPermissionAsync(timelineId, GetOptionalAuthUserId())) { return ForbidWithCommonResponse(); } var posts = await _postService.GetPostsAsync(timelineId, modifiedSince, includeDeleted ?? false, page, numberPerPage); var result = await Map(posts); 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.GetTimelineIdByNameAsync(timeline); if (!UserHasAllTimelineManagementPermission && !await _timelineService.HasReadPermissionAsync(timelineId, GetOptionalAuthUserId())) { return ForbidWithCommonResponse(); } var post = await _postService.GetPostAsync(timelineId, postId); var result = await Map(post); 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.GetTimelineIdByNameAsync(timeline); if (!UserHasAllTimelineManagementPermission && !await _timelineService.HasReadPermissionAsync(timelineId, GetOptionalAuthUserId())) { return ForbidWithCommonResponse(); } return await DataCacheHelper.GenerateActionResult(this, () => _postService.GetPostDataDigestAsync(timelineId, post, dataIndex), async () => { var data = await _postService.GetPostDataAsync(timelineId, post, dataIndex); if (data.ContentType == MimeTypes.TextMarkdown) { return new ByteData(_markdownProcessor.Process(data.Data, Url, timeline, post), data.ContentType); } return data; } ); } /// /// 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.GetTimelineIdByNameAsync(timeline); var userId = GetAuthUserId(); if (!UserHasAllTimelineManagementPermission && !await _timelineService.IsMemberOfAsync(timelineId, userId)) { return ForbidWithCommonResponse(); } 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.CreatePostAsync(timelineId, userId, createRequest); var group = TimelineHub.GenerateTimelinePostChangeListeningGroupName(timeline); await _timelineHubContext.Clients.Group(group).SendAsync(nameof(ITimelineClient.OnTimelinePostChanged), timeline); _logger.LogInformation("Notify group {0} of timeline post change.", group); var result = await Map(post); 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.GetTimelineIdByNameAsync(timeline); if (!UserHasAllTimelineManagementPermission && !await _postService.HasPostModifyPermissionAsync(timelineId, post, GetAuthUserId(), true)) { return ForbidWithCommonResponse(); } var entity = await _postService.PatchPostAsync(timelineId, post, new TimelinePostPatchRequest { Time = body.Time, Color = body.Color }); var result = await Map(entity); 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.GetTimelineIdByNameAsync(timeline); if (!UserHasAllTimelineManagementPermission && !await _postService.HasPostModifyPermissionAsync(timelineId, post, GetAuthUserId(), true)) { return ForbidWithCommonResponse(); } await _postService.DeletePostAsync(timelineId, post); return DeleteWithCommonDeleteResponse(); } } }