Esempio n. 1
0
        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"));
        }
Esempio n. 2
0
        public ActivityDetails GetActivityDetails(int activityId, int userId)
        {
            var activity = GetActivity(activityId, true, true);

            return(activity is null ? null : ActivityDetails.Calculate(activity, userId, _mapper));
        }
Esempio n. 3
0
        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));
            }
        }
Esempio n. 4
0
        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>()
                });
            }
        }