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; } } }