using System.Diagnostics; using System.IO; using System.Net; using System.Text; using System.Text.Json; using System.Windows; using Microsoft.Win32; namespace OmniNotify; public sealed class AppStore { private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; private readonly string _statePath; public AppStore() { var root = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "OmniNotify"); Directory.CreateDirectory(root); _statePath = Path.Combine(root, "state.json"); } public AppState Load() { if (!File.Exists(_statePath)) { var state = new AppState(); state.Channels.Add(new Channel { Name = "default" }); return state; } try { return JsonSerializer.Deserialize(File.ReadAllText(_statePath), JsonOptions) ?? new AppState(); } catch { return new AppState(); } } public void Save(AppState state) { CleanupHistory(state); File.WriteAllText(_statePath, JsonSerializer.Serialize(state, JsonOptions)); } private static void CleanupHistory(AppState state) { var cutoff = DateTime.Now.AddDays(-Math.Max(1, state.Settings.RetainDays)); var survivors = state.History.Where(item => item.ReceivedAt >= cutoff) .OrderByDescending(item => item.ReceivedAt) .Take(Math.Max(1, state.Settings.RetainCount)) .OrderBy(item => item.ReceivedAt) .ToList(); state.History.Clear(); foreach (var item in survivors) { state.History.Add(item); } } } public sealed class NotificationRouter { private readonly AppState _state; private readonly AppStore _store; private readonly PopupCoordinator _popupCoordinator; private readonly Queue _rateWindow = new(); public event Action? StateChanged; public NotificationRouter(AppState state, AppStore store, PopupCoordinator popupCoordinator) { _state = state; _store = store; _popupCoordinator = popupCoordinator; } public void Receive(IncomingMessage message, bool recordHistory = true) { System.Windows.Application.Current.Dispatcher.Invoke(() => { if (_state.Settings.CircuitBreakerOpen) { return; } if (IsRateLimited()) { _state.Settings.CircuitBreakerOpen = true; _popupCoordinator.ShowSystem("熔断保护", "检测到大量消息,已开启熔断保护。"); SaveAndNotify(); return; } var channel = _state.Channels.FirstOrDefault(item => string.Equals(item.Name, message.Channel, StringComparison.Ordinal)); if (channel is null) { AddHistory(message, NotificationStatus.IllegalChannel); SaveAndNotify(); return; } if (_state.Settings.DndEnabled) { AddHistory(message, NotificationStatus.DndMuted); SaveAndNotify(); return; } if (recordHistory) { AddHistory(message, NotificationStatus.Displayed); } _popupCoordinator.Show(channel, message); SaveAndNotify(); }); } public void Replay(HistoryItem item) { var channel = _state.Channels.FirstOrDefault(candidate => string.Equals(candidate.Name, item.Channel, StringComparison.Ordinal)); if (channel is not null) { _popupCoordinator.Show(channel, new IncomingMessage { Channel = item.Channel, Title = item.Title, Body = item.Body }); } } public void ResetCircuitBreaker() { _state.Settings.CircuitBreakerOpen = false; _rateWindow.Clear(); SaveAndNotify(); } private bool IsRateLimited() { var now = DateTime.Now; while (_rateWindow.Count > 0 && (now - _rateWindow.Peek()).TotalSeconds > 1) { _rateWindow.Dequeue(); } _rateWindow.Enqueue(now); return _rateWindow.Count > Math.Max(1, _state.Settings.MaxMessagesPerSecond); } private void AddHistory(IncomingMessage message, NotificationStatus status) { _state.History.Add(new HistoryItem { Channel = message.Channel, Title = message.Title, Body = message.Body, Status = status }); } private void SaveAndNotify() { _store.Save(_state); StateChanged?.Invoke(); } } public sealed class LocalHttpServer : IDisposable { private readonly NotificationRouter _router; private HttpListener? _listener; private CancellationTokenSource? _cts; public LocalHttpServer(NotificationRouter router) { _router = router; } public string? Url { get; private set; } public string? LastError { get; private set; } public void Start(int port) { Url = $"http://127.0.0.1:{port}/"; _cts = new CancellationTokenSource(); _listener = new HttpListener(); _listener.Prefixes.Add(Url); try { _listener.Start(); _ = ListenAsync(_cts.Token); } catch (Exception ex) { LastError = ex.Message; } } private async Task ListenAsync(CancellationToken token) { while (!token.IsCancellationRequested && _listener?.IsListening == true) { try { var context = await _listener.GetContextAsync(); _ = Task.Run(() => HandleAsync(context), token); } catch when (token.IsCancellationRequested) { break; } catch (Exception ex) { LastError = ex.Message; } } } private async Task HandleAsync(HttpListenerContext context) { try { if (context.Request.HttpMethod == "GET") { await WriteAsync(context, 200, "Omni-Notify is listening. POST /notify with JSON: {\"channel\":\"default\",\"title\":\"Hi\",\"body\":\"Text\"}"); return; } if (context.Request.HttpMethod != "POST" || context.Request.Url?.AbsolutePath != "/notify") { await WriteAsync(context, 404, "Not found"); return; } using var reader = new StreamReader(context.Request.InputStream, context.Request.ContentEncoding); var body = await reader.ReadToEndAsync(); var message = JsonSerializer.Deserialize(body, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); if (message is null || string.IsNullOrWhiteSpace(message.Channel)) { await WriteAsync(context, 400, "Invalid message"); return; } _router.Receive(message); await WriteAsync(context, 202, "Accepted"); } catch (Exception ex) { await WriteAsync(context, 500, ex.Message); } } private static async Task WriteAsync(HttpListenerContext context, int code, string text) { var bytes = Encoding.UTF8.GetBytes(text); context.Response.StatusCode = code; context.Response.ContentType = "text/plain; charset=utf-8"; context.Response.ContentLength64 = bytes.Length; await context.Response.OutputStream.WriteAsync(bytes); context.Response.Close(); } public void Dispose() { _cts?.Cancel(); _listener?.Close(); } } public static class StartupManager { private const string AppName = "OmniNotify"; public static void Apply(bool enabled) { using var key = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Run", true); if (key is null) { return; } if (enabled) { key.SetValue(AppName, Process.GetCurrentProcess().MainModule?.FileName ?? ""); } else { key.DeleteValue(AppName, false); } } }