using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Timeline.Entities;
using Timeline.Models;
using Timeline.Models.Validation;
using Timeline.Services.Exceptions;
using static Timeline.Resources.Services.TimelineService;
namespace Timeline.Services
{
public static class TimelineHelper
{
public static string ExtractTimelineName(string name, out bool isPersonal)
{
if (name.StartsWith("@", StringComparison.OrdinalIgnoreCase))
{
isPersonal = true;
return name[1..];
}
else
{
isPersonal = false;
return name;
}
}
}
public enum TimelineUserRelationshipType
{
Own = 0b1,
Join = 0b10,
Default = Own | Join
}
public class TimelineUserRelationship
{
public TimelineUserRelationship(TimelineUserRelationshipType type, long userId)
{
Type = type;
UserId = userId;
}
public TimelineUserRelationshipType Type { get; set; }
public long UserId { get; set; }
}
public class TimelineChangePropertyParams
{
public string? Name { get; set; }
public string? Title { get; set; }
public string? Description { get; set; }
public TimelineVisibility? Visibility { get; set; }
public string? Color { get; set; }
}
///
/// This define the interface of both personal timeline and ordinary timeline.
///
public interface ITimelineService : IBasicTimelineService
{
///
/// Get the timeline info.
///
/// Id of timeline.
/// The timeline info.
/// Thrown when timeline does not exist.
Task GetTimeline(long id);
///
/// Set the properties of a timeline.
///
/// The id of the timeline.
/// The new properties. Null member means not to change.
/// Thrown when is null.
/// Thrown when timeline with given id does not exist.
/// Thrown when a timeline with new name already exists.
Task ChangeProperty(long id, TimelineChangePropertyParams newProperties);
///
/// Add a member to timeline.
///
/// Timeline id.
/// User id.
/// True if the memeber was added. False if it is already a member.
/// Thrown when timeline does not exist.
/// Thrown when the user does not exist.
Task AddMember(long timelineId, long userId);
///
/// Remove a member from timeline.
///
/// Timeline id.
/// User id.
/// True if the memeber was removed. False if it was not a member before.
/// Thrown when timeline does not exist.
/// Thrown when the user does not exist.
Task RemoveMember(long timelineId, long userId);
///
/// Check whether a user can manage(change timeline info, member, ...) a timeline.
///
/// The id of the timeline.
/// The id of the user to check on.
/// True if the user can manage the timeline, otherwise false.
/// Thrown when timeline does not exist.
///
/// This method does not check whether visitor is administrator.
/// Return false if user with user id does not exist.
///
Task HasManagePermission(long timelineId, long userId);
///
/// Verify whether a visitor has the permission to read a timeline.
///
/// The id of the timeline.
/// The id of the user to check on. Null means visitor without account.
/// True if can read, false if can't read.
/// Thrown when timeline does not exist.
///
/// This method does not check whether visitor is administrator.
/// Return false if user with visitor id does not exist.
///
Task HasReadPermission(long timelineId, long? visitorId);
///
/// Verify whether a user is member of a timeline.
///
/// The id of the timeline.
/// The id of user to check on.
/// True if it is a member, false if not.
/// Thrown when timeline does not exist.
///
/// Timeline owner is also considered as a member.
/// Return false when user with user id does not exist.
///
Task IsMemberOf(long timelineId, long userId);
///
/// Get all timelines including personal and ordinary timelines.
///
/// Filter timelines related (own or is a member) to specific user.
/// Filter timelines with given visibility. If null or empty, all visibilities are returned. Duplicate value are ignored.
/// The list of timelines.
///
/// If user with related user id does not exist, empty list will be returned.
///
Task> GetTimelines(TimelineUserRelationship? relate = null, List? visibility = null);
///
/// Create a timeline.
///
/// The name of the timeline.
/// The id of owner of the timeline.
/// The info of the new timeline.
/// Thrown when is null.
/// Thrown when timeline name is invalid.
/// Thrown when the timeline already exists.
/// Thrown when the owner user does not exist.
Task CreateTimeline(string timelineName, long ownerId);
///
/// Delete a timeline.
///
/// The id of the timeline to delete.
/// Thrown when the timeline does not exist.
Task DeleteTimeline(long id);
}
public class TimelineService : BasicTimelineService, ITimelineService
{
public TimelineService(DatabaseContext database, IBasicUserService userService, IClock clock)
: base(database, userService, clock)
{
_database = database;
_userService = userService;
_clock = clock;
}
private readonly DatabaseContext _database;
private readonly IBasicUserService _userService;
private readonly IClock _clock;
private readonly TimelineNameValidator _timelineNameValidator = new TimelineNameValidator();
private readonly ColorValidator _colorValidator = new ColorValidator();
private void ValidateTimelineName(string name, string paramName)
{
if (!_timelineNameValidator.Validate(name, out var message))
{
throw new ArgumentException(ExceptionTimelineNameBadFormat.AppendAdditionalMessage(message), paramName);
}
}
public async Task GetTimeline(long id)
{
var entity = await _database.Timelines.Where(t => t.Id == id).SingleOrDefaultAsync();
if (entity is null)
throw new TimelineNotExistException(id);
return entity;
}
public async Task ChangeProperty(long id, TimelineChangePropertyParams newProperties)
{
if (newProperties is null)
throw new ArgumentNullException(nameof(newProperties));
if (newProperties.Name is not null)
ValidateTimelineName(newProperties.Name, nameof(newProperties));
if (newProperties.Color is not null)
{
var (result, message) = _colorValidator.Validate(newProperties.Color);
if (!result)
{
throw new ArgumentException(message, nameof(newProperties));
}
}
var entity = await _database.Timelines.Where(t => t.Id == id).SingleOrDefaultAsync();
if (entity is null)
throw new TimelineNotExistException(id);
var changed = false;
var nameChanged = false;
if (newProperties.Name is not null)
{
var conflict = await _database.Timelines.AnyAsync(t => t.Name == newProperties.Name);
if (conflict)
throw new EntityAlreadyExistException(EntityNames.Timeline, null, ExceptionTimelineNameConflict);
entity.Name = newProperties.Name;
changed = true;
nameChanged = true;
}
if (newProperties.Title != null)
{
changed = true;
entity.Title = newProperties.Title;
}
if (newProperties.Description != null)
{
changed = true;
entity.Description = newProperties.Description;
}
if (newProperties.Visibility.HasValue)
{
changed = true;
entity.Visibility = newProperties.Visibility.Value;
}
if (newProperties.Color is not null)
{
changed = true;
entity.Color = newProperties.Color;
}
if (changed)
{
var currentTime = _clock.GetCurrentTime();
entity.LastModified = currentTime;
if (nameChanged)
entity.NameLastModified = currentTime;
}
await _database.SaveChangesAsync();
}
public async Task AddMember(long timelineId, long userId)
{
if (!await CheckExistence(timelineId))
throw new TimelineNotExistException(timelineId);
if (!await _userService.CheckUserExistence(userId))
throw new UserNotExistException(userId);
if (await _database.TimelineMembers.AnyAsync(m => m.TimelineId == timelineId && m.UserId == userId))
return false;
var entity = new TimelineMemberEntity { UserId = userId, TimelineId = timelineId };
_database.TimelineMembers.Add(entity);
var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync();
timelineEntity.LastModified = _clock.GetCurrentTime();
await _database.SaveChangesAsync();
return true;
}
public async Task RemoveMember(long timelineId, long userId)
{
if (!await CheckExistence(timelineId))
throw new TimelineNotExistException(timelineId);
if (!await _userService.CheckUserExistence(userId))
throw new UserNotExistException(userId);
var entity = await _database.TimelineMembers.SingleOrDefaultAsync(m => m.TimelineId == timelineId && m.UserId == userId);
if (entity is null) return false;
_database.TimelineMembers.Remove(entity);
var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync();
timelineEntity.LastModified = _clock.GetCurrentTime();
await _database.SaveChangesAsync();
return true;
}
public async Task HasManagePermission(long timelineId, long userId)
{
var entity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.OwnerId }).SingleOrDefaultAsync();
if (entity is null)
throw new TimelineNotExistException(timelineId);
return entity.OwnerId == userId;
}
public async Task HasReadPermission(long timelineId, long? visitorId)
{
var entity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.Visibility }).SingleOrDefaultAsync();
if (entity is null)
throw new TimelineNotExistException(timelineId);
if (entity.Visibility == TimelineVisibility.Public)
return true;
if (entity.Visibility == TimelineVisibility.Register && visitorId != null)
return true;
if (visitorId == null)
{
return false;
}
else
{
var memberEntity = await _database.TimelineMembers.Where(m => m.UserId == visitorId && m.TimelineId == timelineId).SingleOrDefaultAsync();
return memberEntity is not null;
}
}
public async Task IsMemberOf(long timelineId, long userId)
{
var entity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.OwnerId }).SingleOrDefaultAsync();
if (entity is null)
throw new TimelineNotExistException(timelineId);
if (userId == entity.OwnerId)
return true;
return await _database.TimelineMembers.AnyAsync(m => m.TimelineId == timelineId && m.UserId == userId);
}
public async Task> GetTimelines(TimelineUserRelationship? relate = null, List? visibility = null)
{
List entities;
IQueryable ApplyTimelineVisibilityFilter(IQueryable query)
{
if (visibility != null && visibility.Count != 0)
{
return query.Where(t => visibility.Contains(t.Visibility));
}
return query;
}
bool allVisibilities = visibility == null || visibility.Count == 0;
if (relate == null)
{
entities = await ApplyTimelineVisibilityFilter(_database.Timelines).ToListAsync();
}
else
{
entities = new List();
if ((relate.Type & TimelineUserRelationshipType.Own) != 0)
{
entities.AddRange(await ApplyTimelineVisibilityFilter(_database.Timelines.Where(t => t.OwnerId == relate.UserId)).ToListAsync());
}
if ((relate.Type & TimelineUserRelationshipType.Join) != 0)
{
entities.AddRange(await ApplyTimelineVisibilityFilter(_database.TimelineMembers.Where(m => m.UserId == relate.UserId).Include(m => m.Timeline).Select(m => m.Timeline)).ToListAsync());
}
}
return entities;
}
public async Task CreateTimeline(string name, long owner)
{
if (name == null)
throw new ArgumentNullException(nameof(name));
ValidateTimelineName(name, nameof(name));
var conflict = await _database.Timelines.AnyAsync(t => t.Name == name);
if (conflict)
throw new EntityAlreadyExistException(EntityNames.Timeline, null, ExceptionTimelineNameConflict);
var entity = CreateNewTimelineEntity(name, owner);
_database.Timelines.Add(entity);
await _database.SaveChangesAsync();
return entity;
}
public async Task DeleteTimeline(long id)
{
var entity = await _database.Timelines.Where(t => t.Id == id).SingleOrDefaultAsync();
if (entity is null)
throw new TimelineNotExistException(id);
_database.Timelines.Remove(entity);
await _database.SaveChangesAsync();
}
}
public static class TimelineServiceExtensions
{
public static async Task> GetTimelineList(this ITimelineService service, IEnumerable ids)
{
var timelines = new List();
foreach (var id in ids)
{
timelines.Add(await service.GetTimeline(id));
}
return timelines;
}
}
}