using AutoMapper;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using Timeline.Filters;
using Timeline.Helpers;
using Timeline.Models;
using Timeline.Models.Http;
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 ILogger _logger;
private readonly IUserService _userService;
private readonly ITimelineService _service;
private readonly IMapper _mapper;
///
///
///
public TimelineController(ILogger logger, IUserService userService, ITimelineService service, IMapper mapper)
{
_logger = logger;
_userService = userService;
_service = service;
_mapper = mapper;
}
///
/// 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 = _mapper.Map>(timelines);
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/{name}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status304NotModified)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task> TimelineGet([FromRoute][GeneralTimelineName] string name, [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 != null)
{
ifModifiedSince = headerIfModifiedSince.Value;
}
bool returnNotModified = false;
if (ifModifiedSince.HasValue)
{
var lastModified = await _service.GetTimelineLastModifiedTime(name);
if (lastModified < ifModifiedSince.Value)
{
if (checkUniqueId != null)
{
var uniqueId = await _service.GetTimelineUniqueId(name);
if (uniqueId == checkUniqueId)
{
returnNotModified = true;
}
}
else
{
returnNotModified = true;
}
}
}
if (returnNotModified)
{
return StatusCode(StatusCodes.Status304NotModified);
}
else
{
var timeline = await _service.GetTimeline(name);
var result = _mapper.Map(timeline);
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/{name}/posts")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task>> PostListGet([FromRoute][GeneralTimelineName] string name, [FromQuery] DateTime? modifiedSince, [FromQuery] bool? includeDeleted)
{
if (!this.IsAdministrator() && !await _service.HasReadPermission(name, this.GetOptionalUserId()))
{
return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid());
}
List posts = await _service.GetPosts(name, modifiedSince, includeDeleted ?? false);
var result = _mapper.Map>(posts);
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/{name}/posts/{id}/data")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(void), StatusCodes.Status304NotModified)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task PostDataGet([FromRoute][GeneralTimelineName] string name, [FromRoute] long id, [FromHeader(Name = "If-None-Match")] string? ifNoneMatch)
{
_ = ifNoneMatch;
if (!this.IsAdministrator() && !await _service.HasReadPermission(name, this.GetOptionalUserId()))
{
return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid());
}
try
{
return await DataCacheHelper.GenerateActionResult(this, () => _service.GetPostDataETag(name, id), async () =>
{
var data = await _service.GetPostData(name, id);
return data;
});
}
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/{name}/posts")]
[Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task> PostPost([FromRoute][GeneralTimelineName] string name, [FromBody] TimelinePostCreateRequest body)
{
var id = this.GetUserId();
if (!this.IsAdministrator() && !await _service.IsMemberOf(name, id))
{
return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid());
}
var content = body.Content;
TimelinePost post;
if (content.Type == TimelinePostContentTypes.Text)
{
var text = content.Text;
if (text == null)
{
return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_TextContentTextRequired));
}
post = await _service.CreateTextPost(name, id, text, body.Time);
}
else if (content.Type == TimelinePostContentTypes.Image)
{
var base64Data = content.Data;
if (base64Data == null)
{
return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_ImageContentDataRequired));
}
byte[] data;
try
{
data = Convert.FromBase64String(base64Data);
}
catch (FormatException)
{
return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_ImageContentDataNotBase64));
}
try
{
post = await _service.CreateImagePost(name, id, data, body.Time);
}
catch (ImageException)
{
return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_ImageContentDataNotImage));
}
}
else
{
return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_ContentUnknownType));
}
var result = _mapper.Map(post);
return result;
}
///
/// Delete a post.
///
/// Timeline name.
/// Post id.
/// Info of deletion.
[HttpDelete("timelines/{name}/posts/{id}")]
[Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task> PostDelete([FromRoute][GeneralTimelineName] string name, [FromRoute] long id)
{
if (!this.IsAdministrator() && !await _service.HasPostModifyPermission(name, id, this.GetUserId()))
{
return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid());
}
try
{
await _service.DeletePost(name, id);
return CommonDeleteResponse.Delete();
}
catch (TimelinePostNotExistException)
{
return CommonDeleteResponse.NotExist();
}
}
///
/// Change properties of a timeline.
///
/// Timeline name.
///
/// The new info.
[HttpPatch("timelines/{name}")]
[Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task> TimelinePatch([FromRoute][GeneralTimelineName] string name, [FromBody] TimelinePatchRequest body)
{
if (!this.IsAdministrator() && !(await _service.HasManagePermission(name, this.GetUserId())))
{
return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid());
}
await _service.ChangeProperty(name, _mapper.Map(body));
var timeline = await _service.GetTimeline(name);
var result = _mapper.Map(timeline);
return result;
}
///
/// Add a member to timeline.
///
/// Timeline name.
/// The new member's username.
[HttpPut("timelines/{name}/members/{member}")]
[Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task TimelineMemberPut([FromRoute][GeneralTimelineName] string name, [FromRoute][Username] string member)
{
if (!this.IsAdministrator() && !(await _service.HasManagePermission(name, this.GetUserId())))
{
return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid());
}
try
{
await _service.ChangeMember(name, new List { member }, null);
return Ok();
}
catch (UserNotExistException)
{
return BadRequest(ErrorResponse.TimelineController.MemberPut_NotExist());
}
}
///
/// Remove a member from timeline.
///
/// Timeline name.
/// The member's username.
[HttpDelete("timelines/{name}/members/{member}")]
[Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task TimelineMemberDelete([FromRoute][GeneralTimelineName] string name, [FromRoute][Username] string member)
{
if (!this.IsAdministrator() && !(await _service.HasManagePermission(name, this.GetUserId())))
{
return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid());
}
try
{
await _service.ChangeMember(name, null, new List { member });
return Ok(CommonDeleteResponse.Delete());
}
catch (UserNotExistException)
{
return Ok(CommonDeleteResponse.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 = _mapper.Map(timeline);
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/{name}")]
[Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task> TimelineDelete([FromRoute][TimelineName] string name)
{
if (!this.IsAdministrator() && !(await _service.HasManagePermission(name, this.GetUserId())))
{
return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid());
}
try
{
await _service.DeleteTimeline(name);
return CommonDeleteResponse.Delete();
}
catch (TimelineNotExistException)
{
return CommonDeleteResponse.NotExist();
}
}
}
}