using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using SixLabors.ImageSharp;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Timeline.Entities;
using Timeline.Helpers;
using Timeline.Models;
using Timeline.Models.Validation;
using static Timeline.Resources.Services.TimelineService;
namespace Timeline.Services
{
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 PostData : ICacheableData
{
#pragma warning disable CA1819 // Properties should not return arrays
public byte[] Data { get; set; } = default!;
#pragma warning restore CA1819 // Properties should not return arrays
public string Type { get; set; } = default!;
public string ETag { get; set; } = default!;
public DateTime? LastModified { get; set; }
}
///
/// This define the common interface of both personal timeline and normal timeline.
///
///
/// The "name" parameter in each method has different meaning.
/// => name of the ordinary timeline
/// => username of the owner of the personal timeline
/// => username if begin with '@' otherwise timeline name
///
/// is thrown when name is illegal.
/// For ordinary timeline, it means the name is not a valid timeline name.
/// For personal timeline, it means the name is not a valid username.
///
/// is thrown when timeline does not exist.
/// For ordinary timeline, it means the timeline of the name does not exist.
/// For personal timeline, it means the user with the username does not exist and the inner exception should be a .
///
public interface IBaseTimelineService
{
///
/// Get the timeline info.
///
/// See remarks of .
/// The timeline info.
/// Thrown when is null.
/// See remarks of .
/// See remarks of .
Task GetTimeline(string name);
///
/// Set the properties of a timeline.
///
/// See remarks of .
/// The new properties. Null member means not to change.
/// Thrown when or is null.
/// See remarks of .
/// See remarks of .
Task ChangeProperty(string name, TimelineChangePropertyRequest newProperties);
///
/// Get all the posts in the timeline.
///
/// See remarks of .
/// A list of all posts.
/// Thrown when is null.
/// See remarks of .
/// See remarks of .
Task> GetPosts(string name);
///
/// Get the etag of data of a post.
///
/// See remarks of .
/// The id of the post.
/// The etag of the data.
/// Thrown when is null.
/// See remarks of .
/// See remarks of .
/// Thrown when post of does not exist or has been deleted.
/// Thrown when post has no data. See remarks.
///
Task GetPostDataETag(string name, long postId);
///
/// Get the data of a post.
///
/// See remarks of .
/// The id of the post.
/// The data and its type.
/// Thrown when is null.
/// See remarks of .
/// See remarks of .
/// Thrown when post of does not exist or has been deleted.
/// Thrown when post has no data. See remarks.
///
/// Use this method to retrieve the image of image post.
///
///
Task GetPostData(string name, long postId);
///
/// Create a new text post in timeline.
///
/// See remarks of .
/// The author's user id.
/// The content text.
/// The time of the post. If null, then use current time.
/// The info of the created post.
/// Thrown when or is null.
/// See remarks of .
/// See remarks of .
/// Thrown if user with does not exist.
Task CreateTextPost(string name, long authorId, string text, DateTime? time);
///
/// Create a new image post in timeline.
///
/// See remarks of .
/// The author's user id.
/// The image data.
/// The time of the post. If null, then use current time.
/// The info of the created post.
/// Thrown when or is null.
/// See remarks of .
/// See remarks of .
/// Thrown if user with does not exist.
/// Thrown if data is not a image. Validated by .
Task CreateImagePost(string name, long authorId, byte[] data, DateTime? time);
///
/// Delete a post.
///
/// See remarks of .
/// The id of the post to delete.
/// Thrown when is null.
/// See remarks of .
/// See remarks of .
///
/// 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);
///
/// Remove members to a timeline.
///
/// 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.
/// See remarks of .
/// See remarks of .
/// 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 name, IList? add, IList? remove);
///
/// Check whether a user can manage(change timeline info, member, ...) a timeline.
///
/// See remarks of .
/// The user id.
/// True if the user can manage the timeline, otherwise false.
/// Thrown when is null.
/// See remarks of .
/// See remarks of .
///
/// This method does not check whether visitor is administrator.
/// Return false if user with user id does not exist.
///
Task HasManagePermission(string name, long userId);
///
/// Verify whether a visitor has the permission to read a timeline.
///
/// See remarks of .
/// 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.
/// See remarks of .
/// See remarks of .
///
/// This method does not check whether visitor is administrator.
/// Return false if user with visitor id does not exist.
///
Task HasReadPermission(string name, long? visitorId);
///
/// Verify whether a user has the permission to modify a post.
///
/// See remarks of .
/// The id of the user to check on.
/// True if you want it to throw . Default false.
/// True if can modify, false if can't modify.
/// Thrown when is null.
/// See remarks of .
/// See remarks of .
/// Thrown when the post with given id does not exist or is deleted already and is true.
///
/// 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.
/// Return false when user with modifier id does not exist.
///
Task HasPostModifyPermission(string name, long id, long modifierId, bool throwOnPostNotExist = false);
///
/// Verify whether a user is member of a timeline.
///
/// See remarks of .
/// The id of user to check on.
/// True if it is a member, false if not.
/// Thrown when is null.
/// See remarks of .
/// See remarks of .
///
/// Timeline owner is also considered as a member.
/// Return false when user with user id does not exist.
///
Task IsMemberOf(string name, long userId);
}
///
/// Service for normal timeline.
///
public interface ITimelineService : IBaseTimelineService
{
///
/// Get all timelines including personal 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 name, long owner);
///
/// Delete a timeline.
///
/// The name of the timeline.
/// Thrown when is null.
/// Thrown when timeline name is invalid.
/// Thrown when the timeline does not exist.
Task DeleteTimeline(string name);
}
public interface IOrdinaryTimelineService : IBaseTimelineService
{
}
public interface IPersonalTimelineService : IBaseTimelineService
{
}
public abstract class BaseTimelineService : IBaseTimelineService
{
protected BaseTimelineService(ILoggerFactory loggerFactory, DatabaseContext database, IImageValidator imageValidator, IDataManager dataManager, IUserService userService, IClock clock)
{
_logger = loggerFactory.CreateLogger();
Clock = clock;
Database = database;
ImageValidator = imageValidator;
DataManager = dataManager;
UserService = userService;
}
private ILogger _logger;
protected IClock Clock { get; }
protected UsernameValidator UsernameValidator { get; } = new UsernameValidator();
protected DatabaseContext Database { get; }
protected IImageValidator ImageValidator { get; }
protected IDataManager DataManager { get; }
protected IUserService UserService { 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 is illegal. It is not a valid timeline name (for normal timeline service) or a valid username (for personal timeline service).
///
/// 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);
protected abstract string GenerateName(string name);
public async Task GetTimeline(string name)
{
if (name == null)
throw new ArgumentNullException(nameof(name));
var timelineId = await FindTimelineId(name);
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 owner = await UserService.GetUserById(timelineEntity.OwnerId);
var members = new List();
foreach (var memberEntity in timelineMemberEntities)
{
members.Add(await UserService.GetUserById(memberEntity.UserId));
}
return new Models.Timeline
{
Name = GenerateName(name),
Description = timelineEntity.Description ?? "",
Owner = owner,
Visibility = timelineEntity.Visibility,
Members = members
};
}
public async Task> GetPosts(string name)
{
if (name == null)
throw new ArgumentNullException(nameof(name));
var timelineId = await FindTimelineId(name);
var postEntities = await Database.TimelinePosts.OrderBy(p => p.Time).Where(p => p.TimelineId == timelineId && p.Content != null).ToListAsync();
var posts = new List();
foreach (var entity in postEntities)
{
if (entity.Content != null) // otherwise it is deleted
{
var author = await UserService.GetUserById(entity.AuthorId);
var type = entity.ContentType;
ITimelinePostContent content = type switch
{
TimelinePostContentTypes.Text => new TextTimelinePostContent(entity.Content),
TimelinePostContentTypes.Image => new ImageTimelinePostContent(entity.Content),
_ => throw new DatabaseCorruptedException(string.Format(CultureInfo.InvariantCulture, ExceptionDatabaseUnknownContentType, type))
};
posts.Add(new TimelinePost(
id: entity.LocalId,
content: content,
time: entity.Time,
author: author,
lastUpdated: entity.LastUpdated,
timelineName: GenerateName(name)
));
}
}
return posts;
}
public async Task GetPostDataETag(string name, long postId)
{
if (name == null)
throw new ArgumentNullException(nameof(name));
var timelineId = await FindTimelineId(name);
var postEntity = await Database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == postId).SingleOrDefaultAsync();
if (postEntity == null)
throw new TimelinePostNotExistException(name, postId);
if (postEntity.Content == null)
throw new TimelinePostNotExistException(name, postId, true);
if (postEntity.ContentType != TimelinePostContentTypes.Image)
throw new BadPostTypeException(postEntity.ContentType, TimelinePostContentTypes.Image, ExceptionGetDataNonImagePost);
var tag = postEntity.Content;
return tag;
}
public async Task GetPostData(string name, long postId)
{
if (name == null)
throw new ArgumentNullException(nameof(name));
var timelineId = await FindTimelineId(name);
var postEntity = await Database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == postId).SingleOrDefaultAsync();
if (postEntity == null)
throw new TimelinePostNotExistException(name, postId);
if (postEntity.Content == null)
throw new TimelinePostNotExistException(name, postId, true);
if (postEntity.ContentType != TimelinePostContentTypes.Image)
throw new BadPostTypeException(postEntity.ContentType, TimelinePostContentTypes.Image, ExceptionGetDataNonImagePost);
var tag = postEntity.Content;
byte[] data;
try
{
data = await DataManager.GetEntry(tag);
}
catch (InvalidOperationException e)
{
throw new DatabaseCorruptedException(ExceptionGetDataDataEntryNotExist, e);
}
if (postEntity.ExtraContent == null)
{
_logger.LogWarning(LogGetDataNoFormat);
var format = Image.DetectFormat(data);
postEntity.ExtraContent = format.DefaultMimeType;
await Database.SaveChangesAsync();
}
return new PostData
{
Data = data,
Type = postEntity.ExtraContent,
ETag = tag,
LastModified = postEntity.LastUpdated
};
}
public async Task CreateTextPost(string name, long authorId, string text, DateTime? time)
{
if (name == null)
throw new ArgumentNullException(nameof(name));
if (text == null)
throw new ArgumentNullException(nameof(text));
var timelineId = await FindTimelineId(name);
var timelineEntity = await Database.Timelines.Where(t => t.Id == timelineId).SingleAsync();
var author = await UserService.GetUserById(authorId);
var currentTime = Clock.GetCurrentTime();
var finalTime = time ?? currentTime;
timelineEntity.CurrentPostLocalId += 1;
var postEntity = new TimelinePostEntity
{
LocalId = timelineEntity.CurrentPostLocalId,
ContentType = TimelinePostContentTypes.Text,
Content = text,
AuthorId = authorId,
TimelineId = timelineId,
Time = finalTime,
LastUpdated = currentTime
};
Database.TimelinePosts.Add(postEntity);
await Database.SaveChangesAsync();
return new TimelinePost(
id: postEntity.LocalId,
content: new TextTimelinePostContent(text),
time: finalTime,
author: author,
lastUpdated: currentTime,
timelineName: GenerateName(name)
);
}
public async Task CreateImagePost(string name, long authorId, byte[] data, DateTime? time)
{
if (name == null)
throw new ArgumentNullException(nameof(name));
if (data == null)
throw new ArgumentNullException(nameof(data));
var timelineId = await FindTimelineId(name);
var timelineEntity = await Database.Timelines.Where(t => t.Id == timelineId).SingleAsync();
var author = await UserService.GetUserById(authorId);
var imageFormat = await ImageValidator.Validate(data);
var imageFormatText = imageFormat.DefaultMimeType;
var tag = await DataManager.RetainEntry(data);
var currentTime = Clock.GetCurrentTime();
var finalTime = time ?? currentTime;
timelineEntity.CurrentPostLocalId += 1;
var postEntity = new TimelinePostEntity
{
LocalId = timelineEntity.CurrentPostLocalId,
ContentType = TimelinePostContentTypes.Image,
Content = tag,
ExtraContent = imageFormatText,
AuthorId = authorId,
TimelineId = timelineId,
Time = finalTime,
LastUpdated = currentTime
};
Database.TimelinePosts.Add(postEntity);
await Database.SaveChangesAsync();
return new TimelinePost(
id: postEntity.LocalId,
content: new ImageTimelinePostContent(tag),
time: finalTime,
author: author,
lastUpdated: currentTime,
timelineName: GenerateName(name)
);
}
public async Task DeletePost(string name, long id)
{
if (name == null)
throw new ArgumentNullException(nameof(name));
var timelineId = await FindTimelineId(name);
var post = await Database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == id).SingleOrDefaultAsync();
if (post == null || post.Content == null)
throw new TimelinePostNotExistException(name, id);
string? dataTag = null;
if (post.ContentType == TimelinePostContentTypes.Image)
{
dataTag = post.Content;
}
post.Content = null;
post.LastUpdated = Clock.GetCurrentTime();
await Database.SaveChangesAsync();
if (dataTag != null)
{
await DataManager.FreeEntry(dataTag);
}
}
public async Task ChangeProperty(string name, TimelineChangePropertyRequest 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));
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);
}
}
var timelineId = await FindTimelineId(name);
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);
}
await Database.SaveChangesAsync();
}
public async Task HasManagePermission(string name, long userId)
{
if (name == null)
throw new ArgumentNullException(nameof(name));
var timelineId = await FindTimelineId(name);
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 name, long? visitorId)
{
if (name == null)
throw new ArgumentNullException(nameof(name));
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 && 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 HasPostModifyPermission(string name, long id, long modifierId, bool throwOnPostNotExist = false)
{
if (name == null)
throw new ArgumentNullException(nameof(name));
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.TimelinePosts.Where(p => p.Id == id).Select(p => new { p.AuthorId }).SingleOrDefaultAsync();
if (postEntity == null && throwOnPostNotExist)
{
throw new TimelinePostNotExistException(name, id, false);
}
return timelineEntity.OwnerId == modifierId || postEntity == null || postEntity.AuthorId == modifierId;
}
public async Task IsMemberOf(string name, long userId)
{
if (name == null)
throw new ArgumentNullException(nameof(name));
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;
return await Database.TimelineMembers.AnyAsync(m => m.TimelineId == timelineId && m.UserId == userId);
}
}
public class OrdinaryTimelineService : BaseTimelineService, IOrdinaryTimelineService
{
private readonly TimelineNameValidator _timelineNameValidator = new TimelineNameValidator();
private void ValidateTimelineName(string name, string paramName)
{
if (!_timelineNameValidator.Validate(name, out var message))
{
throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, ExceptionTimelineNameBadFormat, message), paramName);
}
}
public OrdinaryTimelineService(ILoggerFactory loggerFactory, DatabaseContext database, IImageValidator imageValidator, IDataManager dataManager, IUserService userService, IClock clock)
: base(loggerFactory, database, imageValidator, dataManager, userService, clock)
{
}
protected override async Task FindTimelineId(string name)
{
if (name == null)
throw new ArgumentNullException(nameof(name));
ValidateTimelineName(name, nameof(name));
var timelineEntity = await Database.Timelines.Where(t => t.Name == name).Select(t => new { t.Id }).SingleOrDefaultAsync();
if (timelineEntity == null)
{
throw new TimelineNotExistException(name);
}
else
{
return timelineEntity.Id;
}
}
protected override string GenerateName(string name)
{
return name;
}
}
public class PersonalTimelineService : BaseTimelineService, IPersonalTimelineService
{
public PersonalTimelineService(ILoggerFactory loggerFactory, DatabaseContext database, IImageValidator imageValidator, IDataManager dataManager, IUserService userService, IClock clock)
: base(loggerFactory, database, imageValidator, dataManager, userService, clock)
{
}
protected override async Task FindTimelineId(string name)
{
if (name == null)
throw new ArgumentNullException(nameof(name));
long userId;
try
{
userId = await UserService.GetUserIdByUsername(name);
}
catch (ArgumentException e)
{
throw new ArgumentException(ExceptionFindTimelineUsernameBadFormat, nameof(name), e);
}
catch (UserNotExistException e)
{
throw new TimelineNotExistException(name, e);
}
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
{
CurrentPostLocalId = 0,
Name = null,
OwnerId = userId,
Visibility = TimelineVisibility.Register,
CreateTime = Clock.GetCurrentTime()
};
Database.Timelines.Add(newTimelineEntity);
await Database.SaveChangesAsync();
return newTimelineEntity.Id;
}
}
protected override string GenerateName(string name)
{
return "@" + name;
}
}
public class TimelineService : ITimelineService
{
private readonly TimelineNameValidator _timelineNameValidator = new TimelineNameValidator();
private readonly DatabaseContext _database;
private readonly IUserService _userService;
private readonly IClock _clock;
private readonly IOrdinaryTimelineService _ordinaryTimelineService;
private readonly IPersonalTimelineService _personalTimelineService;
public TimelineService(DatabaseContext database, IUserService userService, IClock clock, IOrdinaryTimelineService ordinaryTimelineService, IPersonalTimelineService personalTimelineService)
{
_database = database;
_userService = userService;
_clock = clock;
_ordinaryTimelineService = ordinaryTimelineService;
_personalTimelineService = personalTimelineService;
}
private void ValidateTimelineName(string name, string paramName)
{
if (!_timelineNameValidator.Validate(name, out var message))
{
throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, ExceptionTimelineNameBadFormat, message), paramName);
}
}
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)
{
var owner = await _userService.GetUserById(entity.OwnerId);
var timeline = new Models.Timeline
{
Name = entity.Name ?? ("@" + owner.Username),
Description = entity.Description ?? "",
Owner = owner,
Visibility = entity.Visibility,
Members = new List()
};
foreach (var m in entity.Members)
{
timeline.Members.Add(await _userService.GetUserById(m.UserId));
}
result.Add(timeline);
}
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.GetUserById(owner);
var conflict = await _database.Timelines.AnyAsync(t => t.Name == name);
if (conflict)
throw new ConflictException(ExceptionTimelineNameConflict);
var newEntity = new TimelineEntity
{
CurrentPostLocalId = 0,
Name = name,
OwnerId = owner,
Visibility = TimelineVisibility.Register,
CreateTime = _clock.GetCurrentTime()
};
_database.Timelines.Add(newEntity);
await _database.SaveChangesAsync();
return new Models.Timeline
{
Name = name,
Description = "",
Owner = user,
Visibility = newEntity.Visibility,
Members = new List()
};
}
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();
}
private IBaseTimelineService BranchName(string name, out string realName)
{
if (name == null)
throw new ArgumentNullException(nameof(name));
if (name.StartsWith('@'))
{
realName = name.Substring(1);
return _personalTimelineService;
}
else
{
realName = name;
return _ordinaryTimelineService;
}
}
public Task GetTimeline(string name)
{
var s = BranchName(name, out var realName);
return s.GetTimeline(realName);
}
public Task ChangeProperty(string name, TimelineChangePropertyRequest newProperties)
{
var s = BranchName(name, out var realName);
return s.ChangeProperty(realName, newProperties);
}
public Task> GetPosts(string name)
{
var s = BranchName(name, out var realName);
return s.GetPosts(realName);
}
public Task GetPostDataETag(string name, long postId)
{
var s = BranchName(name, out var realName);
return s.GetPostDataETag(realName, postId);
}
public Task GetPostData(string name, long postId)
{
var s = BranchName(name, out var realName);
return s.GetPostData(realName, postId);
}
public Task CreateTextPost(string name, long authorId, string text, DateTime? time)
{
var s = BranchName(name, out var realName);
return s.CreateTextPost(realName, authorId, text, time);
}
public Task CreateImagePost(string name, long authorId, byte[] data, DateTime? time)
{
var s = BranchName(name, out var realName);
return s.CreateImagePost(realName, authorId, data, time);
}
public Task DeletePost(string name, long id)
{
var s = BranchName(name, out var realName);
return s.DeletePost(realName, id);
}
public Task ChangeMember(string name, IList? add, IList? remove)
{
var s = BranchName(name, out var realName);
return s.ChangeMember(realName, add, remove);
}
public Task HasManagePermission(string name, long userId)
{
var s = BranchName(name, out var realName);
return s.HasManagePermission(realName, userId);
}
public Task HasReadPermission(string name, long? visitorId)
{
var s = BranchName(name, out var realName);
return s.HasReadPermission(realName, visitorId);
}
public Task HasPostModifyPermission(string name, long id, long modifierId, bool throwOnPostNotExist = false)
{
var s = BranchName(name, out var realName);
return s.HasPostModifyPermission(realName, id, modifierId, throwOnPostNotExist);
}
public Task IsMemberOf(string name, long userId)
{
var s = BranchName(name, out var realName);
return s.IsMemberOf(realName, userId);
}
}
}