using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Globalization;
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.Substring(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; }
}
///
/// This define the interface of both personal timeline and ordinary timeline.
///
public interface ITimelineService : IBasicTimelineService
{
///
/// Get the timeline last modified time (not include name change).
///
/// The name of the timeline.
/// The timeline info.
/// Thrown when is null.
/// Throw when is of bad format.
///
/// Thrown when timeline with name does not exist.
/// If it is a personal timeline, then inner exception is .
///
Task GetTimelineLastModifiedTime(string timelineName);
///
/// Get the timeline unique id.
///
/// The name of the timeline.
/// The timeline info.
/// Thrown when is null.
/// Throw when is of bad format.
///
/// Thrown when timeline with name does not exist.
/// If it is a personal timeline, then inner exception is .
///
Task GetTimelineUniqueId(string timelineName);
///
/// Get the timeline info.
///
/// The name of the timeline.
/// The timeline info.
/// Thrown when is null.
/// Throw when is of bad format.
///
/// Thrown when timeline with name does not exist.
/// If it is a personal timeline, then inner exception is .
///
Task GetTimeline(string timelineName);
///
/// Get timeline by id.
///
/// Id of timeline.
/// The timeline.
/// Thrown when timeline with given id does not exist.
Task GetTimelineById(long id);
///
/// Set the properties of a timeline.
///
/// The name of the timeline.
/// The new properties. Null member means not to change.
/// Thrown when or is null.
/// Throw when is of bad format.
///
/// Thrown when timeline with name does not exist.
/// If it is a personal timeline, then inner exception is .
///
Task ChangeProperty(string timelineName, TimelineChangePropertyRequest newProperties);
///
/// Change member of timeline.
///
/// The name of the timeline.
/// A list of usernames of members to add. May be null.
/// A list of usernames of members to remove. May be null.
/// Thrown when is null.
/// Throw when is of bad format.
///
/// Thrown when timeline with name does not exist.
/// If it is a personal timeline, then inner exception is .
///
/// Thrown when names in or is not a valid username.
/// Thrown when one of the user to change does not exist.
///
/// Operating on a username that is of bad format or does not exist always throws.
/// Add a user that already is a member has no effects.
/// Remove a user that is not a member also has not effects.
/// Add and remove an identical user results in no effects.
/// More than one same usernames are regarded as one.
///
Task ChangeMember(string timelineName, IList? membersToAdd, IList? membersToRemove);
///
/// Check whether a user can manage(change timeline info, member, ...) a timeline.
///
/// The name of the timeline.
/// The id of the user to check on.
/// True if the user can manage the timeline, otherwise false.
/// Thrown when is null.
/// Throw when is of bad format.
///
/// Thrown when timeline with name does not exist.
/// If it is a personal timeline, then inner exception is .
///
///
/// This method does not check whether visitor is administrator.
/// Return false if user with user id does not exist.
///
Task HasManagePermission(string timelineName, long userId);
///
/// Verify whether a visitor has the permission to read a timeline.
///
/// The name 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 is null.
/// Throw when is of bad format.
///
/// Thrown when timeline with name does not exist.
/// If it is a personal timeline, then inner exception is .
///
///
/// This method does not check whether visitor is administrator.
/// Return false if user with visitor id does not exist.
///
Task HasReadPermission(string timelineName, long? visitorId);
///
/// Verify whether a user is member of a timeline.
///
/// The name of the timeline.
/// The id of user to check on.
/// True if it is a member, false if not.
/// Thrown when is null.
/// Throw when is of bad format.
///
/// Thrown when timeline with name does not exist.
/// If it is a personal timeline, then inner exception is .
///
///
/// Timeline owner is also considered as a member.
/// Return false when user with user id does not exist.
///
Task IsMemberOf(string timelineName, 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 name of the timeline to delete.
/// Thrown when is null.
/// Thrown when timeline name is invalid.
/// Thrown when the timeline does not exist.
Task DeleteTimeline(string timelineName);
///
/// Change name of a timeline.
///
/// The old timeline name.
/// The new timeline name.
/// The new timeline info.
/// Thrown when or is null.
/// Thrown when or is of invalid format.
/// Thrown when timeline does not exist.
/// Thrown when a timeline with new name already exists.
///
/// You can only change name of general timeline.
///
Task ChangeTimelineName(string oldTimelineName, string newTimelineName);
}
public class TimelineService : BasicTimelineService, ITimelineService
{
public TimelineService(DatabaseContext database, IUserService userService, IClock clock)
: base(database, userService, clock)
{
_database = database;
_userService = userService;
_clock = clock;
}
private readonly DatabaseContext _database;
private readonly IUserService _userService;
private readonly IClock _clock;
private readonly UsernameValidator _usernameValidator = new UsernameValidator();
private readonly TimelineNameValidator _timelineNameValidator = new TimelineNameValidator();
private void ValidateTimelineName(string name, string paramName)
{
if (!_timelineNameValidator.Validate(name, out var message))
{
throw new ArgumentException(ExceptionTimelineNameBadFormat.AppendAdditionalMessage(message), paramName);
}
}
/// Remember to include Members when query.
private async Task MapTimelineFromEntity(TimelineEntity entity)
{
var owner = await _userService.GetUser(entity.OwnerId);
var members = new List();
foreach (var memberEntity in entity.Members)
{
members.Add(await _userService.GetUser(memberEntity.UserId));
}
var name = entity.Name ?? ("@" + owner.Username);
return new Models.Timeline
{
UniqueID = entity.UniqueId,
Name = name,
NameLastModified = entity.NameLastModified,
Title = string.IsNullOrEmpty(entity.Title) ? name : entity.Title,
Description = entity.Description ?? "",
Owner = owner,
Visibility = entity.Visibility,
Members = members,
CreateTime = entity.CreateTime,
LastModified = entity.LastModified
};
}
public async Task GetTimelineLastModifiedTime(string timelineName)
{
if (timelineName == null)
throw new ArgumentNullException(nameof(timelineName));
var timelineId = await GetTimelineIdByName(timelineName);
var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.LastModified }).SingleAsync();
return timelineEntity.LastModified;
}
public async Task GetTimelineUniqueId(string timelineName)
{
if (timelineName == null)
throw new ArgumentNullException(nameof(timelineName));
var timelineId = await GetTimelineIdByName(timelineName);
var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.UniqueId }).SingleAsync();
return timelineEntity.UniqueId;
}
public async Task GetTimeline(string timelineName)
{
if (timelineName == null)
throw new ArgumentNullException(nameof(timelineName));
var timelineId = await GetTimelineIdByName(timelineName);
var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Include(t => t.Members).SingleAsync();
return await MapTimelineFromEntity(timelineEntity);
}
public async Task GetTimelineById(long id)
{
var timelineEntity = await _database.Timelines.Where(t => t.Id == id).Include(t => t.Members).SingleOrDefaultAsync();
if (timelineEntity is null)
throw new TimelineNotExistException(id);
return await MapTimelineFromEntity(timelineEntity);
}
public async Task ChangeProperty(string timelineName, TimelineChangePropertyRequest newProperties)
{
if (timelineName == null)
throw new ArgumentNullException(nameof(timelineName));
if (newProperties == null)
throw new ArgumentNullException(nameof(newProperties));
var timelineId = await GetTimelineIdByName(timelineName);
var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync();
var changed = false;
if (newProperties.Title != null)
{
changed = true;
timelineEntity.Title = newProperties.Title;
}
if (newProperties.Description != null)
{
changed = true;
timelineEntity.Description = newProperties.Description;
}
if (newProperties.Visibility.HasValue)
{
changed = true;
timelineEntity.Visibility = newProperties.Visibility.Value;
}
if (changed)
{
var currentTime = _clock.GetCurrentTime();
timelineEntity.LastModified = currentTime;
}
await _database.SaveChangesAsync();
}
public async Task ChangeMember(string timelineName, IList? add, IList? remove)
{
if (timelineName == null)
throw new ArgumentNullException(nameof(timelineName));
List? RemoveDuplicateAndCheckFormat(IList? list, string paramName)
{
if (list != null)
{
List result = new List();
var count = list.Count;
for (var index = 0; index < count; index++)
{
var username = list[index];
if (result.Contains(username))
{
continue;
}
var (validationResult, message) = _usernameValidator.Validate(username);
if (!validationResult)
throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, ExceptionChangeMemberUsernameBadFormat, index), nameof(paramName));
result.Add(username);
}
return result;
}
else
{
return null;
}
}
var simplifiedAdd = RemoveDuplicateAndCheckFormat(add, nameof(add));
var simplifiedRemove = RemoveDuplicateAndCheckFormat(remove, nameof(remove));
// remove those both in add and remove
if (simplifiedAdd != null && simplifiedRemove != null)
{
var usersToClean = simplifiedRemove.Where(u => simplifiedAdd.Contains(u)).ToList();
foreach (var u in usersToClean)
{
simplifiedAdd.Remove(u);
simplifiedRemove.Remove(u);
}
if (simplifiedAdd.Count == 0)
simplifiedAdd = null;
if (simplifiedRemove.Count == 0)
simplifiedRemove = null;
}
if (simplifiedAdd == null && simplifiedRemove == null)
return;
var timelineId = await GetTimelineIdByName(timelineName);
async Task?> CheckExistenceAndGetId(List? list)
{
if (list == null)
return null;
List result = new List();
foreach (var username in list)
{
result.Add(await _userService.GetUserIdByUsername(username));
}
return result;
}
var userIdsAdd = await CheckExistenceAndGetId(simplifiedAdd);
var userIdsRemove = await CheckExistenceAndGetId(simplifiedRemove);
if (userIdsAdd != null)
{
var membersToAdd = userIdsAdd.Select(id => new TimelineMemberEntity { UserId = id, TimelineId = timelineId }).ToList();
_database.TimelineMembers.AddRange(membersToAdd);
}
if (userIdsRemove != null)
{
var membersToRemove = await _database.TimelineMembers.Where(m => m.TimelineId == timelineId && userIdsRemove.Contains(m.UserId)).ToListAsync();
_database.TimelineMembers.RemoveRange(membersToRemove);
}
var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync();
timelineEntity.LastModified = _clock.GetCurrentTime();
await _database.SaveChangesAsync();
}
public async Task HasManagePermission(string timelineName, long userId)
{
if (timelineName == null)
throw new ArgumentNullException(nameof(timelineName));
var timelineId = await GetTimelineIdByName(timelineName);
var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.OwnerId }).SingleAsync();
return userId == timelineEntity.OwnerId;
}
public async Task HasReadPermission(string timelineName, long? visitorId)
{
if (timelineName == null)
throw new ArgumentNullException(nameof(timelineName));
var timelineId = await GetTimelineIdByName(timelineName);
var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.Visibility }).SingleAsync();
if (timelineEntity.Visibility == TimelineVisibility.Public)
return true;
if (timelineEntity.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 != null;
}
}
public async Task IsMemberOf(string timelineName, long userId)
{
if (timelineName == null)
throw new ArgumentNullException(nameof(timelineName));
var timelineId = await GetTimelineIdByName(timelineName);
var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.OwnerId }).SingleAsync();
if (userId == timelineEntity.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).Include(t => t.Members).ToListAsync();
}
else
{
entities = new List();
if ((relate.Type & TimelineUserRelationshipType.Own) != 0)
{
entities.AddRange(await ApplyTimelineVisibilityFilter(_database.Timelines.Where(t => t.OwnerId == relate.UserId)).Include(t => t.Members).ToListAsync());
}
if ((relate.Type & TimelineUserRelationshipType.Join) != 0)
{
entities.AddRange(await ApplyTimelineVisibilityFilter(_database.TimelineMembers.Where(m => m.UserId == relate.UserId).Include(m => m.Timeline).ThenInclude(t => t.Members).Select(m => m.Timeline)).ToListAsync());
}
}
var result = new List();
foreach (var entity in entities)
{
result.Add(await MapTimelineFromEntity(entity));
}
return result;
}
public async Task CreateTimeline(string name, long owner)
{
if (name == null)
throw new ArgumentNullException(nameof(name));
ValidateTimelineName(name, nameof(name));
var user = await _userService.GetUser(owner);
var conflict = await _database.Timelines.AnyAsync(t => t.Name == name);
if (conflict)
throw new EntityAlreadyExistException(EntityNames.Timeline, null, ExceptionTimelineNameConflict);
var newEntity = CreateNewTimelineEntity(name, user.Id);
_database.Timelines.Add(newEntity);
await _database.SaveChangesAsync();
return await MapTimelineFromEntity(newEntity);
}
public async Task DeleteTimeline(string name)
{
if (name == null)
throw new ArgumentNullException(nameof(name));
ValidateTimelineName(name, nameof(name));
var entity = await _database.Timelines.Where(t => t.Name == name).SingleOrDefaultAsync();
if (entity == null)
throw new TimelineNotExistException(name);
_database.Timelines.Remove(entity);
await _database.SaveChangesAsync();
}
public async Task ChangeTimelineName(string oldTimelineName, string newTimelineName)
{
if (oldTimelineName == null)
throw new ArgumentNullException(nameof(oldTimelineName));
if (newTimelineName == null)
throw new ArgumentNullException(nameof(newTimelineName));
ValidateTimelineName(oldTimelineName, nameof(oldTimelineName));
ValidateTimelineName(newTimelineName, nameof(newTimelineName));
var entity = await _database.Timelines.Include(t => t.Members).Where(t => t.Name == oldTimelineName).SingleOrDefaultAsync();
if (entity == null)
throw new TimelineNotExistException(oldTimelineName);
if (oldTimelineName == newTimelineName)
return await MapTimelineFromEntity(entity);
var conflict = await _database.Timelines.AnyAsync(t => t.Name == newTimelineName);
if (conflict)
throw new EntityAlreadyExistException(EntityNames.Timeline, null, ExceptionTimelineNameConflict);
var now = _clock.GetCurrentTime();
entity.Name = newTimelineName;
entity.NameLastModified = now;
entity.LastModified = now;
await _database.SaveChangesAsync();
return await MapTimelineFromEntity(entity);
}
}
}