using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Text.RegularExpressions; using Cleanuparr.Domain.Enums; using ValidationException = Cleanuparr.Domain.Exceptions.ValidationException; namespace Cleanuparr.Persistence.Models.Configuration.Notification; public sealed partial record PushoverConfig : IConfig { [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public Guid Id { get; init; } = Guid.NewGuid(); [Required] public Guid NotificationConfigId { get; init; } public NotificationConfig NotificationConfig { get; init; } = null!; /// /// Application API token (30 characters, [A-Za-z0-9]) /// [Required] [MaxLength(50)] public string ApiToken { get; init; } = string.Empty; /// /// User/group key (30 characters, [A-Za-z0-9]) /// [Required] [MaxLength(50)] public string UserKey { get; init; } = string.Empty; /// /// Target specific devices (comma-separated when sent to API) /// public List Devices { get; init; } = []; /// /// Notification priority (-2 to 2) /// [Required] public PushoverPriority Priority { get; init; } = PushoverPriority.Normal; /// /// Notification sound (built-in or custom) /// [MaxLength(50)] public string? Sound { get; init; } /// /// Retry interval in seconds for emergency priority (min 30) /// public int? Retry { get; init; } /// /// Expiration time in seconds for emergency priority (max 10800) /// public int? Expire { get; init; } /// /// Tags for receipt tracking and batch cancellation /// public List Tags { get; init; } = []; [GeneratedRegex(@"^[A-Za-z0-9_-]+$")] private static partial Regex DeviceNameRegex(); public bool IsValid() { if (string.IsNullOrWhiteSpace(ApiToken) || string.IsNullOrWhiteSpace(UserKey)) { return false; } if (Priority == PushoverPriority.Emergency) { if (Retry is null or < 30) { return false; } if (Expire is null or < 1 or > 10800) { return false; } } // Sound, if provided, must not be whitespace-only if (Sound is not null && Sound.Length > 0 && string.IsNullOrWhiteSpace(Sound)) { return false; } return true; } public void Validate() { if (string.IsNullOrWhiteSpace(ApiToken)) { throw new ValidationException("Pushover API token is required"); } if (string.IsNullOrWhiteSpace(UserKey)) { throw new ValidationException("Pushover user key is required"); } if (Priority == PushoverPriority.Emergency) { if (!Retry.HasValue || Retry.Value < 30) { throw new ValidationException("Retry interval must be at least 30 seconds for emergency priority"); } if (!Expire.HasValue || Expire.Value < 1) { throw new ValidationException("Expire time is required for emergency priority"); } if (Expire.Value > 10800) { throw new ValidationException("Expire time cannot exceed 10800 seconds (3 hours)"); } } // Validate device names if provided foreach (string device in Devices.Where(d => !string.IsNullOrWhiteSpace(d))) { if (device.Length > 25) { throw new ValidationException($"Device name '{device}' exceeds 25 character limit"); } if (!DeviceNameRegex().IsMatch(device)) { throw new ValidationException($"Device name '{device}' contains invalid characters. Only letters, numbers, underscores, and hyphens are allowed."); } } // Validate sound - if provided, must not be whitespace-only if (Sound is not null && Sound.Length > 0 && string.IsNullOrWhiteSpace(Sound)) { throw new ValidationException("Sound name cannot be empty or whitespace when specified"); } } }