using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Timeline.Entities;
using Timeline.Models;
using Timeline.Models.Http;
using Timeline.Models.Validation;
namespace Timeline.Services
{
///
/// This define the common interface of both personal timeline
/// and normal timeline.
///
///
/// The "name" parameter in method means name of timeline in
/// while username of the owner
/// of the personal timeline in .
///
public interface IBaseTimelineService
{
///
/// Get all the posts in the timeline.
///
/// Username or the timeline name. See remarks of .
/// A list of all posts.
/// Thrown when is null.
///
/// Thrown when timeline name is of bad format.
/// For normal timeline, it means name is an empty string.
/// For personal timeline, it means the username is of bad format,
/// the inner exception should be a .
///
///
/// Thrown when timeline does not exist.
/// For normal timeline, it means the name does not exist.
/// For personal timeline, it means the user of that username does not exist
/// and the inner exception should be a .
///
Task> GetPosts(string name);
///
/// Create a new post in timeline.
///
/// Username or the timeline name. See remarks of .
/// The author's username.
/// The content.
/// The time of the post. If null, then use current time.
/// The info of the created post.
/// Thrown when or or is null.
///
/// Thrown when timeline name is of bad format.
/// For normal timeline, it means name is an empty string.
/// For personal timeline, it means the username is of bad format,
/// the inner exception should be a .
///
///
/// Thrown when timeline does not exist.
/// For normal timeline, it means the name does not exist.
/// For personal timeline, it means the user of that username does not exist
/// and the inner exception should be a .
///
/// Thrown if is of bad format.
/// Thrown if does not exist.
Task CreatePost(string name, string author, string content, DateTime? time);
///
/// Delete a post
///
/// Username or the timeline name. See remarks of .
/// The id of the post to delete.
/// Thrown when or is null.
///
/// Thrown when timeline name is of bad format.
/// For normal timeline, it means name is an empty string.
/// For personal timeline, it means the username is of bad format,
/// the inner exception should be a .
///
///
/// Thrown when timeline does not exist.
/// For normal timeline, it means the name does not exist.
/// For personal timeline, it means the user of that username does not exist
/// and the inner exception should be a .
///
///
/// Thrown when the post with given id does not exist or is deleted already.
///
///
/// First use
/// to check the permission.
///
Task DeletePost(string name, long id);
///
/// Set the properties of a timeline.
///
/// Username or the timeline name. See remarks of .
/// The new properties. Null member means not to change.
/// Thrown when or is null.
///
/// Thrown when timeline name is of bad format.
/// For normal timeline, it means name is an empty string.
/// For personal timeline, it means the username is of bad format,
/// the inner exception should be a .
///
///
/// Thrown when timeline does not exist.
/// For normal timeline, it means the name does not exist.
/// For personal timeline, it means the user of that username does not exist
/// and the inner exception should be a .
///
Task ChangeProperty(string name, TimelinePropertyChangeRequest newProperties);
///
/// Remove members to a timeline.
///
/// Username or the timeline name. See remarks of .
/// 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.
///
/// Thrown when timeline name is of bad format.
/// For normal timeline, it means name is an empty string.
/// For personal timeline, it means the username is of bad format,
/// the inner exception should be a .
///
///
/// Thrown when timeline does not exist.
/// For normal timeline, it means the name does not exist.
/// For personal timeline, it means the user of that username does not exist
/// and the inner exception should be a .
///
///
/// Thrown when an exception occurs on the user list.
/// The inner exception is
/// when one of the username is invalid.
/// The inner exception is
/// 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 name, IList? add, IList? remove);
///
/// Verify whether a visitor has the permission to read a timeline.
///
/// Username or the timeline name. See remarks of .
/// The user to check on. Null means visitor without account.
/// True if can read, false if can't read.
/// Thrown when is null.
///
/// Thrown when timeline name is of bad format.
/// For normal timeline, it means name is an empty string.
/// For personal timeline, it means the username is of bad format,
/// the inner exception should be a .
///
///
/// Thrown when timeline does not exist.
/// For normal timeline, it means the name does not exist.
/// For personal timeline, it means the user of that username does not exist
/// and the inner exception should be a .
///
///
/// Thrown when is of bad format.
///
///
/// Thrown when does not exist.
///
Task HasReadPermission(string name, string? username);
///
/// Verify whether a user has the permission to modify a post.
///
/// Username or the timeline name. See remarks of .
/// The user to check on.
/// True if can modify, false if can't modify.
/// Thrown when or is null.
///
/// Thrown when timeline name is of bad format.
/// For normal timeline, it means name is an empty string.
/// For personal timeline, it means the username is of bad format,
/// the inner exception should be a .
///
///
/// Thrown when timeline does not exist.
/// For normal timeline, it means the name does not exist.
/// For personal timeline, it means the user of that username does not exist
/// and the inner exception should be a .
///
///
/// Thrown when the post with given id does not exist or is deleted already.
///
///
/// Thrown when is of bad format.
///
///
/// Thrown when does not exist.
///
///
/// This method does not check whether the user is administrator.
/// It only checks whether he is the author of the post or the owner of the timeline.
///
Task HasPostModifyPermission(string name, long id, string username);
///
/// Verify whether a user is member of a timeline.
///
/// Username or the timeline name. See remarks of .
/// The user to check on.
/// True if it is a member, false if not.
/// Thrown when or is null.
///
/// Thrown when timeline name is of bad format.
/// For normal timeline, it means name is an empty string.
/// For personal timeline, it means the username is of bad format,
/// the inner exception should be a .
///
///
/// Thrown when timeline does not exist.
/// For normal timeline, it means the name does not exist.
/// For personal timeline, it means the user of that username does not exist
/// and the inner exception should be a .
///
///
/// Thrown when is not a valid username.
///
///
/// Thrown when user does not exist.
///
///
/// Timeline owner is also considered as a member.
///
Task IsMemberOf(string name, string username);
}
///
/// Service for normal timeline.
///
public interface ITimelineService : IBaseTimelineService
{
///
/// Get the timeline info.
///
/// The name of the timeline.
/// The timeline info.
/// Thrown when is null.
///
/// Thrown when timeline name is invalid. Currently it means it is an empty string.
///
///
/// Thrown when timeline with the name does not exist.
///
Task GetTimeline(string name);
///
/// Create a timeline.
///
/// The name of the timeline.
/// The owner of the timeline.
/// Thrown when or is null.
///
/// Thrown when timeline name is invalid. Currently it means it is an empty string.
///
///
/// Thrown when the timeline already exists.
///
///
/// Thrown when the username of the owner is not valid.
///
///
/// Thrown when the owner user does not exist.
Task CreateTimeline(string name, string owner);
}
public interface IPersonalTimelineService : IBaseTimelineService
{
///
/// Get the timeline info.
///
/// The username of the owner of the personal timeline.
/// The timeline info.
///
/// Thrown when is null.
///
///
/// Thrown when is of bad format. Inner exception MUST be .
///
///
/// Thrown when the user does not exist. Inner exception MUST be .
///
Task GetTimeline(string username);
}
public abstract class BaseTimelineService : IBaseTimelineService
{
protected BaseTimelineService(ILoggerFactory loggerFactory, DatabaseContext database, IClock clock)
{
Clock = clock;
Database = database;
}
protected IClock Clock { get; }
protected UsernameValidator UsernameValidator { get; } = new UsernameValidator();
protected DatabaseContext Database { get; }
///
/// Find the timeline id by the name.
/// For details, see remarks.
///
/// The username or the timeline name. See remarks.
/// The id of the timeline entity.
/// Thrown when is null.
///
/// Thrown when timeline name is of bad format.
/// For normal timeline, it means name is an empty string.
/// For personal timeline, it means the username is of bad format,
/// the inner exception should be a .
///
///
/// Thrown when timeline does not exist.
/// For normal timeline, it means the name does not exist.
/// For personal timeline, it means the user of that username does not exist
/// and the inner exception should be a .
///
///
/// This is the common but different part for both types of timeline service.
/// For class that implements , this method should
/// find the timeline entity id by the given as the username of the owner.
/// For class that implements , this method should
/// find the timeline entity id by the given as the timeline name.
/// This method should be called by many other method that follows the contract.
///
protected abstract Task FindTimelineId(string name);
public async Task> GetPosts(string name)
{
if (name == null)
throw new ArgumentNullException(nameof(name));
var timelineId = await FindTimelineId(name);
var postEntities = await Database.TimelinePosts.Where(p => p.TimelineId == timelineId).ToListAsync();
var posts = new List(await Task.WhenAll(postEntities.Select(async p => new TimelinePostInfo
{
Id = p.Id,
Content = p.Content,
Author = (await Database.Users.Where(u => u.Id == p.AuthorId).Select(u => new { u.Name }).SingleAsync()).Name,
Time = p.Time
})));
return posts;
}
public async Task CreatePost(string name, string author, string content, DateTime? time)
{
if (name == null)
throw new ArgumentNullException(nameof(name));
if (author == null)
throw new ArgumentNullException(nameof(author));
if (content == null)
throw new ArgumentNullException(nameof(content));
{
var (result, message) = UsernameValidator.Validate(author);
if (!result)
{
throw new UsernameBadFormatException(author, message);
}
}
var timelineId = await FindTimelineId(name);
var authorEntity = Database.Users.Where(u => u.Name == author).Select(u => new { u.Id }).SingleOrDefault();
if (authorEntity == null)
{
throw new UserNotExistException(author);
}
var authorId = authorEntity.Id;
var currentTime = Clock.GetCurrentTime();
var postEntity = new TimelinePostEntity
{
Content = content,
AuthorId = authorId,
TimelineId = timelineId,
Time = time ?? currentTime,
LastUpdated = currentTime
};
Database.TimelinePosts.Add(postEntity);
await Database.SaveChangesAsync();
return new TimelinePostCreateResponse
{
Id = postEntity.Id,
Time = postEntity.Time
};
}
public async Task DeletePost(string name, long id)
{
if (name == null)
throw new ArgumentNullException(nameof(name));
var timelineId = FindTimelineId(name);
var post = await Database.TimelinePosts.Where(p => p.Id == id).SingleOrDefaultAsync();
if (post == null)
throw new TimelinePostNotExistException(id);
Database.TimelinePosts.Remove(post);
await Database.SaveChangesAsync();
}
public async Task ChangeProperty(string name, TimelinePropertyChangeRequest newProperties)
{
if (name == null)
throw new ArgumentNullException(nameof(name));
if (newProperties == null)
throw new ArgumentNullException(nameof(newProperties));
var timelineId = await FindTimelineId(name);
var timelineEntity = await Database.Timelines.Where(t => t.Id == timelineId).SingleAsync();
if (newProperties.Description != null)
{
timelineEntity.Description = newProperties.Description;
}
if (newProperties.Visibility.HasValue)
{
timelineEntity.Visibility = newProperties.Visibility.Value;
}
await Database.SaveChangesAsync();
}
public async Task ChangeMember(string name, IList? add, IList? remove)
{
if (name == null)
throw new ArgumentNullException(nameof(name));
// remove duplication and check the format of each username.
// Return a username->index map.
Dictionary? RemoveDuplicateAndCheckFormat(IList? list, TimelineMemberOperationUserException.MemberOperation operation)
{
if (list != null)
{
Dictionary result = new Dictionary();
var count = list.Count;
for (var index = 0; index < count; index++)
{
var username = list[index];
if (result.ContainsKey(username))
{
continue;
}
var (validationResult, message) = UsernameValidator.Validate(username);
if (!validationResult)
throw new TimelineMemberOperationUserException(
index, operation, username,
new UsernameBadFormatException(username, message));
result.Add(username, index);
}
return result;
}
else
{
return null;
}
}
var simplifiedAdd = RemoveDuplicateAndCheckFormat(add, TimelineMemberOperationUserException.MemberOperation.Add);
var simplifiedRemove = RemoveDuplicateAndCheckFormat(remove, TimelineMemberOperationUserException.MemberOperation.Remove);
// remove those both in add and remove
if (simplifiedAdd != null && simplifiedRemove != null)
{
var usersToClean = simplifiedRemove.Keys.Where(u => simplifiedAdd.ContainsKey(u));
foreach (var u in usersToClean)
{
simplifiedAdd.Remove(u);
simplifiedRemove.Remove(u);
}
}
var timelineId = await FindTimelineId(name);
async Task?> CheckExistenceAndGetId(Dictionary? map, TimelineMemberOperationUserException.MemberOperation operation)
{
if (map == null)
return null;
List result = new List();
foreach (var (username, index) in map)
{
var user = await Database.Users.Where(u => u.Name == username).Select(u => new { u.Id }).SingleOrDefaultAsync();
if (user == null)
{
throw new TimelineMemberOperationUserException(index, operation, username,
new UserNotExistException(username));
}
result.Add(user.Id);
}
return result;
}
var userIdsAdd = await CheckExistenceAndGetId(simplifiedAdd, TimelineMemberOperationUserException.MemberOperation.Add);
var userIdsRemove = await CheckExistenceAndGetId(simplifiedRemove, TimelineMemberOperationUserException.MemberOperation.Remove);
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);
}
await Database.SaveChangesAsync();
}
public async Task HasReadPermission(string name, string? username)
{
if (name == null)
throw new ArgumentNullException(nameof(name));
long? userId = null;
if (username != null)
{
var (result, message) = UsernameValidator.Validate(username);
if (!result)
{
throw new UsernameBadFormatException(username);
}
var user = await Database.Users.Where(u => u.Name == username).Select(u => new { u.Id }).SingleOrDefaultAsync();
if (user == null)
{
throw new UserNotExistException(username);
}
userId = user.Id;
}
var timelineId = await FindTimelineId(name);
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 && username != null)
return true;
if (userId == null)
{
return false;
}
else
{
var memberEntity = await Database.TimelineMembers.Where(m => m.UserId == userId && m.TimelineId == timelineId).SingleOrDefaultAsync();
return memberEntity != null;
}
}
public async Task HasPostModifyPermission(string name, long id, string username)
{
if (name == null)
throw new ArgumentNullException(nameof(name));
if (username == null)
throw new ArgumentNullException(nameof(username));
{
var (result, message) = UsernameValidator.Validate(username);
if (!result)
{
throw new UsernameBadFormatException(username);
}
}
var user = await Database.Users.Where(u => u.Name == username).Select(u => new { u.Id }).SingleOrDefaultAsync();
if (user == null)
{
throw new UserNotExistException(username);
}
var userId = user.Id;
var timelineId = await FindTimelineId(name);
var timelineEntity = await Database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.OwnerId }).SingleAsync();
var postEntity = await Database.Timelines.Where(p => p.Id == id).Select(p => new { p.OwnerId }).SingleOrDefaultAsync();
if (postEntity == null)
throw new TimelinePostNotExistException(id);
return timelineEntity.OwnerId == userId || postEntity.OwnerId == userId;
}
public async Task IsMemberOf(string name, string username)
{
if (name == null)
throw new ArgumentNullException(nameof(name));
if (username == null)
throw new ArgumentNullException(nameof(username));
{
var (result, message) = UsernameValidator.Validate(username);
if (!result)
{
throw new UsernameBadFormatException(username);
}
}
var user = await Database.Users.Where(u => u.Name == username).Select(u => new { u.Id }).SingleOrDefaultAsync();
if (user == null)
{
throw new UserNotExistException(username);
}
var userId = user.Id;
var timelineId = await FindTimelineId(name);
var timelineEntity = await Database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.OwnerId }).SingleAsync();
if (userId == timelineEntity.OwnerId)
return true;
var timelineMemberEntity = await Database.TimelineMembers.Where(m => m.TimelineId == timelineId && m.UserId == userId).SingleOrDefaultAsync();
return timelineMemberEntity != null;
}
}
public class PersonalTimelineService : BaseTimelineService, IPersonalTimelineService
{
public PersonalTimelineService(ILoggerFactory loggerFactory, DatabaseContext database, IClock clock)
: base(loggerFactory, database, clock)
{
}
protected override async Task FindTimelineId(string name)
{
{
var (result, message) = UsernameValidator.Validate(name);
if (!result)
{
throw new TimelineNameBadFormatException(name, new UsernameBadFormatException(name, message));
}
}
var userEntity = await Database.Users.Where(u => u.Name == name).Select(u => new { u.Id }).SingleOrDefaultAsync();
if (userEntity == null)
{
throw new TimelineNotExistException(name, new UserNotExistException(name));
}
var userId = userEntity.Id;
var timelineEntity = await Database.Timelines.Where(t => t.OwnerId == userId && t.Name == null).Select(t => new { t.Id }).SingleOrDefaultAsync();
if (timelineEntity != null)
{
return timelineEntity.Id;
}
else
{
var newTimelineEntity = new TimelineEntity
{
Name = null,
Description = null,
OwnerId = userId,
Visibility = TimelineVisibility.Register,
CreateTime = Clock.GetCurrentTime(),
};
Database.Timelines.Add(newTimelineEntity);
await Database.SaveChangesAsync();
return newTimelineEntity.Id;
}
}
public async Task GetTimeline(string username)
{
if (username == null)
throw new ArgumentNullException(nameof(username));
var timelineId = await FindTimelineId(username);
var timelineEntity = await Database.Timelines.Where(t => t.Id == timelineId).SingleAsync();
var timelineMemberEntities = await Database.TimelineMembers.Where(m => m.TimelineId == timelineId).Select(m => new { m.UserId }).ToListAsync();
var memberUsernameTasks = timelineMemberEntities.Select(m => Database.Users.Where(u => u.Id == m.UserId).Select(u => u.Name).SingleAsync()).ToArray();
var memberUsernames = await Task.WhenAll(memberUsernameTasks);
return new BaseTimelineInfo
{
Description = timelineEntity.Description,
Owner = username,
Visibility = timelineEntity.Visibility,
Members = memberUsernames.ToList()
};
}
}
}