public Result <ActivityDetails> SetTurnDisabled(int turnId, int byUserId, bool disabled) { var turn = _db.Turns.Find(turnId); if (turn != null) { if (turn.IsDisabled != disabled) { turn.IsDisabled = disabled; turn.ModifierId = byUserId; var activity = GetActivity(turn.ActivityId, false, true); if (activity == null) { return(Result.Failure <ActivityDetails>("no such activity")); } var details = ActivityDetails.Calculate(activity, byUserId, _mapper); _db.Entry(activity).State = EntityState.Modified; _db.SaveChanges(); details.Update(activity); return(Result.Success(details)); } return(Result.Success(GetActivityDetails(turn.ActivityId, byUserId))); } return(Result.Failure <ActivityDetails>("Invalid turn id")); }
public ActivityDetails GetActivityDetails(int activityId, int userId) { var activity = GetActivity(activityId, true, true); return(activity is null ? null : ActivityDetails.Calculate(activity, userId, _mapper)); }
public async Task <Result <ActivityDetails, TurnError> > TakeTurnAsync(DateTimeOffset activityModifiedDate, int activityId, int byUserId, int forUserId, DateTimeOffset when) { try { var now = DateTimeOffset.Now; _db.Turns.Add(new Turn { ActivityId = activityId, CreatorId = byUserId, UserId = forUserId, Occurred = when }); var activity = GetActivity(activityId, false, true); if (activity == null) { _logger.LogError($"Activity {activityId} is missing"); return(Result.Failure <ActivityDetails, TurnError>(TurnError.ActivityMissing)); } if (_appSettings.Value.ValidateActivityModifiedDate && activity.ModifiedDate != activityModifiedDate) { _logger.LogWarning($"Activity {activity.Id} was modified {activity.ModifiedDate} and doesn't match {activityModifiedDate}"); return(Result.Failure <ActivityDetails, TurnError>(TurnError.ActivityModified)); } var details = ActivityDetails.Calculate(activity, byUserId, _mapper); var turnTaker = await _db.Users.FindAsync(forUserId); FormattableString fs = $"{turnTaker.DisplayName} took a turn."; var myTurnBuilder = new StringBuilder().AppendFormattable(fs); var otherTurnBuilder = new StringBuilder().AppendFormattable(fs); if (details.CurrentTurnUserId.HasValue) { otherTurnBuilder.AppendFormattable($" It's {details.CurrentTurnUserDisplayName}'s turn."); myTurnBuilder.AppendFormattable($" It's your turn."); } if (details.Due.HasValue) { fs = $" Due in {(details.Due.Value - now).ToDisplayString()}."; otherTurnBuilder.AppendFormattable(fs); myTurnBuilder.AppendFormattable(fs); } var myTurnMessage = myTurnBuilder.ToString(); var otherTurnMessage = otherTurnBuilder.ToString(); var url = $"{_appSettings.Value.PushNotifications.ServerUrl}/activity/{activityId}"; var failures = new List <PushFailure>(); foreach (var participant in activity.Participants) { var pushNotified = false; foreach (var setting in participant.NotificationSettings.OrderBy(x => x.Type)) { switch (setting.Type) { case NotificationType.OverdueAnybody: case NotificationType.OverdueMine: setting.NextCheck = now; // send a close push notification in case they still have a previous notification open but not if they // already got a notification about a turn being taken because that will replace any existing notification if (setting.Push && !pushNotified) { failures.AddRange(await _pushNotificationService.SendCloseToAllDevicesAsync("turn", setting.Participant.UserId, activityId.ToString())); pushNotified = true; } break; case NotificationType.TurnTakenAnybody: if (setting.Push) { failures.AddRange(await _pushNotificationService.SendToAllDevicesAsync("turn", setting.Participant.UserId, activity.Name, otherTurnMessage, url, activityId.ToString())); pushNotified = true; } break; case NotificationType.TurnTakenMine: if (details.CurrentTurnUserId.HasValue && setting.Push && setting.Participant.UserId == details.CurrentTurnUserId) { failures.AddRange(await _pushNotificationService.SendToAllDevicesAsync("turn", setting.Participant.UserId, activity.Name, myTurnMessage, url, activityId.ToString())); pushNotified = true; } break; default: _logger.LogError($"Unhandled notification type {setting.Type}"); break; } } } // Ensure we are done sending each push message before cleaning up failures and continuing await _pushNotificationService.CleanupFailuresAsync(failures); // Always mark the activity as modified when taking a turn so our checks elsewhere that compare // the modified timestamp will still work for activities that wouldn't normally have anything update, // like when they're non-periodic or the next-turn user doesn't change. _db.Entry(activity).State = EntityState.Modified; await _db.SaveChangesAsync(); details.Update(activity); return(Result.Success <ActivityDetails, TurnError>(details)); } catch (Exception e) { _logger.LogError(e, "Failed to take a turn"); return(Result.Failure <ActivityDetails, TurnError>(TurnError.Exception)); } }
public Result <ActivityDetails, ValidityError> SaveActivity(EditableActivity activity, int ownerId) { try { if (activity is null) { return(ValidityError.ForInvalidObject <ActivityDetails>("no activity provided")); } if (string.IsNullOrWhiteSpace(activity.Name)) { return(ValidityError.ForInvalidObject <ActivityDetails>("empty name")); } TimeSpan?period = null; if (!activity.PeriodUnit.HasValue) { activity.PeriodCount = null; } else if (!activity.PeriodCount.HasValue) { activity.PeriodUnit = null; } else if (activity.PeriodCount.Value == 0) { return(ValidityError.ForInvalidObject <ActivityDetails>("invalid period count")); } else { switch (activity.PeriodUnit.Value) { case Unit.Hour: period = TimeSpan.FromHours(activity.PeriodCount.Value); break; case Unit.Day: period = TimeSpan.FromDays(activity.PeriodCount.Value); break; case Unit.Week: period = TimeSpan.FromDays(7 * activity.PeriodCount.Value); break; case Unit.Month: period = TimeSpan.FromDays(365.25 / 12 * activity.PeriodCount.Value); break; case Unit.Year: period = TimeSpan.FromDays(365.25 * activity.PeriodCount.Value); break; default: return(ValidityError.ForInvalidObject <ActivityDetails>("invalid period unit")); } } activity.Participants ??= new List <UserInfo>(); // sanitize the default notification settings to ensure there's only one per type var defaultNotificationSettings = (activity.DefaultNotificationSettings ?? new List <NotificationInfo>()) .Where(x => x.AnyActive && x.Type.IsAllowed(activity.TakeTurns)) .GroupBy(x => x.Type) .Select(g => g.First()) .ToDictionary(x => x.Type); if (activity.Id < 0) { return(ValidityError.ForInvalidObject <ActivityDetails>("invalid ID")); } var userIds = activity.Participants.Select(p => p.Id).Append(ownerId).ToHashSet(); var add = false; Activity activityToUpdate; if (activity.Id == 0) { add = true; activityToUpdate = new Activity { Participants = _db.Users.Where(user => userIds.Contains(user.Id)).ToList().Select(CreateNewParticipant).ToList(), DefaultNotificationSettings = _mapper.Map <List <DefaultNotificationSetting> >(defaultNotificationSettings.Values), Owner = _db.Users.Find(ownerId), Turns = new List <Turn>(0) }; foreach (var participant in activityToUpdate.Participants) { participant.NotificationSettings.AddRange(_mapper.Map <List <NotificationSetting> >(defaultNotificationSettings.Values)); } } else { activityToUpdate = GetActivity(activity.Id, false, true, true); if (activityToUpdate is null) { return(ValidityError.ForInvalidObject <ActivityDetails>("invalid ID")); } if (activityToUpdate.IsDisabled) { return(ValidityError.ForInvalidObject <ActivityDetails>("activity is disabled")); } // remove any participants that should no longer be there activityToUpdate.Participants.RemoveAll(x => !userIds.Contains(x.UserId)); // remove any existing participants from the list so we're left with only new participants userIds.ExceptWith(activityToUpdate.Participants.Select(x => x.UserId)); // add new participants activityToUpdate.Participants.AddRange(_db.Users.Where(user => userIds.Contains(user.Id)).ToList().Select(CreateNewParticipant)); // remove any default notifications that should no longer be there activityToUpdate.DefaultNotificationSettings.RemoveAll(x => { var remove = !defaultNotificationSettings.ContainsKey(x.Type); if (remove) { foreach (var participant in activityToUpdate.Participants) { participant.NotificationSettings.RemoveAll(y => y.Type == x.Type && y.Origin == NotificationOrigin.Default); } } return(remove); }); // remove any participant notifications that are no longer valid because the activity no longer has turns needed if (activityToUpdate.TakeTurns && !activity.TakeTurns) { foreach (var participant in activityToUpdate.Participants) { participant.NotificationSettings.RemoveAll(x => !x.Type.IsAllowed(activity.TakeTurns)); } } // update any existing default notification and remove it from the new list foreach (var noteToUpdate in activityToUpdate.DefaultNotificationSettings) { if (defaultNotificationSettings.Remove(noteToUpdate.Type, out var updatedNote)) { _mapper.Map(updatedNote, noteToUpdate); // update any existing participant notifications that originated from a default notification foreach (var participantNote in activityToUpdate.Participants.SelectMany(x => x.NotificationSettings).Where(x => x.Type == updatedNote.Type && x.Origin == NotificationOrigin.Default)) { //make sure we don't clear the participant ID updatedNote.ParticipantId = participantNote.ParticipantId; _mapper.Map(updatedNote, participantNote); } } } // add any remaining notifications foreach (var note in defaultNotificationSettings.Values) { activityToUpdate.DefaultNotificationSettings.Add(_mapper.Map <DefaultNotificationSetting>(note)); foreach (var participant in activityToUpdate.Participants.Where(participant => participant.NotificationSettings.All(x => x.Type != note.Type))) { participant.NotificationSettings.Add(_mapper.Map <NotificationSetting>(note)); } } } activityToUpdate.OwnerId = ownerId; activityToUpdate.Name = activity.Name; activityToUpdate.Description = string.IsNullOrWhiteSpace(activity.Description) ? null : activity.Description.Trim(); activityToUpdate.PeriodCount = activity.PeriodCount; activityToUpdate.PeriodUnit = activity.PeriodUnit; activityToUpdate.Period = period; activityToUpdate.TakeTurns = activity.TakeTurns; if (add) { _db.Activities.Add(activityToUpdate); } else { _db.Activities.Update(activityToUpdate); } var details = ActivityDetails.Calculate(activityToUpdate, ownerId, _mapper); _db.SaveChanges(); details.Update(activityToUpdate); return(Result.Success <ActivityDetails, ValidityError>(details)); } catch (Exception e) { var message = $"Failed to save activity '{activity?.Id}'"; _logger.LogError(e, message); return(ValidityError.ForInvalidObject <ActivityDetails>(message)); } Participant CreateNewParticipant(User user) { return(new Participant { UserId = user.Id, User = user, DismissUntilTimeOfDay = _appSettings.Value.PushNotifications.DefaultDismissTime, NotificationSettings = new List <NotificationSetting>() }); } }