Winify – Rev 76

Subversion Repositories:
Rev:
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 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<GotifyConnection> _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.Toasts _toasts;

        #endregion

        #region Constructors, Destructors and Finalizers

        public MainForm()
        {
            InitializeComponent();

            _cancellationTokenSource = new CancellationTokenSource();
            _cancellationToken = _cancellationTokenSource.Token;

            ChangedConfigurationContinuation = new ScheduledContinuation();

            _toasts = new Toasts.Toasts(_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);
        }

        /// <summary>
        ///     Clean up any resources being used.
        /// </summary>
        /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
        protected override void Dispose(bool disposing)
        {
            if (disposing && components != null) components.Dispose();

            base.Dispose(disposing);
        }

        #endregion

        #region Event Handlers

        private async void MainForm_Load(object sender, EventArgs e)
        {
            Configuration = await LoadConfiguration();

            var servers = await LoadServers();
            _gotifyConnections = new ConcurrentBag<GotifyConnection>();
            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;
                }

                var configuredNotification = new ToastForm(
                    $"{e.Message.Title} ({e.Message.Server.Name}/{e.Message.AppId})",
                    e.Message.Message)
                {
                    EnableChime = announcement.EnableChime,
                    Chime = announcement.Chime ?? Configuration.Chime,
                    LingerTime = (int)announcement.LingerTime,
                    Image = e.Image
                };

                await _toasts.Queue(configuredNotification);

                return;
            }

            if (Configuration.InfiniteToastDuration)
            {
                var infiniteToastForm = new ToastForm(
                    $"{e.Message.Title} ({e.Message.Server.Name}/{e.Message.AppId})",
                    e.Message.Message)
                {
                    Chime = Configuration.Chime,
                    Image = e.Image
                };

                await _toasts.Queue(infiniteToastForm);

                return;
            }

            var toastForm = new ToastForm(
                $"{e.Message.Title} ({e.Message.Server.Name}/{e.Message.AppId})",
                e.Message.Message)
            {
                Chime = Configuration.Chime,
                LingerTime = Configuration.ToastDuration,
                Image = e.Image
            };

            await _toasts.Queue(toastForm);
        }

        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",
                        "<!ATTLIST Configuration xmlns:xsi CDATA #IMPLIED xsi:noNamespaceSchemaLocation CDATA #IMPLIED>",
                        CancellationToken.None))
            {
                case SerializationSuccess<Configuration.Configuration> _:
                    Log.Information("Serialized configuration.");
                    break;
                case SerializationFailure serializationFailure:
                    Log.Warning(serializationFailure.Exception.Message, "Failed to serialize configuration.");
                    break;
            }
        }

        public static async Task<Configuration.Configuration> LoadConfiguration()
        {
            if (!Directory.Exists(Constants.UserApplicationDirectory))
                Directory.CreateDirectory(Constants.UserApplicationDirectory);

            var deserializationResult =
                await Serialization.Deserialize<Configuration.Configuration>(Constants.ConfigurationFile,
                    Constants.ConfigurationNamespace, Constants.ConfigurationXsd, CancellationToken.None);

            switch (deserializationResult)
            {
                case SerializationSuccess<Configuration.Configuration> 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",
                        "<!ATTLIST Announcements xmlns:xsi CDATA #IMPLIED xsi:noNamespaceSchemaLocation CDATA #IMPLIED>",
                        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<Server>()
            };

            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",
                        "<!ATTLIST Servers xmlns:xsi CDATA #IMPLIED xsi:noNamespaceSchemaLocation CDATA #IMPLIED>",
                        CancellationToken.None))
            {
                case SerializationFailure serializationFailure:
                    Log.Warning(serializationFailure.Exception, "Unable to serialize servers.");
                    break;
            }
        }

        private static async Task<Announcements.Announcements> LoadAnnouncements()
        {
            if (!Directory.Exists(Constants.UserApplicationDirectory))
                Directory.CreateDirectory(Constants.UserApplicationDirectory);

            var deserializationResult =
                await Serialization.Deserialize<Announcements.Announcements>(Constants.AnnouncementsFile,
                    "urn:winify-announcements-schema", "Announcements.xsd", CancellationToken.None);

            switch (deserializationResult)
            {
                case SerializationSuccess<Announcements.Announcements> 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<Servers.Servers> LoadServers()
        {
            if (!Directory.Exists(Constants.UserApplicationDirectory))
                Directory.CreateDirectory(Constants.UserApplicationDirectory);

            var deserializationResult =
                await Serialization.Deserialize<Servers.Servers>(Constants.ServersFile,
                    "urn:winify-servers-schema", "Servers.xsd", CancellationToken.None);

            switch (deserializationResult)
            {
                case SerializationSuccess<Servers.Servers> serializationSuccess:
                    // Decrypt password.
                    var deviceId = Miscellaneous.GetMachineGuid();
                    var @protected = new Servers.Servers
                    {
                        Server = new BindingListWithCollectionChanged<Server>()
                    };
                    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
    }
}

Generated by GNU Enscript 1.6.5.90.