using System; using System.Collections.Concurrent; using System.ComponentModel; using System.Drawing; using System.IO; using System.Net; using System.Reflection; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; using Microsoft.Win32; using NetSparkleUpdater; using NetSparkleUpdater.Enums; using NetSparkleUpdater.SignatureVerifiers; using NetSparkleUpdater.UI.WinForms; using Serilog; using Servers; using Toasts; using Winify.Gotify; using Winify.Settings; using Winify.Utilities; using Winify.Utilities.Serialization; using ScheduledContinuation = Toasts.ScheduledContinuation; namespace Winify { public partial class MainForm : Form { #region Public Enums, Properties and Fields public Configuration.Configuration Configuration { get; set; } public ScheduledContinuation ChangedConfigurationContinuation { get; set; } public bool MemorySinkEnabled { get; set; } #endregion #region Private Delegates, Events, Enums, Properties, Indexers and Fields private AboutForm _aboutForm; private ConcurrentBag _gotifyConnections; private SettingsForm _settingsForm; private readonly SparkleUpdater _sparkle; private readonly CancellationTokenSource _cancellationTokenSource; private readonly CancellationToken _cancellationToken; private LogViewForm _logViewForm; private readonly LogMemorySink _memorySink; private readonly Toasts.ToastDisplay _toastDisplay; #endregion #region Constructors, Destructors and Finalizers public MainForm() { InitializeComponent(); SystemEvents.PowerModeChanged += OnPowerModeChanged; _cancellationTokenSource = new CancellationTokenSource(); _cancellationToken = _cancellationTokenSource.Token; ChangedConfigurationContinuation = new ScheduledContinuation(); _toastDisplay = new Toasts.ToastDisplay(_cancellationToken); } public MainForm(Mutex mutex) : this() { _memorySink = new LogMemorySink(); Log.Logger = new LoggerConfiguration() .MinimumLevel.Debug() .WriteTo.Conditional(condition => MemorySinkEnabled, configureSink => configureSink.Sink(_memorySink)) .WriteTo.File(Path.Combine(Constants.UserApplicationDirectory, "Logs", $"{Constants.AssemblyName}.log"), rollingInterval: RollingInterval.Day) .CreateLogger(); // Start application update. var manifestModuleName = Assembly.GetEntryAssembly().ManifestModule.FullyQualifiedName; var icon = Icon.ExtractAssociatedIcon(manifestModuleName); _sparkle = new SparkleUpdater("https://winify.grimore.org/update/appcast.xml", new Ed25519Checker(SecurityMode.Strict, "LonrgxVjSF0GnY4hzwlRJnLkaxnDn2ikdmOifILzLJY=")) { UIFactory = new UIFactory(icon), RelaunchAfterUpdate = true, SecurityProtocolType = SecurityProtocolType.Tls12 }; _sparkle.StartLoop(true, true); } /// /// Clean up any resources being used. /// /// true if managed resources should be disposed; otherwise, false. protected override void Dispose(bool disposing) { if (disposing && components != null) components.Dispose(); base.Dispose(disposing); } #endregion #region Event Handlers private async void OnPowerModeChanged(object sender, PowerModeChangedEventArgs e) { switch (e.Mode) { case PowerModes.Resume: // Refresh connection to gotify server. while (_gotifyConnections.TryTake(out var gotifyConnection)) { gotifyConnection.GotifyMessage -= GotifyConnectionGotifyMessage; await gotifyConnection.Stop(); gotifyConnection.Dispose(); } var servers = await LoadServers(); foreach (var server in servers.Server) { var gotifyConnection = new GotifyConnection(server, Configuration); gotifyConnection.GotifyMessage += GotifyConnectionGotifyMessage; gotifyConnection.Start(); _gotifyConnections.Add(gotifyConnection); } break; } } private async void MainForm_Load(object sender, EventArgs e) { Configuration = await LoadConfiguration(); var servers = await LoadServers(); _gotifyConnections = new ConcurrentBag(); foreach (var server in servers.Server) { var gotifyConnection = new GotifyConnection(server, Configuration); gotifyConnection.GotifyMessage += GotifyConnectionGotifyMessage; gotifyConnection.Start(); _gotifyConnections.Add(gotifyConnection); } } private void LogViewToolStripMenuItem_Click(object sender, EventArgs e) { if (_logViewForm != null) { return; } _logViewForm = new LogViewForm(this, _memorySink, _cancellationToken); _logViewForm.Closing += LogViewForm_Closing; _logViewForm.Show(); } private void LogViewForm_Closing(object sender, CancelEventArgs e) { if (_logViewForm == null) { return; } _logViewForm.Closing -= LogViewForm_Closing; _logViewForm.Close(); _logViewForm = null; } private async void SettingsToolStripMenuItem_Click(object sender, EventArgs e) { if (_settingsForm == null) { var servers = await LoadServers(); var announcements = await LoadAnnouncements(); _settingsForm = new SettingsForm(this, servers, announcements, _cancellationToken); _settingsForm.Save += SettingsForm_Save; _settingsForm.Closing += SettingsForm_Closing; _settingsForm.Show(); } } private async void SettingsForm_Save(object sender, SettingsSavedEventArgs e) { // Save the configuration. Miscellaneous.LaunchOnBootSet(Configuration.LaunchOnBoot); ChangedConfigurationContinuation.Schedule(TimeSpan.FromSeconds(1), async () => { await SaveConfiguration(); }, _cancellationToken); // Save the servers. await Task.WhenAll(SaveServers(e.Servers), SaveAnnouncements(e.Announcements)); // Update connections to gotify servers. while (_gotifyConnections.TryTake(out var gotifyConnection)) { gotifyConnection.GotifyMessage -= GotifyConnectionGotifyMessage; await gotifyConnection.Stop(); gotifyConnection.Dispose(); } foreach (var server in e.Servers.Server) { var gotifyConnection = new GotifyConnection(server, Configuration); gotifyConnection.GotifyMessage += GotifyConnectionGotifyMessage; gotifyConnection.Start(); _gotifyConnections.Add(gotifyConnection); } } private async void GotifyConnectionGotifyMessage(object sender, GotifyMessageEventArgs e) { var announcements = await LoadAnnouncements(); foreach (var announcement in announcements.Announcement) { if (announcement.AppId != e.Message.AppId) { continue; } if (announcement.Ignore) { return; } if (announcement.LingerTime <= 0) { return; } await _toastDisplay.Queue( new ToastDisplayData { Title = $"{e.Message.Title} ({e.Message.Server.Name}/{e.Message.AppId})", Body = e.Message.Message, EnableChime = announcement.EnableChime, Chime = announcement.Chime ?? Configuration.Chime, LingerTime = (int)announcement.LingerTime, Image = e.Image, Content = e.Message.Extras.GotifyMessageExtrasClientDisplay.ContentType }); return; } if (Configuration.InfiniteToastDuration) { await _toastDisplay.Queue(new ToastDisplayData { Title = $"{e.Message.Title} ({e.Message.Server.Name}/{e.Message.AppId})", Body = e.Message.Message, Chime = Configuration.Chime, Image = e.Image, Content = e.Message.Extras.GotifyMessageExtrasClientDisplay.ContentType }); return; } await _toastDisplay.Queue(new ToastDisplayData { Title = $"{e.Message.Title} ({e.Message.Server.Name}/{e.Message.AppId})", Body = e.Message.Message, Chime = Configuration.Chime, LingerTime = Configuration.ToastDuration, Image = e.Image, Content = e.Message.Extras.GotifyMessageExtrasClientDisplay.ContentType }); } private void SettingsForm_Closing(object sender, CancelEventArgs e) { if (_settingsForm == null) { return; } _settingsForm.Save -= SettingsForm_Save; _settingsForm.Closing -= SettingsForm_Closing; _settingsForm.Dispose(); _settingsForm = null; } private void AboutToolStripMenuItem_Click(object sender, EventArgs e) { if (_aboutForm != null) { return; } _aboutForm = new AboutForm(); _aboutForm.Closing += AboutForm_Closing; _aboutForm.Show(); } private void AboutForm_Closing(object sender, CancelEventArgs e) { if (_aboutForm == null) { return; } _aboutForm.Closing -= AboutForm_Closing; _aboutForm.Dispose(); _aboutForm = null; } private void QuitToolStripMenuItem_Click(object sender, EventArgs e) { Close(); } private async void UpdateToolStripMenuItem_Click(object sender, EventArgs e) { // Manually check for updates, this will not show a ui var result = await _sparkle.CheckForUpdatesQuietly(); var updates = result.Updates; if (result.Status == UpdateStatus.UpdateAvailable) { // if update(s) are found, then we have to trigger the UI to show it gracefully _sparkle.ShowUpdateNeededUI(); return; } MessageBox.Show("No updates available at this time.", "Winify", MessageBoxButtons.OK, MessageBoxIcon.Asterisk, MessageBoxDefaultButton.Button1, MessageBoxOptions.DefaultDesktopOnly, false); } #endregion #region Public Methods public async Task SaveConfiguration() { if (!Directory.Exists(Constants.UserApplicationDirectory)) Directory.CreateDirectory(Constants.UserApplicationDirectory); switch (await Serialization.Serialize(Configuration, Constants.ConfigurationFile, "Configuration", "", CancellationToken.None)) { case SerializationSuccess _: Log.Information("Serialized configuration."); break; case SerializationFailure serializationFailure: Log.Warning(serializationFailure.Exception.Message, "Failed to serialize configuration."); break; } } public static async Task LoadConfiguration() { if (!Directory.Exists(Constants.UserApplicationDirectory)) Directory.CreateDirectory(Constants.UserApplicationDirectory); var deserializationResult = await Serialization.Deserialize(Constants.ConfigurationFile, Constants.ConfigurationNamespace, Constants.ConfigurationXsd, CancellationToken.None); switch (deserializationResult) { case SerializationSuccess serializationSuccess: return serializationSuccess.Result; case SerializationFailure serializationFailure: Log.Warning(serializationFailure.Exception, "Failed to load configuration."); return new Configuration.Configuration(); default: return new Configuration.Configuration(); } } #endregion #region Private Methods private static async Task SaveAnnouncements(Announcements.Announcements announcements) { switch (await Serialization.Serialize(announcements, Constants.AnnouncementsFile, "Announcements", "", CancellationToken.None)) { case SerializationFailure serializationFailure: Log.Warning(serializationFailure.Exception, "Unable to serialize announcements."); break; } } private static async Task SaveServers(Servers.Servers servers) { // Encrypt password for all servers. var deviceId = Miscellaneous.GetMachineGuid(); var @protected = new Servers.Servers { Server = new BindingListWithCollectionChanged() }; foreach (var server in servers.Server) { var password = Encoding.UTF8.GetBytes(server.Password); var encrypted = await AES.Encrypt(password, deviceId); var armored = Convert.ToBase64String(encrypted); @protected.Server.Add(new Server(server.Name, server.Url, server.Username, armored)); } switch (await Serialization.Serialize(@protected, Constants.ServersFile, "Servers", "", CancellationToken.None)) { case SerializationFailure serializationFailure: Log.Warning(serializationFailure.Exception, "Unable to serialize servers."); break; } } private static async Task LoadAnnouncements() { if (!Directory.Exists(Constants.UserApplicationDirectory)) Directory.CreateDirectory(Constants.UserApplicationDirectory); var deserializationResult = await Serialization.Deserialize(Constants.AnnouncementsFile, "urn:winify-announcements-schema", "Announcements.xsd", CancellationToken.None); switch (deserializationResult) { case SerializationSuccess serializationSuccess: return serializationSuccess.Result; case SerializationFailure serializationFailure: Log.Warning(serializationFailure.Exception, "Unable to load announcements."); return new Announcements.Announcements(); default: return new Announcements.Announcements(); } } private static async Task LoadServers() { if (!Directory.Exists(Constants.UserApplicationDirectory)) Directory.CreateDirectory(Constants.UserApplicationDirectory); var deserializationResult = await Serialization.Deserialize(Constants.ServersFile, "urn:winify-servers-schema", "Servers.xsd", CancellationToken.None); switch (deserializationResult) { case SerializationSuccess serializationSuccess: // Decrypt password. var deviceId = Miscellaneous.GetMachineGuid(); var @protected = new Servers.Servers { Server = new BindingListWithCollectionChanged() }; foreach (var server in serializationSuccess.Result.Server) { var unarmored = Convert.FromBase64String(server.Password); byte[] decrypted; try { decrypted = await AES.Decrypt(unarmored, deviceId); } catch(Exception exception) { Log.Warning(exception, $"Could not decrypt password for server {server.Name} in configuration file."); continue; } var password = Encoding.UTF8.GetString(decrypted); @protected.Server.Add(new Server(server.Name, server.Url, server.Username, password)); } return @protected; case SerializationFailure serializationFailure: Log.Warning(serializationFailure.Exception, "Unable to load servers."); return new Servers.Servers(); default: return new Servers.Servers(); } } #endregion } }