Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions App/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,18 @@ public static IServiceCollection AddWsjtxWatcherServices(this IServiceCollection
services.AddSingleton<RelayWsjtGateway>();
services.AddSingleton<IWsjtGateway, WsjtGateway>();
services.AddSingleton<ICloudlogIgnoredCallsignImportService, CloudlogIgnoredCallsignImportService>();
services.AddSingleton<RuleFieldValueResolver>();
services.AddSingleton<RuleNamedSetResolver>();
services.AddSingleton<AlertRuleEvaluator>();
services.AddSingleton<AlertRuleCooldownGate>();
services.AddSingleton<DecodedMessageFactory>();
services.AddSingleton<WatcherController>();
services.AddSingleton<MainViewModel>();
services.AddSingleton<SettingsViewModel>();
services.AddSingleton<AlertRulesViewModel>();
services.AddTransient<AlertRuleEditorViewModel>();
services.AddTransient<RelaySourceSelectionViewModel>();
services.AddSingleton<CallsignPatternViewModel>();
services.AddSingleton<IgnoredCallsignViewModel>();
services.AddTransient<DxccSelectionViewModel>();
return services;
}
}
39 changes: 39 additions & 0 deletions Core/Models/AlertRule.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System.Text.Json.Serialization;

namespace WsjtxWatcher.Core.Models;

public sealed class AlertRule
{
public string Id { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public RuleSource Source { get; set; } = RuleSource.UserDefined;
public bool IsEnabled { get; set; }
public RuleTriggerType TriggerType { get; set; } = RuleTriggerType.DecodeMessage;
public RuleConditionGroup RootCondition { get; set; } = RuleConditionGroup.CreateAll();
public RuleActionConfig Actions { get; set; } = new();
public int CooldownSeconds { get; set; } = 10;
public int Priority { get; set; } = 100;
public int SortOrder { get; set; }
public DateTime CreatedAtUtc { get; set; } = DateTime.UtcNow;

[JsonIgnore]
public bool IsReadOnly => Source == RuleSource.SystemPreset;

public AlertRule Clone()
{
return new AlertRule
{
Id = Id,
Name = Name,
Source = Source,
IsEnabled = IsEnabled,
TriggerType = TriggerType,
RootCondition = RootCondition.Clone(),
Actions = Actions.Clone(),
CooldownSeconds = CooldownSeconds,
Priority = Priority,
SortOrder = SortOrder,
CreatedAtUtc = CreatedAtUtc
};
}
}
193 changes: 193 additions & 0 deletions Core/Models/AlertRuleCatalog.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
namespace WsjtxWatcher.Core.Models;

public static class AlertRuleCatalog
{
public const string SystemMyCallsignRuleId = "system_my_callsign";
public const string SystemWatchedCallsignRuleId = "system_watched_callsign";
public const string SystemAnyMessageRuleId = "system_any_message";
public const string SystemDxccRuleId = "system_selected_dxcc";
public const string SystemLoggedQsoRuleId = "system_logged_qso";
public const int DefaultCooldownSeconds = 10;
public const int DefaultPriority = 100;

public static IReadOnlyList<AlertRule> CreateSystemRules()
{
return
[
CreateSystemRule(
SystemMyCallsignRuleId,
"我的呼号",
RuleTriggerType.DecodeMessage,
RuleConditionGroup.CreateAny(
RulePredicate.Create(RuleField.TransmitterCallsign, RuleOperator.Equals, RuleOperand.ForContextRef(RuleContextRef.MyCallsign)),
RulePredicate.Create(RuleField.ReceiverCallsign, RuleOperator.Equals, RuleOperand.ForContextRef(RuleContextRef.MyCallsign)))),
CreateSystemRule(
SystemWatchedCallsignRuleId,
"指定呼号",
RuleTriggerType.DecodeMessage,
RuleConditionGroup.CreateAny(
RulePredicate.Create(RuleField.TransmitterCallsign, RuleOperator.Regex, RuleOperand.ForString("^JA")),
RulePredicate.Create(RuleField.ReceiverCallsign, RuleOperator.Regex, RuleOperand.ForString("^JA")))),
CreateSystemRule(
SystemAnyMessageRuleId,
"任意消息",
RuleTriggerType.DecodeMessage,
RuleConditionGroup.CreateAll(new RuleConstantPredicate { Value = true })),
CreateSystemRule(
SystemDxccRuleId,
"指定 DXCC",
RuleTriggerType.DecodeMessage,
RuleConditionGroup.CreateAny(
RulePredicate.Create(RuleField.FromCountryId, RuleOperator.In, RuleOperand.ForNumberList(DefaultSelectedDxccIds.Select(id => (double)id))),
RulePredicate.Create(RuleField.ToCountryId, RuleOperator.In, RuleOperand.ForNumberList(DefaultSelectedDxccIds.Select(id => (double)id))))),
CreateSystemRule(
SystemLoggedQsoRuleId,
"QSO 完成",
RuleTriggerType.LoggedQso,
RuleConditionGroup.CreateAll(new RuleConstantPredicate { Value = true }))
];
}

public static IReadOnlyCollection<int> DefaultSelectedDxccIds { get; } =
[
257,
67,
115,
71,
229,
225,
17,
172,
363,
16
];

public static List<AlertRule> NormalizeCustomRules(IEnumerable<AlertRule>? rules)
{
return (rules ?? [])
.Where(rule => rule is not null && !string.IsNullOrWhiteSpace(rule.Id))
.Select(rule => SanitizeCustomRule(rule.Clone()))
.OrderBy(rule => rule.SortOrder)
.ThenBy(rule => rule.CreatedAtUtc)
.ThenBy(rule => rule.Id, StringComparer.Ordinal)
.ToList();
}

public static IReadOnlyList<AlertRule> GetAllRules(AppSettings settings)
{
return [.. CreateSystemRules(), .. NormalizeCustomRules(settings.AlertRules)];
}

public static AlertRule GetRequiredRule(AppSettings settings, string ruleId)
{
var rule = FindRule(settings, ruleId);
if (rule is null)
{
throw new InvalidOperationException($"Alert rule '{ruleId}' was not found.");
}

return rule;
}

public static AlertRule? FindRule(AppSettings settings, string ruleId)
{
foreach (var systemRule in CreateSystemRules())
{
if (string.Equals(systemRule.Id, ruleId, StringComparison.Ordinal))
{
return systemRule.Clone();
}
}

return NormalizeCustomRules(settings.AlertRules)
.FirstOrDefault(rule => string.Equals(rule.Id, ruleId, StringComparison.Ordinal))?
.Clone();
}

public static AlertRule CreateCustomRule(RuleTriggerType triggerType, int nextSortOrder)
{
return new AlertRule
{
Id = $"rule_{Guid.NewGuid():N}",
Name = triggerType == RuleTriggerType.LoggedQso ? "新建 QSO 规则" : "新建消息规则",
Source = RuleSource.UserDefined,
IsEnabled = true,
TriggerType = triggerType,
RootCondition = RuleConditionGroup.CreateAll(),
Actions = new RuleActionConfig(),
CooldownSeconds = DefaultCooldownSeconds,
Priority = DefaultPriority,
SortOrder = nextSortOrder,
CreatedAtUtc = DateTime.UtcNow
};
}

public static void UpsertCustomRule(AppSettings settings, AlertRule rule)
{
if (rule.Source == RuleSource.SystemPreset)
{
throw new InvalidOperationException("System preset rules are read-only.");
}

var normalized = NormalizeCustomRules(settings.AlertRules);
var sanitized = SanitizeCustomRule(rule.Clone());
var index = normalized.FindIndex(candidate => string.Equals(candidate.Id, sanitized.Id, StringComparison.Ordinal));
if (index >= 0)
{
normalized[index] = sanitized;
}
else
{
normalized.Add(sanitized);
}

settings.AlertRules = NormalizeCustomRules(normalized);
}

public static bool RemoveCustomRule(AppSettings settings, string ruleId)
{
var normalized = NormalizeCustomRules(settings.AlertRules);
var removed = normalized.RemoveAll(rule => string.Equals(rule.Id, ruleId, StringComparison.Ordinal)) > 0;
settings.AlertRules = NormalizeCustomRules(normalized);
return removed;
}

public static bool IsSystemRuleId(string ruleId)
{
return string.Equals(ruleId, SystemMyCallsignRuleId, StringComparison.Ordinal)
|| string.Equals(ruleId, SystemWatchedCallsignRuleId, StringComparison.Ordinal)
|| string.Equals(ruleId, SystemAnyMessageRuleId, StringComparison.Ordinal)
|| string.Equals(ruleId, SystemDxccRuleId, StringComparison.Ordinal)
|| string.Equals(ruleId, SystemLoggedQsoRuleId, StringComparison.Ordinal);
}

private static AlertRule CreateSystemRule(string id, string name, RuleTriggerType triggerType, RuleConditionGroup rootCondition)
{
return new AlertRule
{
Id = id,
Name = name,
Source = RuleSource.SystemPreset,
IsEnabled = true,
TriggerType = triggerType,
RootCondition = rootCondition,
Actions = new RuleActionConfig(),
CooldownSeconds = DefaultCooldownSeconds,
Priority = DefaultPriority,
SortOrder = 0,
CreatedAtUtc = DateTime.UnixEpoch
};
}

private static AlertRule SanitizeCustomRule(AlertRule rule)
{
rule.Id = (rule.Id ?? string.Empty).Trim();
rule.Name = string.IsNullOrWhiteSpace(rule.Name) ? "未命名规则" : rule.Name.Trim();
rule.Source = RuleSource.UserDefined;
rule.CooldownSeconds = Math.Max(0, rule.CooldownSeconds);
rule.Priority = Math.Max(0, rule.Priority);
rule.RootCondition = rule.RootCondition?.Clone() ?? RuleConditionGroup.CreateAll();
rule.Actions = rule.Actions?.Clone() ?? new RuleActionConfig();
return rule;
}
}
47 changes: 4 additions & 43 deletions Core/Models/AppSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,39 +12,13 @@ public sealed class AppSettings
public string Language { get; set; } = string.Empty;
public string MyCallsign { get; set; } = string.Empty;
public string MyGrid { get; set; } = string.Empty;
public List<string> WatchedCallsignPatterns { get; set; } = [];
public bool NotifyOnMyCall { get; set; }
public bool NotifyOnAnyMessage { get; set; }
public bool NotifyOnSelectedDxcc { get; set; }
public bool NotifyOnLoggedQso { get; set; }
public bool VibrateOnMyCall { get; set; }
public bool VibrateOnAnyMessage { get; set; }
public bool VibrateOnSelectedDxcc { get; set; }
public bool VibrateOnLoggedQso { get; set; }
public List<AlertRule> AlertRules { get; set; } = [];
public bool AutoIgnoreLoggedQso { get; set; } = true;
public IgnoredCallsignMatchTarget IgnoredCallsignMatchTarget { get; set; } = IgnoredCallsignMatchTarget.TransmitterOnly;
public WatchedCallsignMatchTarget WatchedCallsignMatchTarget { get; set; } = WatchedCallsignMatchTarget.TransmitterOnly;
public SelectedDxccMatchTarget SelectedDxccMatchTarget { get; set; } = SelectedDxccMatchTarget.TransmitterOnly;
public AppTheme Theme { get; set; } = AppTheme.FollowSystem;
public HashSet<int> PreferredDxccIds { get; set; } = new(DefaultPreferredDxccIds);

public static IReadOnlyCollection<int> DefaultPreferredDxccIds { get; } = new[]
{
257,
67,
115,
71,
229,
225,
17,
172,
363,
16
};

public AppSettings Clone()
{
var clone = new AppSettings
return new AppSettings
{
DataSourceType = DataSourceType,
Port = Port,
Expand All @@ -56,22 +30,9 @@ public AppSettings Clone()
Language = Language,
MyCallsign = MyCallsign,
MyGrid = MyGrid,
WatchedCallsignPatterns = [.. WatchedCallsignPatterns],
NotifyOnMyCall = NotifyOnMyCall,
NotifyOnAnyMessage = NotifyOnAnyMessage,
NotifyOnSelectedDxcc = NotifyOnSelectedDxcc,
NotifyOnLoggedQso = NotifyOnLoggedQso,
VibrateOnMyCall = VibrateOnMyCall,
VibrateOnAnyMessage = VibrateOnAnyMessage,
VibrateOnSelectedDxcc = VibrateOnSelectedDxcc,
VibrateOnLoggedQso = VibrateOnLoggedQso,
AlertRules = AlertRuleCatalog.NormalizeCustomRules(AlertRules),
Theme = Theme,
AutoIgnoreLoggedQso = AutoIgnoreLoggedQso,
IgnoredCallsignMatchTarget = IgnoredCallsignMatchTarget,
WatchedCallsignMatchTarget = WatchedCallsignMatchTarget,
SelectedDxccMatchTarget = SelectedDxccMatchTarget,
PreferredDxccIds = new HashSet<int>(PreferredDxccIds)
AutoIgnoreLoggedQso = AutoIgnoreLoggedQso
};
return clone;
}
}
5 changes: 2 additions & 3 deletions Core/Models/DecodedRadioMessage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,9 @@ public sealed class DecodedRadioMessage
public int ToCountryId { get; init; }
public int FromCountryId { get; init; }
public bool ContainsMyCallsign { get; init; }
public bool MatchesWatchedCallsignPattern { get; init; }
public bool MatchesSelectedDxcc { get; init; }
public double DialFrequencyHz { get; init; }
public bool IsIgnored { get; set; }
public string CurrentBand { get; init; } = string.Empty;
public bool MatchesAlertRule { get; set; }

public static DecodedRadioMessage CreateUserTransmit(string message, string mode)
{
Expand Down
9 changes: 0 additions & 9 deletions Core/Models/IgnoredCallsignMatchTarget.cs

This file was deleted.

7 changes: 7 additions & 0 deletions Core/Models/NamedSetBandMatchMode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace WsjtxWatcher.Core.Models;

public enum NamedSetBandMatchMode
{
MatchBand,
IgnoreBand
}
22 changes: 22 additions & 0 deletions Core/Models/RuleActionConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace WsjtxWatcher.Core.Models;

public sealed class RuleActionConfig
{
public bool SendNotification { get; set; }
public bool Vibrate { get; set; }
public string NotificationTitleTemplate { get; set; } = string.Empty;
public string NotificationBodyTemplate { get; set; } = string.Empty;
public int HighlightColorArgb { get; set; }

public RuleActionConfig Clone()
{
return new RuleActionConfig
{
SendNotification = SendNotification,
Vibrate = Vibrate,
NotificationTitleTemplate = NotificationTitleTemplate,
NotificationBodyTemplate = NotificationBodyTemplate,
HighlightColorArgb = HighlightColorArgb
};
}
}
Loading
Loading