From 0a11d5a583b43cf09020a0af9e8561bb28b76640 Mon Sep 17 00:00:00 2001 From: Alexander Horner <33007665+alexhorner@users.noreply.github.com> Date: Tue, 6 Dec 2022 19:37:39 +0000 Subject: [PATCH 01/14] Add very basic but functional support for authentication and custom servers --- .gitignore | 401 +++++++++++++++++++++- ntfysh_client/Form1.Designer.cs | 70 ++-- ntfysh_client/Form1.cs | 4 +- ntfysh_client/Form1.resx | 44 +-- ntfysh_client/NotificationListener.cs | 37 +- ntfysh_client/SubscribeDialog.Designer.cs | 114 +++++- ntfysh_client/SubscribeDialog.cs | 73 +++- ntfysh_client/SubscribedTopic.cs | 23 ++ ntfysh_client/ntfysh_client.csproj | 1 + 9 files changed, 662 insertions(+), 105 deletions(-) create mode 100644 ntfysh_client/SubscribedTopic.cs diff --git a/.gitignore b/.gitignore index 103db3d..f08db5b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,399 @@ -bin/ -obj/ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory .vs/ -packages/ \ No newline at end of file +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml +/.idea \ No newline at end of file diff --git a/ntfysh_client/Form1.Designer.cs b/ntfysh_client/Form1.Designer.cs index e9b6d1e..91caa7e 100644 --- a/ntfysh_client/Form1.Designer.cs +++ b/ntfysh_client/Form1.Designer.cs @@ -40,12 +40,12 @@ namespace ntfysh_client this.exitToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.menuStrip1 = new System.Windows.Forms.MenuStrip(); this.fileToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); - this.helpToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); - this.label1 = new System.Windows.Forms.Label(); this.exitToolStripMenuItem1 = new System.Windows.Forms.ToolStripMenuItem(); - this.aboutToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.helpToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.ntfyshWebsiteToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.toolStripMenuItem1 = new System.Windows.Forms.ToolStripSeparator(); + this.aboutToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.label1 = new System.Windows.Forms.Label(); this.trayContextMenu.SuspendLayout(); this.menuStrip1.SuspendLayout(); this.SuspendLayout(); @@ -75,8 +75,7 @@ namespace ntfysh_client // // notificationTopics // - this.notificationTopics.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) - | System.Windows.Forms.AnchorStyles.Right))); + this.notificationTopics.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); this.notificationTopics.FormattingEnabled = true; this.notificationTopics.Location = new System.Drawing.Point(12, 50); this.notificationTopics.Name = "notificationTopics"; @@ -96,9 +95,7 @@ namespace ntfysh_client // // trayContextMenu // - this.trayContextMenu.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { - this.showControlWindowToolStripMenuItem, - this.exitToolStripMenuItem}); + this.trayContextMenu.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { this.showControlWindowToolStripMenuItem, this.exitToolStripMenuItem }); this.trayContextMenu.Name = "trayContextMenu"; this.trayContextMenu.Size = new System.Drawing.Size(190, 48); // @@ -121,9 +118,7 @@ namespace ntfysh_client // menuStrip1 // this.menuStrip1.BackColor = System.Drawing.Color.White; - this.menuStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { - this.fileToolStripMenuItem, - this.helpToolStripMenuItem}); + this.menuStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { this.fileToolStripMenuItem, this.helpToolStripMenuItem }); this.menuStrip1.Location = new System.Drawing.Point(0, 0); this.menuStrip1.Name = "menuStrip1"; this.menuStrip1.Size = new System.Drawing.Size(353, 24); @@ -132,45 +127,24 @@ namespace ntfysh_client // // fileToolStripMenuItem // - this.fileToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { - this.exitToolStripMenuItem1}); + this.fileToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { this.exitToolStripMenuItem1 }); this.fileToolStripMenuItem.Name = "fileToolStripMenuItem"; this.fileToolStripMenuItem.Size = new System.Drawing.Size(37, 20); this.fileToolStripMenuItem.Text = "File"; // - // helpToolStripMenuItem - // - this.helpToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { - this.ntfyshWebsiteToolStripMenuItem, - this.toolStripMenuItem1, - this.aboutToolStripMenuItem}); - this.helpToolStripMenuItem.Name = "helpToolStripMenuItem"; - this.helpToolStripMenuItem.Size = new System.Drawing.Size(44, 20); - this.helpToolStripMenuItem.Text = "Help"; - // - // label1 - // - this.label1.AutoSize = true; - this.label1.Location = new System.Drawing.Point(12, 34); - this.label1.Name = "label1"; - this.label1.Size = new System.Drawing.Size(145, 13); - this.label1.TabIndex = 1; - this.label1.Text = "Subscribed notification topics"; - // // exitToolStripMenuItem1 // this.exitToolStripMenuItem1.Image = ((System.Drawing.Image)(resources.GetObject("exitToolStripMenuItem1.Image"))); this.exitToolStripMenuItem1.Name = "exitToolStripMenuItem1"; - this.exitToolStripMenuItem1.Size = new System.Drawing.Size(180, 22); + this.exitToolStripMenuItem1.Size = new System.Drawing.Size(93, 22); this.exitToolStripMenuItem1.Text = "Exit"; // - // aboutToolStripMenuItem + // helpToolStripMenuItem // - this.aboutToolStripMenuItem.Image = ((System.Drawing.Image)(resources.GetObject("aboutToolStripMenuItem.Image"))); - this.aboutToolStripMenuItem.Name = "aboutToolStripMenuItem"; - this.aboutToolStripMenuItem.Size = new System.Drawing.Size(185, 22); - this.aboutToolStripMenuItem.Text = "About"; - this.aboutToolStripMenuItem.Click += new System.EventHandler(this.aboutToolStripMenuItem_Click); + this.helpToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { this.ntfyshWebsiteToolStripMenuItem, this.toolStripMenuItem1, this.aboutToolStripMenuItem }); + this.helpToolStripMenuItem.Name = "helpToolStripMenuItem"; + this.helpToolStripMenuItem.Size = new System.Drawing.Size(44, 20); + this.helpToolStripMenuItem.Text = "Help"; // // ntfyshWebsiteToolStripMenuItem // @@ -185,6 +159,23 @@ namespace ntfysh_client this.toolStripMenuItem1.Name = "toolStripMenuItem1"; this.toolStripMenuItem1.Size = new System.Drawing.Size(182, 6); // + // aboutToolStripMenuItem + // + this.aboutToolStripMenuItem.Image = ((System.Drawing.Image)(resources.GetObject("aboutToolStripMenuItem.Image"))); + this.aboutToolStripMenuItem.Name = "aboutToolStripMenuItem"; + this.aboutToolStripMenuItem.Size = new System.Drawing.Size(185, 22); + this.aboutToolStripMenuItem.Text = "About"; + this.aboutToolStripMenuItem.Click += new System.EventHandler(this.aboutToolStripMenuItem_Click); + // + // label1 + // + this.label1.AutoSize = true; + this.label1.Location = new System.Drawing.Point(12, 34); + this.label1.Name = "label1"; + this.label1.Size = new System.Drawing.Size(145, 13); + this.label1.TabIndex = 1; + this.label1.Text = "Subscribed notification topics"; + // // Form1 // this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); @@ -213,7 +204,6 @@ namespace ntfysh_client this.menuStrip1.PerformLayout(); this.ResumeLayout(false); this.PerformLayout(); - } #endregion diff --git a/ntfysh_client/Form1.cs b/ntfysh_client/Form1.cs index 2963b0b..e3e441c 100644 --- a/ntfysh_client/Form1.cs +++ b/ntfysh_client/Form1.cs @@ -36,7 +36,7 @@ namespace ntfysh_client if (result == DialogResult.OK) { - notificationListener.SubscribeToTopic(dialog.getTopicId()); + notificationListener.SubscribeToTopic(dialog.getTopicId(), dialog.getServerUrl(), dialog.getUsername(), dialog.getPassword()); notificationTopics.Items.Add(dialog.getTopicId()); this.SaveTopicsToFile(); } @@ -117,7 +117,7 @@ namespace ntfysh_client while (!reader.EndOfStream) { var topic = reader.ReadLine(); - notificationListener.SubscribeToTopic(topic); + notificationListener.SubscribeToTopic(topic, "https://ntfy.sh", null, null); notificationTopics.Items.Add(topic); } } diff --git a/ntfysh_client/Form1.resx b/ntfysh_client/Form1.resx index 4b4cee7..7adc2ca 100644 --- a/ntfysh_client/Form1.resx +++ b/ntfysh_client/Form1.resx @@ -124,6 +124,24 @@ 123, 17 + + + iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6 + JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAACXBIWXMAAA7DAAAOwwHHb6hkAAAAiklE + QVQ4T8WPQQqAIBBFvUOrXIhu3dcZOkqtu5R1ia4SdRDrxyyGGKmBIOGBDL6nmk9WCGE9yUpW0q9Ads7V + GuCQ/kHAe79joAEO6dcLFmttpQEO6c+Bvpm2oZ0zwB4zVQBiF8cIsMdMDPCb+G2vA/wgP/z6C6WAhBgo + fUFCDGi4BxIGShLpvy5jDoPes/0oNG3VAAAAAElFTkSuQmCC + + + + + iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6 + JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAACXBIWXMAAA7DAAAOwwHHb6hkAAAAgUlE + QVQ4T2OgGjjOLq4AZeIFWNWBBI9yib06zC3uABXCCkDyIHVYDTnMI2pzhEvs5VFucSeoEAo4wiNii08e + DHAZQshwFICumCTNMADyK1gTl2gJiCYUNlgBSDPQ1v8gGipEPKDIBRSFAa6oIsoQQvGM1xCqpESsglgA + seroBRgYAOoOWBJbfVcRAAAAAElFTkSuQmCC + + AAABAAEAMjIAAAEAIADIKAAAFgAAACgAAAAyAAAAZAAAAAEAIAAAAAAAECcAACMuAAAjLgAAAAAAAAAA @@ -303,16 +321,10 @@ /////8AA////////wAD////////AAA== - - - iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6 - JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAACXBIWXMAAA7DAAAOwwHHb6hkAAAAiklE - QVQ4T8WPQQqAIBBFvUOrXIhu3dcZOkqtu5R1ia4SdRDrxyyGGKmBIOGBDL6nmk9WCGE9yUpW0q9Ads7V - GuCQ/kHAe79joAEO6dcLFmttpQEO6c+Bvpm2oZ0zwB4zVQBiF8cIsMdMDPCb+G2vA/wgP/z6C6WAhBgo - fUFCDGi4BxIGShLpvy5jDoPes/0oNG3VAAAAAElFTkSuQmCC - - - + + 269, 17 + + iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6 JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAACXBIWXMAAA7DAAAOwwHHb6hkAAAAgUlE @@ -321,9 +333,6 @@ seroBRgYAOoOWBJbfVcRAAAAAElFTkSuQmCC - - 269, 17 - iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6 @@ -343,15 +352,6 @@ dDIA6OTNQHX/YXysBjAxMcUxMjKuBrGB9DIgPxqmARhwOUBD3sD4WA0QERGRBHLvCwoKygLpm6KiohIw DVBNz5HY2L0AtHkOEE8F4okgvoyMjDSSJsIGsLOzuwKFfnBycppJS0vLAJ39BUQTbQA6RnYBMkY3YAVI gES8Aqp9QAEDAwCq9oYvtggceQAAAABJRU5ErkJggg== - - - - - iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6 - JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAACXBIWXMAAA7DAAAOwwHHb6hkAAAAgUlE - QVQ4T2OgGjjOLq4AZeIFWNWBBI9yib06zC3uABXCCkDyIHVYDTnMI2pzhEvs5VFucSeoEAo4wiNii08e - DHAZQshwFICumCTNMADyK1gTl2gJiCYUNlgBSDPQ1v8gGipEPKDIBRSFAa6oIsoQQvGM1xCqpESsglgA - seroBRgYAOoOWBJbfVcRAAAAAElFTkSuQmCC diff --git a/ntfysh_client/NotificationListener.cs b/ntfysh_client/NotificationListener.cs index 75f6118..54e5bf1 100644 --- a/ntfysh_client/NotificationListener.cs +++ b/ntfysh_client/NotificationListener.cs @@ -6,6 +6,7 @@ using System.IO; using System.Linq; using System.Net; using System.Net.Http; +using System.Net.Http.Headers; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -19,7 +20,7 @@ namespace ntfysh_client private bool disposedValue; - public Dictionary subscribedTopics; + public readonly Dictionary SubscribedTopics = new Dictionary(); public delegate void NotificationReceiveHandler(object sender, NotificationReceiveEventArgs e); public event NotificationReceiveHandler OnNotificationReceive; @@ -27,22 +28,32 @@ namespace ntfysh_client public NotificationListener() { httpClient = new HttpClient(); - subscribedTopics = new Dictionary(); httpClient.Timeout = TimeSpan.FromMilliseconds(Timeout.Infinite); ServicePointManager.DefaultConnectionLimit = 100; } - public async Task SubscribeToTopic(string topicId) + public async Task SubscribeToTopic(string topicId, string serverUrl, string username, string password) { - HttpRequestMessage msg = new HttpRequestMessage(HttpMethod.Get, $"https://ntfy.sh/{HttpUtility.UrlEncode(topicId)}/json"); - using (var response = await httpClient.SendAsync(msg, HttpCompletionOption.ResponseHeadersRead)) + if (string.IsNullOrWhiteSpace(username)) username = null; + if (string.IsNullOrWhiteSpace(password)) password = null; + + HttpRequestMessage msg = new HttpRequestMessage(HttpMethod.Get, $"{serverUrl}/{HttpUtility.UrlEncode(topicId)}/json"); + + if (username != null && password != null) { - using (var body = await response.Content.ReadAsStreamAsync()) + byte[] boundCredentialsBytes = Encoding.UTF8.GetBytes($"{username}:{password}"); + + msg.Headers.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(boundCredentialsBytes)); + } + + using (HttpResponseMessage response = await httpClient.SendAsync(msg, HttpCompletionOption.ResponseHeadersRead)) + { + using (Stream body = await response.Content.ReadAsStreamAsync()) { using (StreamReader reader = new StreamReader(body)) { - subscribedTopics.Add(topicId, reader); + SubscribedTopics.Add(topicId, new SubscribedTopic(topicId, serverUrl, username, password, reader)); try { @@ -71,9 +82,9 @@ namespace ntfysh_client // If the topic is still registered, then that stream wasn't mean to be closed (maybe network failure?) // Restart it - if (subscribedTopics.ContainsKey(topicId)) + if (SubscribedTopics.ContainsKey(topicId)) { - SubscribeToTopic(topicId); + SubscribeToTopic(topicId, serverUrl, username, password); } } } @@ -85,12 +96,12 @@ namespace ntfysh_client { Debug.WriteLine($"Removing topic {topicId}"); - if (subscribedTopics.ContainsKey(topicId)) + if (SubscribedTopics.ContainsKey(topicId)) { // Not moronic to store it in a variable; this solves a race condition in SubscribeToTopic - var topic = subscribedTopics[topicId]; - subscribedTopics.Remove(topicId); - topic.Close(); + var topic = SubscribedTopics[topicId]; + SubscribedTopics.Remove(topicId); + topic.Stream.Close(); } } diff --git a/ntfysh_client/SubscribeDialog.Designer.cs b/ntfysh_client/SubscribeDialog.Designer.cs index e7d369e..30d5a60 100644 --- a/ntfysh_client/SubscribeDialog.Designer.cs +++ b/ntfysh_client/SubscribeDialog.Designer.cs @@ -30,10 +30,16 @@ namespace ntfysh_client private void InitializeComponent() { this.panel1 = new System.Windows.Forms.Panel(); - this.button1 = new System.Windows.Forms.Button(); this.button2 = new System.Windows.Forms.Button(); + this.button1 = new System.Windows.Forms.Button(); this.label1 = new System.Windows.Forms.Label(); this.topicId = new System.Windows.Forms.TextBox(); + this.serverUrl = new System.Windows.Forms.TextBox(); + this.label2 = new System.Windows.Forms.Label(); + this.username = new System.Windows.Forms.TextBox(); + this.label3 = new System.Windows.Forms.Label(); + this.password = new System.Windows.Forms.TextBox(); + this.label4 = new System.Windows.Forms.Label(); this.panel1.SuspendLayout(); this.SuspendLayout(); // @@ -43,23 +49,11 @@ namespace ntfysh_client this.panel1.Controls.Add(this.button2); this.panel1.Controls.Add(this.button1); this.panel1.Dock = System.Windows.Forms.DockStyle.Bottom; - this.panel1.Location = new System.Drawing.Point(0, 81); + this.panel1.Location = new System.Drawing.Point(0, 175); this.panel1.Name = "panel1"; this.panel1.Size = new System.Drawing.Size(297, 44); this.panel1.TabIndex = 0; // - // button1 - // - this.button1.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); - this.button1.Location = new System.Drawing.Point(212, 11); - this.button1.Margin = new System.Windows.Forms.Padding(3, 10, 10, 10); - this.button1.Name = "button1"; - this.button1.Size = new System.Drawing.Size(75, 23); - this.button1.TabIndex = 2; - this.button1.Text = "Subscribe"; - this.button1.UseVisualStyleBackColor = true; - this.button1.Click += new System.EventHandler(this.button1_Click); - // // button2 // this.button2.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); @@ -72,10 +66,22 @@ namespace ntfysh_client this.button2.UseVisualStyleBackColor = true; this.button2.Click += new System.EventHandler(this.button2_Click); // + // button1 + // + this.button1.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); + this.button1.Location = new System.Drawing.Point(212, 11); + this.button1.Margin = new System.Windows.Forms.Padding(3, 10, 10, 10); + this.button1.Name = "button1"; + this.button1.Size = new System.Drawing.Size(75, 23); + this.button1.TabIndex = 2; + this.button1.Text = "Subscribe"; + this.button1.UseVisualStyleBackColor = true; + this.button1.Click += new System.EventHandler(this.button1_Click); + // // label1 // this.label1.AutoSize = true; - this.label1.Location = new System.Drawing.Point(12, 23); + this.label1.Location = new System.Drawing.Point(12, 9); this.label1.Name = "label1"; this.label1.Size = new System.Drawing.Size(51, 13); this.label1.TabIndex = 1; @@ -83,20 +89,85 @@ namespace ntfysh_client // // topicId // - this.topicId.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + this.topicId.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); - this.topicId.Location = new System.Drawing.Point(15, 39); + this.topicId.Location = new System.Drawing.Point(12, 25); this.topicId.Name = "topicId"; this.topicId.Size = new System.Drawing.Size(273, 20); this.topicId.TabIndex = 0; this.topicId.KeyDown += new System.Windows.Forms.KeyEventHandler(this.topicId_KeyDown); // + // serverUrl + // + this.serverUrl.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.serverUrl.Location = new System.Drawing.Point(12, 64); + this.serverUrl.Name = "serverUrl"; + this.serverUrl.Size = new System.Drawing.Size(273, 20); + this.serverUrl.TabIndex = 2; + this.serverUrl.Text = "https://ntfy.sh"; + this.serverUrl.KeyDown += new System.Windows.Forms.KeyEventHandler(this.serverUrl_KeyDown); + // + // label2 + // + this.label2.AutoSize = true; + this.label2.Location = new System.Drawing.Point(10, 48); + this.label2.Name = "label2"; + this.label2.Size = new System.Drawing.Size(66, 13); + this.label2.TabIndex = 3; + this.label2.Text = "Server URL:"; + // + // username + // + this.username.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.username.Location = new System.Drawing.Point(12, 103); + this.username.Name = "username"; + this.username.Size = new System.Drawing.Size(273, 20); + this.username.TabIndex = 4; + this.username.KeyDown += new System.Windows.Forms.KeyEventHandler(this.username_KeyDown); + // + // label3 + // + this.label3.AutoSize = true; + this.label3.Location = new System.Drawing.Point(10, 87); + this.label3.Name = "label3"; + this.label3.Size = new System.Drawing.Size(58, 13); + this.label3.TabIndex = 5; + this.label3.Text = "Username:"; + // + // password + // + this.password.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.password.Location = new System.Drawing.Point(12, 142); + this.password.Name = "password"; + this.password.Size = new System.Drawing.Size(273, 20); + this.password.TabIndex = 6; + this.password.UseSystemPasswordChar = true; + this.password.KeyDown += new System.Windows.Forms.KeyEventHandler(this.password_KeyDown); + // + // label4 + // + this.label4.AutoSize = true; + this.label4.Location = new System.Drawing.Point(10, 126); + this.label4.Name = "label4"; + this.label4.Size = new System.Drawing.Size(56, 13); + this.label4.TabIndex = 7; + this.label4.Text = "Password:"; + // // SubscribeDialog // this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.BackColor = System.Drawing.Color.White; - this.ClientSize = new System.Drawing.Size(297, 125); + this.ClientSize = new System.Drawing.Size(297, 219); + this.Controls.Add(this.password); + this.Controls.Add(this.label4); + this.Controls.Add(this.username); + this.Controls.Add(this.label3); + this.Controls.Add(this.serverUrl); + this.Controls.Add(this.label2); this.Controls.Add(this.topicId); this.Controls.Add(this.label1); this.Controls.Add(this.panel1); @@ -114,6 +185,9 @@ namespace ntfysh_client } + private System.Windows.Forms.TextBox serverUrl; + private System.Windows.Forms.Label label2; + #endregion private System.Windows.Forms.Panel panel1; @@ -121,5 +195,9 @@ namespace ntfysh_client private System.Windows.Forms.Button button1; private System.Windows.Forms.Label label1; private System.Windows.Forms.TextBox topicId; + private System.Windows.Forms.TextBox username; + private System.Windows.Forms.Label label3; + private System.Windows.Forms.TextBox password; + private System.Windows.Forms.Label label4; } } \ No newline at end of file diff --git a/ntfysh_client/SubscribeDialog.cs b/ntfysh_client/SubscribeDialog.cs index 7e4ded3..861f3fe 100644 --- a/ntfysh_client/SubscribeDialog.cs +++ b/ntfysh_client/SubscribeDialog.cs @@ -1,11 +1,4 @@ using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Data; -using System.Drawing; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using System.Windows.Forms; namespace ntfysh_client @@ -21,6 +14,21 @@ namespace ntfysh_client { return topicId.Text; } + + public string getServerUrl() + { + return serverUrl.Text; + } + + public string getUsername() + { + return username.Text; + } + + public string getPassword() + { + return password.Text; + } private void button1_Click(object sender, EventArgs e) { @@ -32,6 +40,30 @@ namespace ntfysh_client return; } + if (serverUrl.Text.Length < 1) + { + MessageBox.Show("You must specify a server URL. The default is https://ntfy.sh", "Server URL not specified", MessageBoxButtons.OK, MessageBoxIcon.Error); + DialogResult = DialogResult.None; + serverUrl.Focus(); + return; + } + + if (username.Text.Length > 0 && password.Text.Length < 1) + { + MessageBox.Show("You must specify a password alongside the username", "Password not specified", MessageBoxButtons.OK, MessageBoxIcon.Error); + DialogResult = DialogResult.None; + password.Focus(); + return; + } + + if (password.Text.Length > 0 && username.Text.Length < 1) + { + MessageBox.Show("You must specify a username alongside the password", "Username not specified", MessageBoxButtons.OK, MessageBoxIcon.Error); + DialogResult = DialogResult.None; + username.Focus(); + return; + } + DialogResult = DialogResult.OK; } @@ -48,5 +80,32 @@ namespace ntfysh_client e.SuppressKeyPress = true; } } + + private void serverUrl_KeyDown(object sender, KeyEventArgs e) + { + if (e.KeyData == Keys.Enter) + { + button1.PerformClick(); + e.SuppressKeyPress = true; + } + } + + private void username_KeyDown(object sender, KeyEventArgs e) + { + if (e.KeyData == Keys.Enter) + { + button1.PerformClick(); + e.SuppressKeyPress = true; + } + } + + private void password_KeyDown(object sender, KeyEventArgs e) + { + if (e.KeyData == Keys.Enter) + { + button1.PerformClick(); + e.SuppressKeyPress = true; + } + } } } diff --git a/ntfysh_client/SubscribedTopic.cs b/ntfysh_client/SubscribedTopic.cs new file mode 100644 index 0000000..5d51bef --- /dev/null +++ b/ntfysh_client/SubscribedTopic.cs @@ -0,0 +1,23 @@ +using System.IO; + +namespace ntfysh_client +{ + public class SubscribedTopic + { + public SubscribedTopic(string topicId, string serverUrl, string username, string password, StreamReader stream) + { + TopicId = topicId; + ServerUrl = serverUrl; + Username = username; + Password = password; + Stream = stream; + } + + public string TopicId { get; } + public string ServerUrl { get; } + public string Username { get; } + public string Password { get; } + + public StreamReader Stream { get; } + } +} \ No newline at end of file diff --git a/ntfysh_client/ntfysh_client.csproj b/ntfysh_client/ntfysh_client.csproj index b6e42a2..ad03786 100644 --- a/ntfysh_client/ntfysh_client.csproj +++ b/ntfysh_client/ntfysh_client.csproj @@ -74,6 +74,7 @@ SubscribeDialog.cs + AboutBox.cs From 6aa48fdd2f6b45668af9e99cbeb99d889ae4eb76 Mon Sep 17 00:00:00 2001 From: Alexander Horner <33007665+alexhorner@users.noreply.github.com> Date: Tue, 6 Dec 2022 19:59:32 +0000 Subject: [PATCH 02/14] Start some really basic persistence support. Buggy because of async missing --- ntfysh_client/Form1.cs | 82 ++++++++++++++++++++++++++------ ntfysh_client/SubscribedTopic.cs | 2 + 2 files changed, 69 insertions(+), 15 deletions(-) diff --git a/ntfysh_client/Form1.cs b/ntfysh_client/Form1.cs index e3e441c..cd451c5 100644 --- a/ntfysh_client/Form1.cs +++ b/ntfysh_client/Form1.cs @@ -9,6 +9,7 @@ using System.Reflection; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; +using Newtonsoft.Json; namespace ntfysh_client { @@ -94,32 +95,83 @@ namespace ntfysh_client private string GetTopicsFilePath() { - string binaryDirectory = System.IO.Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); - return Path.Combine(binaryDirectory, "topics.txt"); + string binaryDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + return Path.Combine(binaryDirectory ?? throw new InvalidOperationException("Unable to determine path for topics file"), "topics.json"); + } + + private string GetLegacyTopicsFilePath() + { + string binaryDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + return Path.Combine(binaryDirectory ?? throw new InvalidOperationException("Unable to determine path for legacy topics file"), "topics.txt"); } private void SaveTopicsToFile() { - using (StreamWriter writer = new StreamWriter(GetTopicsFilePath())) - { - foreach (string topic in notificationTopics.Items) - { - writer.WriteLine(topic); - } - } + string topicsSerialised = JsonConvert.SerializeObject(notificationListener.SubscribedTopics.Select(st => st.Value).ToList(), Formatting.Indented); + + File.WriteAllText(GetTopicsFilePath(), topicsSerialised); } private void LoadTopics() { - if (!File.Exists(GetTopicsFilePath())) return; - using (StreamReader reader = new StreamReader(GetTopicsFilePath())) + string legacyTopicsPath = GetLegacyTopicsFilePath(); + string topicsFilePath = GetTopicsFilePath(); + + //If we have an old format topics file. Convert it to the new format! + if (File.Exists(legacyTopicsPath)) { - while (!reader.EndOfStream) + //Read old format + List legacyTopics = new List(); + + using (StreamReader reader = new StreamReader(legacyTopicsPath)) { - var topic = reader.ReadLine(); - notificationListener.SubscribeToTopic(topic, "https://ntfy.sh", null, null); - notificationTopics.Items.Add(topic); + while (!reader.EndOfStream) + { + string legacyTopic = reader.ReadLine(); + legacyTopics.Add(legacyTopic); + } } + + //Assemble new format + List newTopics = legacyTopics.Select(lt => new SubscribedTopic(lt, "https://ntfy.sh", null, null, null)).ToList(); + + string newFormatSerialised = JsonConvert.SerializeObject(newTopics, Formatting.Indented); + + //Write new format + File.WriteAllText(topicsFilePath, newFormatSerialised); + + //Delete old format + File.Delete(legacyTopicsPath); + } + + //Check if we have any topics file on disk to load + if (!File.Exists(topicsFilePath)) return; + + //We have a topics file. Load it! + string topicsSerialised = File.ReadAllText(topicsFilePath); + + //Check if the file is empty + if (string.IsNullOrWhiteSpace(topicsSerialised)) + { + //The file is empty. May as well remove it and consider it nonexistent + File.Delete(topicsFilePath); + return; + } + + //Deserialise the topics + List topics = JsonConvert.DeserializeObject>(topicsSerialised); + + if (topics == null) + { + //TODO Deserialise error! + return; + } + + //Load them in + foreach (SubscribedTopic topic in topics) + { + notificationListener.SubscribeToTopic(topic.TopicId, topic.ServerUrl, topic.Username, topic.Password); + notificationTopics.Items.Add(topic.TopicId); } } diff --git a/ntfysh_client/SubscribedTopic.cs b/ntfysh_client/SubscribedTopic.cs index 5d51bef..184f768 100644 --- a/ntfysh_client/SubscribedTopic.cs +++ b/ntfysh_client/SubscribedTopic.cs @@ -1,4 +1,5 @@ using System.IO; +using Newtonsoft.Json; namespace ntfysh_client { @@ -18,6 +19,7 @@ namespace ntfysh_client public string Username { get; } public string Password { get; } + [JsonIgnore] public StreamReader Stream { get; } } } \ No newline at end of file From 5a4dfc01b63a1cfcccc2d8a34835c7a4e1cc4aa6 Mon Sep 17 00:00:00 2001 From: Alexander Horner <33007665+alexhorner@users.noreply.github.com> Date: Tue, 6 Dec 2022 20:14:45 +0000 Subject: [PATCH 03/14] Fix topic uniqueness issue --- ntfysh_client/Form1.cs | 19 ++++++++++--------- ntfysh_client/NotificationListener.cs | 21 +++++++++++---------- ntfysh_client/SubscribeDialog.cs | 18 +++++++++++++++++- 3 files changed, 38 insertions(+), 20 deletions(-) diff --git a/ntfysh_client/Form1.cs b/ntfysh_client/Form1.cs index cd451c5..6c3b9e1 100644 --- a/ntfysh_client/Form1.cs +++ b/ntfysh_client/Form1.cs @@ -31,14 +31,14 @@ namespace ntfysh_client private void subscribeNewTopic_Click(object sender, EventArgs e) { - using (var dialog = new SubscribeDialog()) + using (var dialog = new SubscribeDialog(notificationTopics)) { var result = dialog.ShowDialog(); if (result == DialogResult.OK) { - notificationListener.SubscribeToTopic(dialog.getTopicId(), dialog.getServerUrl(), dialog.getUsername(), dialog.getPassword()); - notificationTopics.Items.Add(dialog.getTopicId()); + notificationListener.SubscribeToTopic(dialog.getUniqueString(), dialog.getTopicId(), dialog.getServerUrl(), dialog.getUsername(), dialog.getPassword()); + notificationTopics.Items.Add(dialog.getUniqueString()); this.SaveTopicsToFile(); } } @@ -48,9 +48,10 @@ namespace ntfysh_client { while (notificationTopics.SelectedIndex > -1) { - var topicId = notificationTopics.Items[notificationTopics.SelectedIndex]; - notificationListener.RemoveTopic((string)topicId); - notificationTopics.Items.RemoveAt(notificationTopics.SelectedIndex); + string topicUniqueString = (string)notificationTopics.Items[notificationTopics.SelectedIndex]; + + notificationListener.RemoveTopicByUniqueString(topicUniqueString); + notificationTopics.Items.Remove(topicUniqueString); } this.SaveTopicsToFile(); @@ -107,7 +108,7 @@ namespace ntfysh_client private void SaveTopicsToFile() { - string topicsSerialised = JsonConvert.SerializeObject(notificationListener.SubscribedTopics.Select(st => st.Value).ToList(), Formatting.Indented); + string topicsSerialised = JsonConvert.SerializeObject(notificationListener.SubscribedTopicsByUnique.Select(st => st.Value).ToList(), Formatting.Indented); File.WriteAllText(GetTopicsFilePath(), topicsSerialised); } @@ -170,8 +171,8 @@ namespace ntfysh_client //Load them in foreach (SubscribedTopic topic in topics) { - notificationListener.SubscribeToTopic(topic.TopicId, topic.ServerUrl, topic.Username, topic.Password); - notificationTopics.Items.Add(topic.TopicId); + notificationListener.SubscribeToTopic($"{topic.TopicId}@{topic.ServerUrl}", topic.TopicId, topic.ServerUrl, topic.Username, topic.Password); + notificationTopics.Items.Add($"{topic.TopicId}@{topic.ServerUrl}"); } } diff --git a/ntfysh_client/NotificationListener.cs b/ntfysh_client/NotificationListener.cs index 54e5bf1..a7057ca 100644 --- a/ntfysh_client/NotificationListener.cs +++ b/ntfysh_client/NotificationListener.cs @@ -20,7 +20,7 @@ namespace ntfysh_client private bool disposedValue; - public readonly Dictionary SubscribedTopics = new Dictionary(); + public readonly Dictionary SubscribedTopicsByUnique = new Dictionary(); public delegate void NotificationReceiveHandler(object sender, NotificationReceiveEventArgs e); public event NotificationReceiveHandler OnNotificationReceive; @@ -33,7 +33,7 @@ namespace ntfysh_client ServicePointManager.DefaultConnectionLimit = 100; } - public async Task SubscribeToTopic(string topicId, string serverUrl, string username, string password) + public async Task SubscribeToTopic(string unique, string topicId, string serverUrl, string username, string password) { if (string.IsNullOrWhiteSpace(username)) username = null; if (string.IsNullOrWhiteSpace(password)) password = null; @@ -53,7 +53,7 @@ namespace ntfysh_client { using (StreamReader reader = new StreamReader(body)) { - SubscribedTopics.Add(topicId, new SubscribedTopic(topicId, serverUrl, username, password, reader)); + SubscribedTopicsByUnique.Add(unique, new SubscribedTopic(topicId, serverUrl, username, password, reader)); try { @@ -82,9 +82,9 @@ namespace ntfysh_client // If the topic is still registered, then that stream wasn't mean to be closed (maybe network failure?) // Restart it - if (SubscribedTopics.ContainsKey(topicId)) + if (SubscribedTopicsByUnique.ContainsKey(unique)) { - SubscribeToTopic(topicId, serverUrl, username, password); + SubscribeToTopic(unique, topicId, serverUrl, username, password); } } } @@ -92,16 +92,17 @@ namespace ntfysh_client } } - public void RemoveTopic(string topicId) + public void RemoveTopicByUniqueString(string topicUniqueString) { - Debug.WriteLine($"Removing topic {topicId}"); + Debug.WriteLine($"Removing topic {topicUniqueString}"); - if (SubscribedTopics.ContainsKey(topicId)) + if (SubscribedTopicsByUnique.ContainsKey(topicUniqueString)) { // Not moronic to store it in a variable; this solves a race condition in SubscribeToTopic - var topic = SubscribedTopics[topicId]; - SubscribedTopics.Remove(topicId); + SubscribedTopic topic = SubscribedTopicsByUnique[topicUniqueString]; topic.Stream.Close(); + + SubscribedTopicsByUnique.Remove(topicUniqueString); } } diff --git a/ntfysh_client/SubscribeDialog.cs b/ntfysh_client/SubscribeDialog.cs index 861f3fe..f5c697e 100644 --- a/ntfysh_client/SubscribeDialog.cs +++ b/ntfysh_client/SubscribeDialog.cs @@ -5,8 +5,11 @@ namespace ntfysh_client { public partial class SubscribeDialog : Form { - public SubscribeDialog() + private readonly ListBox _notificationTopics; + + public SubscribeDialog(ListBox notificationTopics) { + _notificationTopics = notificationTopics; InitializeComponent(); } @@ -30,6 +33,11 @@ namespace ntfysh_client return password.Text; } + public string getUniqueString() + { + return $"{topicId.Text}@{serverUrl.Text}"; + } + private void button1_Click(object sender, EventArgs e) { if (topicId.Text.Length < 1) @@ -64,6 +72,14 @@ namespace ntfysh_client return; } + if (_notificationTopics.Items.Contains(getUniqueString())) + { + MessageBox.Show($"The specified topic '{topicId.Text}' on the server '{serverUrl.Text}' is already subscribed", "Topic already subscribed", MessageBoxButtons.OK, MessageBoxIcon.Error); + DialogResult = DialogResult.None; + username.Focus(); + return; + } + DialogResult = DialogResult.OK; } From ed5a43a0e8ac014f0df8dae8d4e040325c326b52 Mon Sep 17 00:00:00 2001 From: Alexander Horner <33007665+alexhorner@users.noreply.github.com> Date: Tue, 6 Dec 2022 22:34:32 +0000 Subject: [PATCH 04/14] Complete refactor, reasync, rebuild listener --- ntfysh_client/Form1.cs | 108 ++++----- ntfysh_client/NotificationListener.cs | 229 +++++++++--------- ntfysh_client/NotificationReceiveEventArgs.cs | 16 ++ ntfysh_client/NtfyEvent.cs | 25 ++ ntfysh_client/Program.cs | 4 +- ntfysh_client/SubscribeDialog.cs | 37 +-- ntfysh_client/SubscribedTopic.cs | 17 +- ntfysh_client/ntfysh_client.csproj | 4 + 8 files changed, 234 insertions(+), 206 deletions(-) create mode 100644 ntfysh_client/NotificationReceiveEventArgs.cs create mode 100644 ntfysh_client/NtfyEvent.cs diff --git a/ntfysh_client/Form1.cs b/ntfysh_client/Form1.cs index 6c3b9e1..5666191 100644 --- a/ntfysh_client/Form1.cs +++ b/ntfysh_client/Form1.cs @@ -15,33 +15,35 @@ namespace ntfysh_client { public partial class Form1 : Form { - private NotificationListener notificationListener; + private readonly NotificationListener _notificationListener; + private bool _trueExit; - public Form1() + public Form1(NotificationListener notificationListener) { - notificationListener = new NotificationListener(); - notificationListener.OnNotificationReceive += OnNotificationReceive; + _notificationListener = notificationListener; + _notificationListener.OnNotificationReceive += OnNotificationReceive; + InitializeComponent(); } - private void Form1_Load(object sender, EventArgs e) - { - this.LoadTopics(); - } + private void Form1_Load(object sender, EventArgs e) => LoadTopics(); private void subscribeNewTopic_Click(object sender, EventArgs e) { - using (var dialog = new SubscribeDialog(notificationTopics)) - { - var result = dialog.ShowDialog(); + using SubscribeDialog dialog = new SubscribeDialog(notificationTopics); + DialogResult result = dialog.ShowDialog(); - if (result == DialogResult.OK) - { - notificationListener.SubscribeToTopic(dialog.getUniqueString(), dialog.getTopicId(), dialog.getServerUrl(), dialog.getUsername(), dialog.getPassword()); - notificationTopics.Items.Add(dialog.getUniqueString()); - this.SaveTopicsToFile(); - } - } + //Do not subscribe on cancelled dialog + if (result != DialogResult.OK) return; + + //Subscribe + _notificationListener.SubscribeToTopicUsingLongHttpJson(dialog.Unique, dialog.TopicId, dialog.ServerUrl, dialog.Username, dialog.Password); + + //Add to the user visible list + notificationTopics.Items.Add(dialog.Unique); + + //Save the topics persistently + SaveTopicsToFile(); } private void removeSelectedTopics_Click(object sender, EventArgs e) @@ -50,11 +52,11 @@ namespace ntfysh_client { string topicUniqueString = (string)notificationTopics.Items[notificationTopics.SelectedIndex]; - notificationListener.RemoveTopicByUniqueString(topicUniqueString); + _notificationListener.UnsubscribeFromTopic(topicUniqueString); notificationTopics.Items.Remove(topicUniqueString); } - this.SaveTopicsToFile(); + SaveTopicsToFile(); } private void notificationTopics_SelectedValueChanged(object sender, EventArgs e) @@ -64,51 +66,48 @@ namespace ntfysh_client private void notificationTopics_Click(object sender, EventArgs e) { - var ev = (MouseEventArgs)e; - var clickedItemIndex = notificationTopics.IndexFromPoint(new Point(ev.X, ev.Y)); + MouseEventArgs ev = (MouseEventArgs)e; + int clickedItemIndex = notificationTopics.IndexFromPoint(new Point(ev.X, ev.Y)); - if (clickedItemIndex == -1) - { - notificationTopics.ClearSelected(); - } + if (clickedItemIndex == -1) notificationTopics.ClearSelected(); } private void button1_Click(object sender, EventArgs e) { - this.Visible = false; + Visible = false; } private void notifyIcon_Click(object sender, EventArgs e) { - var mouseEv = (MouseEventArgs)e; - if (mouseEv.Button == MouseButtons.Left) - { - this.Visible = !this.Visible; - this.BringToFront(); - } + MouseEventArgs mouseEv = (MouseEventArgs)e; + + if (mouseEv.Button != MouseButtons.Left) return; + + Visible = !Visible; + BringToFront(); } private void showControlWindowToolStripMenuItem_Click(object sender, EventArgs e) { - this.Visible = true; - this.BringToFront(); + Visible = true; + BringToFront(); } private string GetTopicsFilePath() { - string binaryDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + string binaryDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? throw new InvalidOperationException("Unable to determine path for application"); return Path.Combine(binaryDirectory ?? throw new InvalidOperationException("Unable to determine path for topics file"), "topics.json"); } private string GetLegacyTopicsFilePath() { - string binaryDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + string binaryDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? throw new InvalidOperationException("Unable to determine path for application"); return Path.Combine(binaryDirectory ?? throw new InvalidOperationException("Unable to determine path for legacy topics file"), "topics.txt"); } private void SaveTopicsToFile() { - string topicsSerialised = JsonConvert.SerializeObject(notificationListener.SubscribedTopicsByUnique.Select(st => st.Value).ToList(), Formatting.Indented); + string topicsSerialised = JsonConvert.SerializeObject(_notificationListener.SubscribedTopicsByUnique.Select(st => st.Value).ToList(), Formatting.Indented); File.WriteAllText(GetTopicsFilePath(), topicsSerialised); } @@ -128,13 +127,13 @@ namespace ntfysh_client { while (!reader.EndOfStream) { - string legacyTopic = reader.ReadLine(); - legacyTopics.Add(legacyTopic); + string? legacyTopic = reader.ReadLine(); + if (!string.IsNullOrWhiteSpace(legacyTopic)) legacyTopics.Add(legacyTopic); } } //Assemble new format - List newTopics = legacyTopics.Select(lt => new SubscribedTopic(lt, "https://ntfy.sh", null, null, null)).ToList(); + List newTopics = legacyTopics.Select(lt => new SubscribedTopic(lt, "https://ntfy.sh", null, null, null, null)).ToList(); string newFormatSerialised = JsonConvert.SerializeObject(newTopics, Formatting.Indented); @@ -160,9 +159,9 @@ namespace ntfysh_client } //Deserialise the topics - List topics = JsonConvert.DeserializeObject>(topicsSerialised); + List? topics = JsonConvert.DeserializeObject>(topicsSerialised); - if (topics == null) + if (topics is null) { //TODO Deserialise error! return; @@ -171,7 +170,7 @@ namespace ntfysh_client //Load them in foreach (SubscribedTopic topic in topics) { - notificationListener.SubscribeToTopic($"{topic.TopicId}@{topic.ServerUrl}", topic.TopicId, topic.ServerUrl, topic.Username, topic.Password); + _notificationListener.SubscribeToTopicUsingLongHttpJson($"{topic.TopicId}@{topic.ServerUrl}", topic.TopicId, topic.ServerUrl, topic.Username, topic.Password); notificationTopics.Items.Add($"{topic.TopicId}@{topic.ServerUrl}"); } } @@ -185,24 +184,22 @@ namespace ntfysh_client { notifyIcon.Dispose(); } - - private bool trueExit = false; + private void Form1_FormClosing(object sender, FormClosingEventArgs e) { // Let it close - if (trueExit) return; + if (_trueExit) return; - if (e.CloseReason == CloseReason.UserClosing) - { - this.Visible = false; - e.Cancel = true; - } + if (e.CloseReason != CloseReason.UserClosing) return; + + Visible = false; + e.Cancel = true; } private void exitToolStripMenuItem_Click(object sender, EventArgs e) { - trueExit = true; - this.Close(); + _trueExit = true; + Close(); } private void ntfyshWebsiteToolStripMenuItem_Click(object sender, EventArgs e) @@ -212,9 +209,8 @@ namespace ntfysh_client private void aboutToolStripMenuItem_Click(object sender, EventArgs e) { - var d = new AboutBox(); + using AboutBox d = new AboutBox(); d.ShowDialog(); - d.Dispose(); } } } diff --git a/ntfysh_client/NotificationListener.cs b/ntfysh_client/NotificationListener.cs index a7057ca..408c274 100644 --- a/ntfysh_client/NotificationListener.cs +++ b/ntfysh_client/NotificationListener.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; +using System.Net.WebSockets; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -14,153 +15,147 @@ using System.Web; namespace ntfysh_client { - class NotificationListener : IDisposable + public class NotificationListener : IDisposable { - private HttpClient httpClient; - - private bool disposedValue; + private readonly HttpClient _httpClient = new(); + private bool _isDisposed; - public readonly Dictionary SubscribedTopicsByUnique = new Dictionary(); + public readonly Dictionary SubscribedTopicsByUnique = new(); public delegate void NotificationReceiveHandler(object sender, NotificationReceiveEventArgs e); - public event NotificationReceiveHandler OnNotificationReceive; + public event NotificationReceiveHandler? OnNotificationReceive; public NotificationListener() { - httpClient = new HttpClient(); - - httpClient.Timeout = TimeSpan.FromMilliseconds(Timeout.Infinite); + _httpClient.Timeout = TimeSpan.FromMilliseconds(Timeout.Infinite); ServicePointManager.DefaultConnectionLimit = 100; } - public async Task SubscribeToTopic(string unique, string topicId, string serverUrl, string username, string password) + private async Task ListenToTopicAsync(HttpRequestMessage message, CancellationToken cancellationToken) { + if (_isDisposed) throw new ObjectDisposedException(nameof(NotificationListener)); + + while (!cancellationToken.IsCancellationRequested) + { + using HttpResponseMessage response = await _httpClient.SendAsync(message, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + using Stream body = await response.Content.ReadAsStreamAsync(); + + try + { + StringBuilder mainBuffer = new(); + + while (!cancellationToken.IsCancellationRequested) + { + //Read as much as possible + byte[] buffer = new byte[8192]; + int readBytes = await body.ReadAsync(buffer, 0, buffer.Length, cancellationToken); + + //Append it to our main buffer + mainBuffer.Append(Encoding.UTF8.GetString(buffer, 0, readBytes)); + + List lines = mainBuffer.ToString().Split('\n').ToList(); + + //If we have not yet received a full line, meaning theres only 1 part, go back to reading + if (lines.Count <= 1) continue; + + //We now have at least 1 line! Count how many full lines. There will always be a partial line at the end, even if that partial line is empty + + //Separate the partial line from the full lines + int partialLineIndex = lines.Count - 1; + string partialLine = lines[partialLineIndex]; + lines.RemoveAt(partialLineIndex); + + //Process the full lines + foreach (string line in lines) ProcessMessage(line); + + //Write back the partial line + mainBuffer.Clear(); + mainBuffer.Append(partialLine); + } + } + catch (Exception ex) + { + #if DEBUG + Debug.WriteLine(ex); + #endif + + //Fall back to the outer loop to restart the listen, or cancel if requested + } + } + } + + private void ProcessMessage(string message) + { + #if DEBUG + Debug.WriteLine(message); + #endif + + NtfyEvent? evt = JsonConvert.DeserializeObject(message); + + //If we hit this, ntfy sent us an invalid message + if (evt is null) return; + + if (evt.Event == "message") + { + OnNotificationReceive?.Invoke(this, new NotificationReceiveEventArgs(evt.Title, evt.Message)); + } + } + + public void SubscribeToTopicUsingLongHttpJson(string unique, string topicId, string serverUrl, string? username, string? password) + { + if (_isDisposed) throw new ObjectDisposedException(nameof(NotificationListener)); + + if (SubscribedTopicsByUnique.ContainsKey(unique)) throw new InvalidOperationException("A topic with this unique already exists"); + if (string.IsNullOrWhiteSpace(username)) username = null; if (string.IsNullOrWhiteSpace(password)) password = null; - HttpRequestMessage msg = new HttpRequestMessage(HttpMethod.Get, $"{serverUrl}/{HttpUtility.UrlEncode(topicId)}/json"); + HttpRequestMessage message = new HttpRequestMessage(HttpMethod.Get, $"{serverUrl}/{HttpUtility.UrlEncode(topicId)}/json"); if (username != null && password != null) { byte[] boundCredentialsBytes = Encoding.UTF8.GetBytes($"{username}:{password}"); - msg.Headers.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(boundCredentialsBytes)); + message.Headers.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(boundCredentialsBytes)); } - using (HttpResponseMessage response = await httpClient.SendAsync(msg, HttpCompletionOption.ResponseHeadersRead)) - { - using (Stream body = await response.Content.ReadAsStreamAsync()) - { - using (StreamReader reader = new StreamReader(body)) - { - SubscribedTopicsByUnique.Add(unique, new SubscribedTopic(topicId, serverUrl, username, password, reader)); + CancellationTokenSource listenCanceller = new(); + Task listenTask = ListenToTopicAsync(message, listenCanceller.Token); - try - { - // The loop will be broken when this stream is closed - while (true) - { - var line = await reader.ReadLineAsync(); - - Debug.WriteLine(line); - - NtfyEventObject nev = JsonConvert.DeserializeObject(line); - - if (nev.Event == "message") - { - if (OnNotificationReceive != null) - { - var evArgs = new NotificationReceiveEventArgs(nev.Title, nev.Message); - OnNotificationReceive(this, evArgs); - } - } - } - } - catch (Exception ex) - { - Debug.WriteLine(ex); - - // If the topic is still registered, then that stream wasn't mean to be closed (maybe network failure?) - // Restart it - if (SubscribedTopicsByUnique.ContainsKey(unique)) - { - SubscribeToTopic(unique, topicId, serverUrl, username, password); - } - } - } - } - } + SubscribedTopicsByUnique.Add(unique, new SubscribedTopic(topicId, serverUrl, username, password, listenTask, listenCanceller)); } - public void RemoveTopicByUniqueString(string topicUniqueString) + public void UnsubscribeFromTopic(string topicUniqueString) { - Debug.WriteLine($"Removing topic {topicUniqueString}"); - - if (SubscribedTopicsByUnique.ContainsKey(topicUniqueString)) - { - // Not moronic to store it in a variable; this solves a race condition in SubscribeToTopic - SubscribedTopic topic = SubscribedTopicsByUnique[topicUniqueString]; - topic.Stream.Close(); + if (_isDisposed) throw new ObjectDisposedException(nameof(NotificationListener)); - SubscribedTopicsByUnique.Remove(topicUniqueString); - } + #if DEBUG + Debug.WriteLine($"Removing topic {topicUniqueString}"); + #endif + + //Topic isn't even subscribed, ignore + if (!SubscribedTopicsByUnique.TryGetValue(topicUniqueString, out SubscribedTopic topic)) return; + + //Cancel and dispose the task runner + topic.RunnerCanceller.Cancel(); + + //Wait for the task runner to shut down + while (!topic.Runner.IsCompleted) Thread.Sleep(100); + + //Dispose task + topic.Runner.Dispose(); + + //Remove the old topic + SubscribedTopicsByUnique.Remove(topicUniqueString); } - protected virtual void Dispose(bool disposing) - { - if (!disposedValue) - { - if (disposing) - { - // TODO: dispose managed state (managed objects) - } - - // TODO: free unmanaged resources (unmanaged objects) and override finalizer - // TODO: set large fields to null - disposedValue = true; - } - } - - // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources - // ~NotificationListener() - // { - // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - // Dispose(disposing: false); - // } - public void Dispose() { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); - GC.SuppressFinalize(this); + if (_isDisposed) return; + + _httpClient.Dispose(); + + _isDisposed = true; } } - - public class NotificationReceiveEventArgs : EventArgs - { - public string Title { get; private set; } - public string Message { get; private set; } - - public NotificationReceiveEventArgs(string title, string message) - { - Title = title; - Message = message; - } - } - - public class NtfyEventObject - { - [JsonProperty("id")] - public string Id { get; set; } - [JsonProperty("time")] - public Int64 Time { get; set; } - [JsonProperty("event")] - public string Event { get; set; } - [JsonProperty("topic")] - public string Topic { get; set; } - [JsonProperty("message")] - public string Message { get; set; } - [JsonProperty("title")] - public string Title { get; set; } - } } diff --git a/ntfysh_client/NotificationReceiveEventArgs.cs b/ntfysh_client/NotificationReceiveEventArgs.cs new file mode 100644 index 0000000..87d932e --- /dev/null +++ b/ntfysh_client/NotificationReceiveEventArgs.cs @@ -0,0 +1,16 @@ +using System; + +namespace ntfysh_client +{ + public class NotificationReceiveEventArgs : EventArgs + { + public string Title { get; } + public string Message { get; } + + public NotificationReceiveEventArgs(string title, string message) + { + Title = title; + Message = message; + } + } +} \ No newline at end of file diff --git a/ntfysh_client/NtfyEvent.cs b/ntfysh_client/NtfyEvent.cs new file mode 100644 index 0000000..ef4ee82 --- /dev/null +++ b/ntfysh_client/NtfyEvent.cs @@ -0,0 +1,25 @@ +using Newtonsoft.Json; + +namespace ntfysh_client +{ + public class NtfyEvent + { + [JsonProperty("id")] + public string Id { get; set; } = null!; + + [JsonProperty("time")] + public long Time { get; set; } + + [JsonProperty("event")] + public string Event { get; set; } = null!; + + [JsonProperty("topic")] + public string Topic { get; set; } = null!; + + [JsonProperty("message")] + public string Message { get; set; } = null!; + + [JsonProperty("title")] + public string Title { get; set; } = null!; + } +} \ No newline at end of file diff --git a/ntfysh_client/Program.cs b/ntfysh_client/Program.cs index d9332fa..a980f95 100644 --- a/ntfysh_client/Program.cs +++ b/ntfysh_client/Program.cs @@ -8,6 +8,8 @@ namespace ntfysh_client { static class Program { + private static readonly NotificationListener NotificationListener = new NotificationListener(); + /// /// The main entry point for the application. /// @@ -16,7 +18,7 @@ namespace ntfysh_client { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); - Application.Run(new Form1()); + Application.Run(new Form1(NotificationListener)); } } } diff --git a/ntfysh_client/SubscribeDialog.cs b/ntfysh_client/SubscribeDialog.cs index f5c697e..e5233f6 100644 --- a/ntfysh_client/SubscribeDialog.cs +++ b/ntfysh_client/SubscribeDialog.cs @@ -6,6 +6,16 @@ namespace ntfysh_client public partial class SubscribeDialog : Form { private readonly ListBox _notificationTopics; + + public string TopicId => topicId.Text; + + public string ServerUrl => serverUrl.Text; + + public string Username => username.Text; + + public string Password => password.Text; + + public string Unique => $"{topicId.Text}@{serverUrl.Text}"; public SubscribeDialog(ListBox notificationTopics) { @@ -13,31 +23,6 @@ namespace ntfysh_client InitializeComponent(); } - public string getTopicId() - { - return topicId.Text; - } - - public string getServerUrl() - { - return serverUrl.Text; - } - - public string getUsername() - { - return username.Text; - } - - public string getPassword() - { - return password.Text; - } - - public string getUniqueString() - { - return $"{topicId.Text}@{serverUrl.Text}"; - } - private void button1_Click(object sender, EventArgs e) { if (topicId.Text.Length < 1) @@ -72,7 +57,7 @@ namespace ntfysh_client return; } - if (_notificationTopics.Items.Contains(getUniqueString())) + if (_notificationTopics.Items.Contains(Unique)) { MessageBox.Show($"The specified topic '{topicId.Text}' on the server '{serverUrl.Text}' is already subscribed", "Topic already subscribed", MessageBoxButtons.OK, MessageBoxIcon.Error); DialogResult = DialogResult.None; diff --git a/ntfysh_client/SubscribedTopic.cs b/ntfysh_client/SubscribedTopic.cs index 184f768..f5b172b 100644 --- a/ntfysh_client/SubscribedTopic.cs +++ b/ntfysh_client/SubscribedTopic.cs @@ -1,25 +1,30 @@ -using System.IO; +using System.Threading; +using System.Threading.Tasks; using Newtonsoft.Json; namespace ntfysh_client { public class SubscribedTopic { - public SubscribedTopic(string topicId, string serverUrl, string username, string password, StreamReader stream) + public SubscribedTopic(string topicId, string serverUrl, string? username, string? password, Task runner, CancellationTokenSource runnerCanceller) { TopicId = topicId; ServerUrl = serverUrl; Username = username; Password = password; - Stream = stream; + Runner = runner; + RunnerCanceller = runnerCanceller; } public string TopicId { get; } public string ServerUrl { get; } - public string Username { get; } - public string Password { get; } + public string? Username { get; } + public string? Password { get; } [JsonIgnore] - public StreamReader Stream { get; } + public Task Runner { get; } + + [JsonIgnore] + public CancellationTokenSource RunnerCanceller { get; } } } \ No newline at end of file diff --git a/ntfysh_client/ntfysh_client.csproj b/ntfysh_client/ntfysh_client.csproj index ad03786..3708738 100644 --- a/ntfysh_client/ntfysh_client.csproj +++ b/ntfysh_client/ntfysh_client.csproj @@ -12,6 +12,8 @@ 512 true true + latest + enable AnyCPU @@ -66,6 +68,8 @@ Form1.cs + + From fde9a03887af8f7fd5facffb80a9f0caead0bdf7 Mon Sep 17 00:00:00 2001 From: Alexander Horner <33007665+alexhorner@users.noreply.github.com> Date: Tue, 6 Dec 2022 23:17:03 +0000 Subject: [PATCH 05/14] Upgrade to .NET Core 6 (Windows Only Target) and fix UI thread deadlock bug --- ntfysh_client/NotificationListener.cs | 16 +-- ntfysh_client/Program.cs | 1 + ntfysh_client/Properties/AssemblyInfo.cs | 36 ------ .../Properties/Resources.Designer.cs | 63 ---------- ntfysh_client/Properties/Resources.resx | 117 ------------------ ntfysh_client/Properties/Settings.Designer.cs | 26 ---- ntfysh_client/Properties/Settings.settings | 7 -- ntfysh_client/ntfysh_client.csproj | 117 ++---------------- 8 files changed, 18 insertions(+), 365 deletions(-) delete mode 100644 ntfysh_client/Properties/AssemblyInfo.cs delete mode 100644 ntfysh_client/Properties/Resources.Designer.cs delete mode 100644 ntfysh_client/Properties/Resources.resx delete mode 100644 ntfysh_client/Properties/Settings.Designer.cs delete mode 100644 ntfysh_client/Properties/Settings.settings diff --git a/ntfysh_client/NotificationListener.cs b/ntfysh_client/NotificationListener.cs index 408c274..385e244 100644 --- a/ntfysh_client/NotificationListener.cs +++ b/ntfysh_client/NotificationListener.cs @@ -20,7 +20,7 @@ namespace ntfysh_client private readonly HttpClient _httpClient = new(); private bool _isDisposed; - public readonly Dictionary SubscribedTopicsByUnique = new(); + public readonly Dictionary SubscribedTopicsByUnique = new(); public delegate void NotificationReceiveHandler(object sender, NotificationReceiveEventArgs e); public event NotificationReceiveHandler? OnNotificationReceive; @@ -37,8 +37,8 @@ namespace ntfysh_client while (!cancellationToken.IsCancellationRequested) { - using HttpResponseMessage response = await _httpClient.SendAsync(message, HttpCompletionOption.ResponseHeadersRead, cancellationToken); - using Stream body = await response.Content.ReadAsStreamAsync(); + using HttpResponseMessage response = await _httpClient.SendAsync(message, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + await using Stream body = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); try { @@ -48,7 +48,7 @@ namespace ntfysh_client { //Read as much as possible byte[] buffer = new byte[8192]; - int readBytes = await body.ReadAsync(buffer, 0, buffer.Length, cancellationToken); + int readBytes = await body.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false); //Append it to our main buffer mainBuffer.Append(Encoding.UTF8.GetString(buffer, 0, readBytes)); @@ -134,16 +134,16 @@ namespace ntfysh_client #endif //Topic isn't even subscribed, ignore - if (!SubscribedTopicsByUnique.TryGetValue(topicUniqueString, out SubscribedTopic topic)) return; + if (!SubscribedTopicsByUnique.TryGetValue(topicUniqueString, out SubscribedTopic? topic)) return; //Cancel and dispose the task runner - topic.RunnerCanceller.Cancel(); + topic?.RunnerCanceller.Cancel(); //Wait for the task runner to shut down - while (!topic.Runner.IsCompleted) Thread.Sleep(100); + while (topic is not null && !topic.Runner.IsCompleted) Thread.Sleep(100); //Dispose task - topic.Runner.Dispose(); + topic?.Runner.Dispose(); //Remove the old topic SubscribedTopicsByUnique.Remove(topicUniqueString); diff --git a/ntfysh_client/Program.cs b/ntfysh_client/Program.cs index a980f95..6f47948 100644 --- a/ntfysh_client/Program.cs +++ b/ntfysh_client/Program.cs @@ -16,6 +16,7 @@ namespace ntfysh_client [STAThread] static void Main() { + //Application.SetHighDpiMode(HighDpiMode.SystemAware); Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new Form1(NotificationListener)); diff --git a/ntfysh_client/Properties/AssemblyInfo.cs b/ntfysh_client/Properties/AssemblyInfo.cs deleted file mode 100644 index 07fb348..0000000 --- a/ntfysh_client/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("ntfy.sh")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("ntfy.sh")] -[assembly: AssemblyCopyright("Copyright © 2022")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("5a18d152-d620-43fe-b844-def30cfa50ef")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/ntfysh_client/Properties/Resources.Designer.cs b/ntfysh_client/Properties/Resources.Designer.cs deleted file mode 100644 index 8cf2c22..0000000 --- a/ntfysh_client/Properties/Resources.Designer.cs +++ /dev/null @@ -1,63 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace ntfysh_client.Properties { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Resources { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Resources() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("ntfysh_client.Properties.Resources", typeof(Resources).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - } -} diff --git a/ntfysh_client/Properties/Resources.resx b/ntfysh_client/Properties/Resources.resx deleted file mode 100644 index af7dbeb..0000000 --- a/ntfysh_client/Properties/Resources.resx +++ /dev/null @@ -1,117 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - \ No newline at end of file diff --git a/ntfysh_client/Properties/Settings.Designer.cs b/ntfysh_client/Properties/Settings.Designer.cs deleted file mode 100644 index 67b3c60..0000000 --- a/ntfysh_client/Properties/Settings.Designer.cs +++ /dev/null @@ -1,26 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace ntfysh_client.Properties { - - - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.10.0.0")] - internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { - - private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); - - public static Settings Default { - get { - return defaultInstance; - } - } - } -} diff --git a/ntfysh_client/Properties/Settings.settings b/ntfysh_client/Properties/Settings.settings deleted file mode 100644 index 3964565..0000000 --- a/ntfysh_client/Properties/Settings.settings +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/ntfysh_client/ntfysh_client.csproj b/ntfysh_client/ntfysh_client.csproj index 3708738..dfe4310 100644 --- a/ntfysh_client/ntfysh_client.csproj +++ b/ntfysh_client/ntfysh_client.csproj @@ -1,119 +1,20 @@ - - - + + - Debug - AnyCPU - {5A18D152-D620-43FE-B844-DEF30CFA50EF} WinExe - ntfysh_client - ntfysh - v4.7.2 - 512 - true - true + net6.0-windows + true latest enable - - - AnyCPU - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - AnyCPU - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - NotificationHub.ico + - - ..\packages\Newtonsoft.Json.13.0.1\lib\net45\Newtonsoft.Json.dll - - - - - - - - - - - - - + + - - Form - - - AboutBox.cs - - - Form - - - Form1.cs - - - - - - - - Form - - - SubscribeDialog.cs - - - - AboutBox.cs - - - Form1.cs - - - ResXFileCodeGenerator - Resources.Designer.cs - Designer - - - True - Resources.resx - True - - - SubscribeDialog.cs - - - - SettingsSingleFileGenerator - Settings.Designer.cs - - - True - Settings.settings - True - + - - - - - - - + \ No newline at end of file From 012eeaa65f217be22979b9298709cc4a0f594f28 Mon Sep 17 00:00:00 2001 From: Alexander Horner <33007665+alexhorner@users.noreply.github.com> Date: Tue, 6 Dec 2022 23:25:07 +0000 Subject: [PATCH 06/14] Enable .NET Core system aware DPI mode Fix Exit button in window menu Cleanup --- ntfysh_client/Form1.Designer.cs | 52 ++++++++++++++++-------- ntfysh_client/Form1.cs | 6 +++ ntfysh_client/Form1.resx | 72 +++------------------------------ ntfysh_client/Program.cs | 2 +- 4 files changed, 48 insertions(+), 84 deletions(-) diff --git a/ntfysh_client/Form1.Designer.cs b/ntfysh_client/Form1.Designer.cs index 91caa7e..89f66c2 100644 --- a/ntfysh_client/Form1.Designer.cs +++ b/ntfysh_client/Form1.Designer.cs @@ -53,9 +53,10 @@ namespace ntfysh_client // subscribeNewTopic // this.subscribeNewTopic.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); - this.subscribeNewTopic.Location = new System.Drawing.Point(236, 50); + this.subscribeNewTopic.Location = new System.Drawing.Point(275, 58); + this.subscribeNewTopic.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); this.subscribeNewTopic.Name = "subscribeNewTopic"; - this.subscribeNewTopic.Size = new System.Drawing.Size(105, 23); + this.subscribeNewTopic.Size = new System.Drawing.Size(122, 27); this.subscribeNewTopic.TabIndex = 2; this.subscribeNewTopic.Text = "Add"; this.subscribeNewTopic.UseVisualStyleBackColor = true; @@ -65,9 +66,10 @@ namespace ntfysh_client // this.removeSelectedTopics.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); this.removeSelectedTopics.Enabled = false; - this.removeSelectedTopics.Location = new System.Drawing.Point(236, 79); + this.removeSelectedTopics.Location = new System.Drawing.Point(275, 91); + this.removeSelectedTopics.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); this.removeSelectedTopics.Name = "removeSelectedTopics"; - this.removeSelectedTopics.Size = new System.Drawing.Size(105, 23); + this.removeSelectedTopics.Size = new System.Drawing.Size(122, 27); this.removeSelectedTopics.TabIndex = 0; this.removeSelectedTopics.Text = "Remove selected"; this.removeSelectedTopics.UseVisualStyleBackColor = true; @@ -75,12 +77,15 @@ namespace ntfysh_client // // notificationTopics // - this.notificationTopics.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); + this.notificationTopics.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); this.notificationTopics.FormattingEnabled = true; - this.notificationTopics.Location = new System.Drawing.Point(12, 50); + this.notificationTopics.ItemHeight = 15; + this.notificationTopics.Location = new System.Drawing.Point(14, 58); + this.notificationTopics.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); this.notificationTopics.Name = "notificationTopics"; this.notificationTopics.SelectionMode = System.Windows.Forms.SelectionMode.MultiExtended; - this.notificationTopics.Size = new System.Drawing.Size(145, 173); + this.notificationTopics.Size = new System.Drawing.Size(168, 199); this.notificationTopics.TabIndex = 3; this.notificationTopics.Click += new System.EventHandler(this.notificationTopics_Click); this.notificationTopics.SelectedValueChanged += new System.EventHandler(this.notificationTopics_SelectedValueChanged); @@ -95,7 +100,9 @@ namespace ntfysh_client // // trayContextMenu // - this.trayContextMenu.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { this.showControlWindowToolStripMenuItem, this.exitToolStripMenuItem }); + this.trayContextMenu.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.showControlWindowToolStripMenuItem, + this.exitToolStripMenuItem}); this.trayContextMenu.Name = "trayContextMenu"; this.trayContextMenu.Size = new System.Drawing.Size(190, 48); // @@ -118,16 +125,20 @@ namespace ntfysh_client // menuStrip1 // this.menuStrip1.BackColor = System.Drawing.Color.White; - this.menuStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { this.fileToolStripMenuItem, this.helpToolStripMenuItem }); + this.menuStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.fileToolStripMenuItem, + this.helpToolStripMenuItem}); this.menuStrip1.Location = new System.Drawing.Point(0, 0); this.menuStrip1.Name = "menuStrip1"; - this.menuStrip1.Size = new System.Drawing.Size(353, 24); + this.menuStrip1.Padding = new System.Windows.Forms.Padding(7, 2, 0, 2); + this.menuStrip1.Size = new System.Drawing.Size(412, 24); this.menuStrip1.TabIndex = 4; this.menuStrip1.Text = "menuStrip1"; // // fileToolStripMenuItem // - this.fileToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { this.exitToolStripMenuItem1 }); + this.fileToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.exitToolStripMenuItem1}); this.fileToolStripMenuItem.Name = "fileToolStripMenuItem"; this.fileToolStripMenuItem.Size = new System.Drawing.Size(37, 20); this.fileToolStripMenuItem.Text = "File"; @@ -136,12 +147,16 @@ namespace ntfysh_client // this.exitToolStripMenuItem1.Image = ((System.Drawing.Image)(resources.GetObject("exitToolStripMenuItem1.Image"))); this.exitToolStripMenuItem1.Name = "exitToolStripMenuItem1"; - this.exitToolStripMenuItem1.Size = new System.Drawing.Size(93, 22); + this.exitToolStripMenuItem1.Size = new System.Drawing.Size(180, 22); this.exitToolStripMenuItem1.Text = "Exit"; + this.exitToolStripMenuItem1.Click += new System.EventHandler(this.exitToolStripMenuItem1_Click); // // helpToolStripMenuItem // - this.helpToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { this.ntfyshWebsiteToolStripMenuItem, this.toolStripMenuItem1, this.aboutToolStripMenuItem }); + this.helpToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.ntfyshWebsiteToolStripMenuItem, + this.toolStripMenuItem1, + this.aboutToolStripMenuItem}); this.helpToolStripMenuItem.Name = "helpToolStripMenuItem"; this.helpToolStripMenuItem.Size = new System.Drawing.Size(44, 20); this.helpToolStripMenuItem.Text = "Help"; @@ -170,18 +185,19 @@ namespace ntfysh_client // label1 // this.label1.AutoSize = true; - this.label1.Location = new System.Drawing.Point(12, 34); + this.label1.Location = new System.Drawing.Point(14, 39); + this.label1.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); this.label1.Name = "label1"; - this.label1.Size = new System.Drawing.Size(145, 13); + this.label1.Size = new System.Drawing.Size(164, 15); this.label1.TabIndex = 1; this.label1.Text = "Subscribed notification topics"; // // Form1 // - this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.BackColor = System.Drawing.Color.White; - this.ClientSize = new System.Drawing.Size(353, 236); + this.ClientSize = new System.Drawing.Size(412, 272); this.Controls.Add(this.menuStrip1); this.Controls.Add(this.notificationTopics); this.Controls.Add(this.removeSelectedTopics); @@ -191,6 +207,7 @@ namespace ntfysh_client this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon"))); this.KeyPreview = true; this.MainMenuStrip = this.menuStrip1; + this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); this.MaximizeBox = false; this.MinimizeBox = false; this.Name = "Form1"; @@ -204,6 +221,7 @@ namespace ntfysh_client this.menuStrip1.PerformLayout(); this.ResumeLayout(false); this.PerformLayout(); + } #endregion diff --git a/ntfysh_client/Form1.cs b/ntfysh_client/Form1.cs index 5666191..2d51bf1 100644 --- a/ntfysh_client/Form1.cs +++ b/ntfysh_client/Form1.cs @@ -212,5 +212,11 @@ namespace ntfysh_client using AboutBox d = new AboutBox(); d.ShowDialog(); } + + private void exitToolStripMenuItem1_Click(object sender, EventArgs e) + { + _trueExit = true; + Close(); + } } } diff --git a/ntfysh_client/Form1.resx b/ntfysh_client/Form1.resx index 7adc2ca..f988851 100644 --- a/ntfysh_client/Form1.resx +++ b/ntfysh_client/Form1.resx @@ -1,64 +1,4 @@ - - - + @@ -127,7 +67,7 @@ iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6 - JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAACXBIWXMAAA7DAAAOwwHHb6hkAAAAiklE + JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAACXBIWXMAAA7AAAAOwAFq1okJAAAAiklE QVQ4T8WPQQqAIBBFvUOrXIhu3dcZOkqtu5R1ia4SdRDrxyyGGKmBIOGBDL6nmk9WCGE9yUpW0q9Ads7V GuCQ/kHAe79joAEO6dcLFmttpQEO6c+Bvpm2oZ0zwB4zVQBiF8cIsMdMDPCb+G2vA/wgP/z6C6WAhBgo fUFCDGi4BxIGShLpvy5jDoPes/0oNG3VAAAAAElFTkSuQmCC @@ -136,7 +76,7 @@ iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6 - JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAACXBIWXMAAA7DAAAOwwHHb6hkAAAAgUlE + JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAACXBIWXMAAA7AAAAOwAFq1okJAAAAgUlE QVQ4T2OgGjjOLq4AZeIFWNWBBI9yib06zC3uABXCCkDyIHVYDTnMI2pzhEvs5VFucSeoEAo4wiNii08e DHAZQshwFICumCTNMADyK1gTl2gJiCYUNlgBSDPQ1v8gGipEPKDIBRSFAa6oIsoQQvGM1xCqpESsglgA seroBRgYAOoOWBJbfVcRAAAAAElFTkSuQmCC @@ -327,7 +267,7 @@ iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6 - JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAACXBIWXMAAA7DAAAOwwHHb6hkAAAAgUlE + JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAACXBIWXMAAA7AAAAOwAFq1okJAAAAgUlE QVQ4T2OgGjjOLq4AZeIFWNWBBI9yib06zC3uABXCCkDyIHVYDTnMI2pzhEvs5VFucSeoEAo4wiNii08e DHAZQshwFICumCTNMADyK1gTl2gJiCYUNlgBSDPQ1v8gGipEPKDIBRSFAa6oIsoQQvGM1xCqpESsglgA seroBRgYAOoOWBJbfVcRAAAAAElFTkSuQmCC @@ -336,7 +276,7 @@ iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6 - JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAACXBIWXMAAA7DAAAOwwHHb6hkAAABDElE + JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAACXBIWXMAAA7AAAAOwAFq1okJAAABDElE QVQ4T91RO3bCQAzUy3uhChdJnTVlCj5FcMsZsF1jmy4XycPrzl6fBLgEXMFp4srRiF0wcaBP5j0VI2m0 4zH9EwSq8gPP7ELPNFxtpxrbn9vVPkSsykOkqvHi2QxsWwAevhQTzG8eCb1yD7Glv2I5MlM+sLX0GqEq 6+jVPFl6QZz7lOgdJVnzuMq/sEepbsGln2YnR3AAm0IcIE71geJsTO9mcOWAOa3zicxxxGWApXMGabaH @@ -347,7 +287,7 @@ iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6 - JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAACXBIWXMAAA7DAAAOwwHHb6hkAAAAwUlE + JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAACXBIWXMAAA7AAAAOwAFq1okJAAAAwUlE QVQ4T2OgClBUVHwAxP9JxA+g2sEG/JeTk5MkBYP0QLVTwQAFBYXHIAFSMEgPVDvYBfulpKSEScEgPVDt dDIA6OTNQHX/YXysBjAxMcUxMjKuBrGB9DIgPxqmARhwOUBD3sD4WA0QERGRBHLvCwoKygLpm6KiohIw DVBNz5HY2L0AtHkOEE8F4okgvoyMjDSSJsIGsLOzuwKFfnBycppJS0vLAJ39BUQTbQA6RnYBMkY3YAVI diff --git a/ntfysh_client/Program.cs b/ntfysh_client/Program.cs index 6f47948..a8feec7 100644 --- a/ntfysh_client/Program.cs +++ b/ntfysh_client/Program.cs @@ -16,7 +16,7 @@ namespace ntfysh_client [STAThread] static void Main() { - //Application.SetHighDpiMode(HighDpiMode.SystemAware); + Application.SetHighDpiMode(HighDpiMode.SystemAware); Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new Form1(NotificationListener)); From bba9051b4bab8d3bc6ad91644ed91fccc54e5856 Mon Sep 17 00:00:00 2001 From: Alexander Horner <33007665+alexhorner@users.noreply.github.com> Date: Tue, 6 Dec 2022 23:27:30 +0000 Subject: [PATCH 07/14] Fix browser launch --- ntfysh_client/Form1.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ntfysh_client/Form1.cs b/ntfysh_client/Form1.cs index 2d51bf1..664180c 100644 --- a/ntfysh_client/Form1.cs +++ b/ntfysh_client/Form1.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.Data; +using System.Diagnostics; using System.Drawing; using System.IO; using System.Linq; @@ -204,7 +205,10 @@ namespace ntfysh_client private void ntfyshWebsiteToolStripMenuItem_Click(object sender, EventArgs e) { - System.Diagnostics.Process.Start("https://ntfy.sh/"); + Process.Start(new ProcessStartInfo("https://ntfy.sh/") + { + UseShellExecute = true + }); } private void aboutToolStripMenuItem_Click(object sender, EventArgs e) From 76031ee868d8af29930a2bb953fd5e29f58beff0 Mon Sep 17 00:00:00 2001 From: Alexander Horner <33007665+alexhorner@users.noreply.github.com> Date: Tue, 6 Dec 2022 23:31:11 +0000 Subject: [PATCH 08/14] Squash that async bug for good --- ntfysh_client/Form1.cs | 4 ++-- ntfysh_client/NotificationListener.cs | 28 ++++++++++++++++++--------- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/ntfysh_client/Form1.cs b/ntfysh_client/Form1.cs index 664180c..05f07fc 100644 --- a/ntfysh_client/Form1.cs +++ b/ntfysh_client/Form1.cs @@ -47,13 +47,13 @@ namespace ntfysh_client SaveTopicsToFile(); } - private void removeSelectedTopics_Click(object sender, EventArgs e) + private async void removeSelectedTopics_Click(object sender, EventArgs e) { while (notificationTopics.SelectedIndex > -1) { string topicUniqueString = (string)notificationTopics.Items[notificationTopics.SelectedIndex]; - _notificationListener.UnsubscribeFromTopic(topicUniqueString); + await _notificationListener.UnsubscribeFromTopicAsync(topicUniqueString); notificationTopics.Items.Remove(topicUniqueString); } diff --git a/ntfysh_client/NotificationListener.cs b/ntfysh_client/NotificationListener.cs index 385e244..5a08f70 100644 --- a/ntfysh_client/NotificationListener.cs +++ b/ntfysh_client/NotificationListener.cs @@ -37,8 +37,8 @@ namespace ntfysh_client while (!cancellationToken.IsCancellationRequested) { - using HttpResponseMessage response = await _httpClient.SendAsync(message, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); - await using Stream body = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + using HttpResponseMessage response = await _httpClient.SendAsync(message, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + await using Stream body = await response.Content.ReadAsStreamAsync(cancellationToken); try { @@ -48,7 +48,7 @@ namespace ntfysh_client { //Read as much as possible byte[] buffer = new byte[8192]; - int readBytes = await body.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false); + int readBytes = await body.ReadAsync(buffer, 0, buffer.Length, cancellationToken); //Append it to our main buffer mainBuffer.Append(Encoding.UTF8.GetString(buffer, 0, readBytes)); @@ -125,25 +125,35 @@ namespace ntfysh_client SubscribedTopicsByUnique.Add(unique, new SubscribedTopic(topicId, serverUrl, username, password, listenTask, listenCanceller)); } - public void UnsubscribeFromTopic(string topicUniqueString) + public async Task UnsubscribeFromTopicAsync(string topicUniqueString) { if (_isDisposed) throw new ObjectDisposedException(nameof(NotificationListener)); #if DEBUG Debug.WriteLine($"Removing topic {topicUniqueString}"); #endif + + // ReSharper disable once InlineOutVariableDeclaration - Needed to avoid nullable warning + SubscribedTopic topic; //Topic isn't even subscribed, ignore - if (!SubscribedTopicsByUnique.TryGetValue(topicUniqueString, out SubscribedTopic? topic)) return; + if (!SubscribedTopicsByUnique.TryGetValue(topicUniqueString, out topic!)) return; //Cancel and dispose the task runner - topic?.RunnerCanceller.Cancel(); + topic.RunnerCanceller.Cancel(); //Wait for the task runner to shut down - while (topic is not null && !topic.Runner.IsCompleted) Thread.Sleep(100); - + try + { + await topic.Runner; + } + catch (Exception) + { + // ignored + } + //Dispose task - topic?.Runner.Dispose(); + topic.Runner.Dispose(); //Remove the old topic SubscribedTopicsByUnique.Remove(topicUniqueString); From 294747def51b937077398944729a28595d7cf586 Mon Sep 17 00:00:00 2001 From: Alexander Horner <33007665+alexhorner@users.noreply.github.com> Date: Wed, 7 Dec 2022 19:40:37 +0000 Subject: [PATCH 09/14] Adjust layout of main form to make long topic names visible --- ntfysh_client/Form1.Designer.cs | 22 +++++++++++----------- ntfysh_client/Form1.resx | 10 +++++----- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/ntfysh_client/Form1.Designer.cs b/ntfysh_client/Form1.Designer.cs index 89f66c2..6fbabac 100644 --- a/ntfysh_client/Form1.Designer.cs +++ b/ntfysh_client/Form1.Designer.cs @@ -53,10 +53,10 @@ namespace ntfysh_client // subscribeNewTopic // this.subscribeNewTopic.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); - this.subscribeNewTopic.Location = new System.Drawing.Point(275, 58); + this.subscribeNewTopic.Location = new System.Drawing.Point(211, 251); this.subscribeNewTopic.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); this.subscribeNewTopic.Name = "subscribeNewTopic"; - this.subscribeNewTopic.Size = new System.Drawing.Size(122, 27); + this.subscribeNewTopic.Size = new System.Drawing.Size(188, 27); this.subscribeNewTopic.TabIndex = 2; this.subscribeNewTopic.Text = "Add"; this.subscribeNewTopic.UseVisualStyleBackColor = true; @@ -66,10 +66,10 @@ namespace ntfysh_client // this.removeSelectedTopics.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); this.removeSelectedTopics.Enabled = false; - this.removeSelectedTopics.Location = new System.Drawing.Point(275, 91); + this.removeSelectedTopics.Location = new System.Drawing.Point(13, 251); this.removeSelectedTopics.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); this.removeSelectedTopics.Name = "removeSelectedTopics"; - this.removeSelectedTopics.Size = new System.Drawing.Size(122, 27); + this.removeSelectedTopics.Size = new System.Drawing.Size(188, 27); this.removeSelectedTopics.TabIndex = 0; this.removeSelectedTopics.Text = "Remove selected"; this.removeSelectedTopics.UseVisualStyleBackColor = true; @@ -81,11 +81,11 @@ namespace ntfysh_client | System.Windows.Forms.AnchorStyles.Right))); this.notificationTopics.FormattingEnabled = true; this.notificationTopics.ItemHeight = 15; - this.notificationTopics.Location = new System.Drawing.Point(14, 58); + this.notificationTopics.Location = new System.Drawing.Point(13, 46); this.notificationTopics.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); this.notificationTopics.Name = "notificationTopics"; this.notificationTopics.SelectionMode = System.Windows.Forms.SelectionMode.MultiExtended; - this.notificationTopics.Size = new System.Drawing.Size(168, 199); + this.notificationTopics.Size = new System.Drawing.Size(386, 199); this.notificationTopics.TabIndex = 3; this.notificationTopics.Click += new System.EventHandler(this.notificationTopics_Click); this.notificationTopics.SelectedValueChanged += new System.EventHandler(this.notificationTopics_SelectedValueChanged); @@ -147,7 +147,7 @@ namespace ntfysh_client // this.exitToolStripMenuItem1.Image = ((System.Drawing.Image)(resources.GetObject("exitToolStripMenuItem1.Image"))); this.exitToolStripMenuItem1.Name = "exitToolStripMenuItem1"; - this.exitToolStripMenuItem1.Size = new System.Drawing.Size(180, 22); + this.exitToolStripMenuItem1.Size = new System.Drawing.Size(93, 22); this.exitToolStripMenuItem1.Text = "Exit"; this.exitToolStripMenuItem1.Click += new System.EventHandler(this.exitToolStripMenuItem1_Click); // @@ -185,19 +185,19 @@ namespace ntfysh_client // label1 // this.label1.AutoSize = true; - this.label1.Location = new System.Drawing.Point(14, 39); + this.label1.Location = new System.Drawing.Point(13, 27); this.label1.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); this.label1.Name = "label1"; - this.label1.Size = new System.Drawing.Size(164, 15); + this.label1.Size = new System.Drawing.Size(170, 15); this.label1.TabIndex = 1; - this.label1.Text = "Subscribed notification topics"; + this.label1.Text = "Subscribed Notification Topics:"; // // Form1 // this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.BackColor = System.Drawing.Color.White; - this.ClientSize = new System.Drawing.Size(412, 272); + this.ClientSize = new System.Drawing.Size(412, 288); this.Controls.Add(this.menuStrip1); this.Controls.Add(this.notificationTopics); this.Controls.Add(this.removeSelectedTopics); diff --git a/ntfysh_client/Form1.resx b/ntfysh_client/Form1.resx index f988851..dbf516e 100644 --- a/ntfysh_client/Form1.resx +++ b/ntfysh_client/Form1.resx @@ -67,7 +67,7 @@ iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6 - JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAACXBIWXMAAA7AAAAOwAFq1okJAAAAiklE + JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAACXBIWXMAAA6+AAAOvgHqQrHAAAAAiklE QVQ4T8WPQQqAIBBFvUOrXIhu3dcZOkqtu5R1ia4SdRDrxyyGGKmBIOGBDL6nmk9WCGE9yUpW0q9Ads7V GuCQ/kHAe79joAEO6dcLFmttpQEO6c+Bvpm2oZ0zwB4zVQBiF8cIsMdMDPCb+G2vA/wgP/z6C6WAhBgo fUFCDGi4BxIGShLpvy5jDoPes/0oNG3VAAAAAElFTkSuQmCC @@ -76,7 +76,7 @@ iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6 - JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAACXBIWXMAAA7AAAAOwAFq1okJAAAAgUlE + JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAACXBIWXMAAA6+AAAOvgHqQrHAAAAAgUlE QVQ4T2OgGjjOLq4AZeIFWNWBBI9yib06zC3uABXCCkDyIHVYDTnMI2pzhEvs5VFucSeoEAo4wiNii08e DHAZQshwFICumCTNMADyK1gTl2gJiCYUNlgBSDPQ1v8gGipEPKDIBRSFAa6oIsoQQvGM1xCqpESsglgA seroBRgYAOoOWBJbfVcRAAAAAElFTkSuQmCC @@ -267,7 +267,7 @@ iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6 - JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAACXBIWXMAAA7AAAAOwAFq1okJAAAAgUlE + JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAACXBIWXMAAA6+AAAOvgHqQrHAAAAAgUlE QVQ4T2OgGjjOLq4AZeIFWNWBBI9yib06zC3uABXCCkDyIHVYDTnMI2pzhEvs5VFucSeoEAo4wiNii08e DHAZQshwFICumCTNMADyK1gTl2gJiCYUNlgBSDPQ1v8gGipEPKDIBRSFAa6oIsoQQvGM1xCqpESsglgA seroBRgYAOoOWBJbfVcRAAAAAElFTkSuQmCC @@ -276,7 +276,7 @@ iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6 - JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAACXBIWXMAAA7AAAAOwAFq1okJAAABDElE + JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAACXBIWXMAAA6+AAAOvgHqQrHAAAABDElE QVQ4T91RO3bCQAzUy3uhChdJnTVlCj5FcMsZsF1jmy4XycPrzl6fBLgEXMFp4srRiF0wcaBP5j0VI2m0 4zH9EwSq8gPP7ELPNFxtpxrbn9vVPkSsykOkqvHi2QxsWwAevhQTzG8eCb1yD7Glv2I5MlM+sLX0GqEq 6+jVPFl6QZz7lOgdJVnzuMq/sEepbsGln2YnR3AAm0IcIE71geJsTO9mcOWAOa3zicxxxGWApXMGabaH @@ -287,7 +287,7 @@ iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6 - JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAACXBIWXMAAA7AAAAOwAFq1okJAAAAwUlE + JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAACXBIWXMAAA6+AAAOvgHqQrHAAAAAwUlE QVQ4T2OgClBUVHwAxP9JxA+g2sEG/JeTk5MkBYP0QLVTwQAFBYXHIAFSMEgPVDvYBfulpKSEScEgPVDt dDIA6OTNQHX/YXysBjAxMcUxMjKuBrGB9DIgPxqmARhwOUBD3sD4WA0QERGRBHLvCwoKygLpm6KiohIw DVBNz5HY2L0AtHkOEE8F4okgvoyMjDSSJsIGsLOzuwKFfnBycppJS0vLAJ39BUQTbQA6RnYBMkY3YAVI From fe4ae354b2626d05d0aad777350cc2778bbe3d7a Mon Sep 17 00:00:00 2001 From: Alexander Horner <33007665+alexhorner@users.noreply.github.com> Date: Wed, 7 Dec 2022 19:54:48 +0000 Subject: [PATCH 10/14] Rename main form --- .../{Form1.Designer.cs => MainForm.Designer.cs} | 14 +++++++------- ntfysh_client/{Form1.cs => MainForm.cs} | 14 +++++--------- ntfysh_client/{Form1.resx => MainForm.resx} | 0 ntfysh_client/Program.cs | 2 +- ntfysh_client/ntfysh_client.csproj | 5 +---- 5 files changed, 14 insertions(+), 21 deletions(-) rename ntfysh_client/{Form1.Designer.cs => MainForm.Designer.cs} (98%) rename ntfysh_client/{Form1.cs => MainForm.cs} (94%) rename ntfysh_client/{Form1.resx => MainForm.resx} (100%) diff --git a/ntfysh_client/Form1.Designer.cs b/ntfysh_client/MainForm.Designer.cs similarity index 98% rename from ntfysh_client/Form1.Designer.cs rename to ntfysh_client/MainForm.Designer.cs index 6fbabac..83e8285 100644 --- a/ntfysh_client/Form1.Designer.cs +++ b/ntfysh_client/MainForm.Designer.cs @@ -1,7 +1,7 @@  namespace ntfysh_client { - partial class Form1 + partial class MainForm { /// /// Required designer variable. @@ -30,7 +30,7 @@ namespace ntfysh_client private void InitializeComponent() { this.components = new System.ComponentModel.Container(); - System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(Form1)); + System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(MainForm)); this.subscribeNewTopic = new System.Windows.Forms.Button(); this.removeSelectedTopics = new System.Windows.Forms.Button(); this.notificationTopics = new System.Windows.Forms.ListBox(); @@ -192,7 +192,7 @@ namespace ntfysh_client this.label1.TabIndex = 1; this.label1.Text = "Subscribed Notification Topics:"; // - // Form1 + // MainForm // this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; @@ -210,12 +210,12 @@ namespace ntfysh_client this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); this.MaximizeBox = false; this.MinimizeBox = false; - this.Name = "Form1"; + this.Name = "MainForm"; this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; this.Text = "ntfy.sh"; - this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.Form1_FormClosing); - this.FormClosed += new System.Windows.Forms.FormClosedEventHandler(this.Form1_FormClosed); - this.Load += new System.EventHandler(this.Form1_Load); + this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.MainForm_FormClosing); + this.FormClosed += new System.Windows.Forms.FormClosedEventHandler(this.MainForm_FormClosed); + this.Load += new System.EventHandler(this.MainForm_Load); this.trayContextMenu.ResumeLayout(false); this.menuStrip1.ResumeLayout(false); this.menuStrip1.PerformLayout(); diff --git a/ntfysh_client/Form1.cs b/ntfysh_client/MainForm.cs similarity index 94% rename from ntfysh_client/Form1.cs rename to ntfysh_client/MainForm.cs index 05f07fc..296c664 100644 --- a/ntfysh_client/Form1.cs +++ b/ntfysh_client/MainForm.cs @@ -1,25 +1,21 @@ using System; using System.Collections.Generic; -using System.ComponentModel; -using System.Data; using System.Diagnostics; using System.Drawing; using System.IO; using System.Linq; using System.Reflection; -using System.Text; -using System.Threading.Tasks; using System.Windows.Forms; using Newtonsoft.Json; namespace ntfysh_client { - public partial class Form1 : Form + public partial class MainForm : Form { private readonly NotificationListener _notificationListener; private bool _trueExit; - public Form1(NotificationListener notificationListener) + public MainForm(NotificationListener notificationListener) { _notificationListener = notificationListener; _notificationListener.OnNotificationReceive += OnNotificationReceive; @@ -27,7 +23,7 @@ namespace ntfysh_client InitializeComponent(); } - private void Form1_Load(object sender, EventArgs e) => LoadTopics(); + private void MainForm_Load(object sender, EventArgs e) => LoadTopics(); private void subscribeNewTopic_Click(object sender, EventArgs e) { @@ -181,12 +177,12 @@ namespace ntfysh_client notifyIcon.ShowBalloonTip(3000, e.Title, e.Message, ToolTipIcon.Info); } - private void Form1_FormClosed(object sender, FormClosedEventArgs e) + private void MainForm_FormClosed(object sender, FormClosedEventArgs e) { notifyIcon.Dispose(); } - private void Form1_FormClosing(object sender, FormClosingEventArgs e) + private void MainForm_FormClosing(object sender, FormClosingEventArgs e) { // Let it close if (_trueExit) return; diff --git a/ntfysh_client/Form1.resx b/ntfysh_client/MainForm.resx similarity index 100% rename from ntfysh_client/Form1.resx rename to ntfysh_client/MainForm.resx diff --git a/ntfysh_client/Program.cs b/ntfysh_client/Program.cs index a8feec7..5841c89 100644 --- a/ntfysh_client/Program.cs +++ b/ntfysh_client/Program.cs @@ -19,7 +19,7 @@ namespace ntfysh_client Application.SetHighDpiMode(HighDpiMode.SystemAware); Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); - Application.Run(new Form1(NotificationListener)); + Application.Run(new MainForm(NotificationListener)); } } } diff --git a/ntfysh_client/ntfysh_client.csproj b/ntfysh_client/ntfysh_client.csproj index dfe4310..5b9a3a3 100644 --- a/ntfysh_client/ntfysh_client.csproj +++ b/ntfysh_client/ntfysh_client.csproj @@ -7,14 +7,11 @@ latest enable NotificationHub.ico + ntfysh_client.Program - - - - \ No newline at end of file From 1e1187e71ff99d3e70b33b3501c6b78ab2b6c24e Mon Sep 17 00:00:00 2001 From: Alexander Horner <33007665+alexhorner@users.noreply.github.com> Date: Wed, 7 Dec 2022 21:36:03 +0000 Subject: [PATCH 11/14] Basic Websocket support working, without authentication (auth broken) --- ntfysh_client/MainForm.cs | 30 +++++- ntfysh_client/NotificationListener.cs | 72 ++++++++++++- ntfysh_client/SubscribeDialog.Designer.cs | 94 ++++++++++++----- ntfysh_client/SubscribeDialog.cs | 122 +++++++++++++++++++++- ntfysh_client/SubscribeDialog.resx | 62 +---------- 5 files changed, 286 insertions(+), 94 deletions(-) diff --git a/ntfysh_client/MainForm.cs b/ntfysh_client/MainForm.cs index 296c664..acf5b99 100644 --- a/ntfysh_client/MainForm.cs +++ b/ntfysh_client/MainForm.cs @@ -34,8 +34,15 @@ namespace ntfysh_client if (result != DialogResult.OK) return; //Subscribe - _notificationListener.SubscribeToTopicUsingLongHttpJson(dialog.Unique, dialog.TopicId, dialog.ServerUrl, dialog.Username, dialog.Password); - + if (dialog.UseWebsockets) + { + _notificationListener.SubscribeToTopicUsingWebsocket(dialog.Unique, dialog.TopicId, dialog.ServerUrl, dialog.Username, dialog.Password); + } + else + { + _notificationListener.SubscribeToTopicUsingLongHttpJson(dialog.Unique, dialog.TopicId, dialog.ServerUrl, dialog.Username, dialog.Password); + } + //Add to the user visible list notificationTopics.Items.Add(dialog.Unique); @@ -167,7 +174,24 @@ namespace ntfysh_client //Load them in foreach (SubscribedTopic topic in topics) { - _notificationListener.SubscribeToTopicUsingLongHttpJson($"{topic.TopicId}@{topic.ServerUrl}", topic.TopicId, topic.ServerUrl, topic.Username, topic.Password); + string[] parts = topic.ServerUrl.Split("://", 2); + + switch (parts[0].ToLower()) + { + case "ws": + case "wss": + _notificationListener.SubscribeToTopicUsingWebsocket($"{topic.TopicId}@{topic.ServerUrl}", topic.TopicId, topic.ServerUrl, topic.Username, topic.Password); + break; + + case "http": + case "https": + _notificationListener.SubscribeToTopicUsingLongHttpJson($"{topic.TopicId}@{topic.ServerUrl}", topic.TopicId, topic.ServerUrl, topic.Username, topic.Password); + break; + + default: + continue; + } + notificationTopics.Items.Add($"{topic.TopicId}@{topic.ServerUrl}"); } } diff --git a/ntfysh_client/NotificationListener.cs b/ntfysh_client/NotificationListener.cs index 5a08f70..9995599 100644 --- a/ntfysh_client/NotificationListener.cs +++ b/ntfysh_client/NotificationListener.cs @@ -31,7 +31,7 @@ namespace ntfysh_client ServicePointManager.DefaultConnectionLimit = 100; } - private async Task ListenToTopicAsync(HttpRequestMessage message, CancellationToken cancellationToken) + private async Task ListenToTopicWithHttpLongJsonAsync(HttpRequestMessage message, CancellationToken cancellationToken) { if (_isDisposed) throw new ObjectDisposedException(nameof(NotificationListener)); @@ -83,6 +83,59 @@ namespace ntfysh_client } } } + + private async Task ListenToTopicWithWebsocketAsync(Uri uri, NetworkCredential credentials, CancellationToken cancellationToken) + { + if (_isDisposed) throw new ObjectDisposedException(nameof(NotificationListener)); + + while (!cancellationToken.IsCancellationRequested) + { + using ClientWebSocket socket = new(); + socket.Options.Credentials = credentials; + + try + { + StringBuilder mainBuffer = new(); + + await socket.ConnectAsync(uri, cancellationToken); + + while (!cancellationToken.IsCancellationRequested) + { + //Read as much as possible + byte[] buffer = new byte[8192]; + WebSocketReceiveResult? result = await socket.ReceiveAsync(new ArraySegment(buffer), cancellationToken); + + //Append it to our main buffer + mainBuffer.Append(Encoding.UTF8.GetString(buffer, 0, result.Count)); + + List lines = mainBuffer.ToString().Split('\n').ToList(); + //If we have not yet received a full line, meaning theres only 1 part, go back to reading + if (lines.Count <= 1) continue; + + //We now have at least 1 line! Count how many full lines. There will always be a partial line at the end, even if that partial line is empty + //Separate the partial line from the full lines + int partialLineIndex = lines.Count - 1; + string partialLine = lines[partialLineIndex]; + lines.RemoveAt(partialLineIndex); + + //Process the full lines + foreach (string line in lines) ProcessMessage(line); + + //Write back the partial line + mainBuffer.Clear(); + mainBuffer.Append(partialLine); + } + } + catch (Exception ex) + { + #if DEBUG + Debug.WriteLine(ex); + #endif + + //Fall back to the outer loop to restart the listen, or cancel if requested + } + } + } private void ProcessMessage(string message) { @@ -120,10 +173,25 @@ namespace ntfysh_client } CancellationTokenSource listenCanceller = new(); - Task listenTask = ListenToTopicAsync(message, listenCanceller.Token); + Task listenTask = ListenToTopicWithHttpLongJsonAsync(message, listenCanceller.Token); SubscribedTopicsByUnique.Add(unique, new SubscribedTopic(topicId, serverUrl, username, password, listenTask, listenCanceller)); } + + public void SubscribeToTopicUsingWebsocket(string unique, string topicId, string serverUrl, string? username, string? password) + { + if (_isDisposed) throw new ObjectDisposedException(nameof(NotificationListener)); + + if (SubscribedTopicsByUnique.ContainsKey(unique)) throw new InvalidOperationException("A topic with this unique already exists"); + + if (string.IsNullOrWhiteSpace(username)) username = null; + if (string.IsNullOrWhiteSpace(password)) password = null; + + Uri targetUri = new($"{serverUrl}/{HttpUtility.UrlEncode(topicId)}/ws"); + CancellationTokenSource listenCanceller = new(); + Task listenTask = ListenToTopicWithWebsocketAsync(targetUri, new NetworkCredential(username, password), listenCanceller.Token); + SubscribedTopicsByUnique.Add(unique, new SubscribedTopic(topicId, serverUrl, username, password, listenTask, listenCanceller)); + } public async Task UnsubscribeFromTopicAsync(string topicUniqueString) { diff --git a/ntfysh_client/SubscribeDialog.Designer.cs b/ntfysh_client/SubscribeDialog.Designer.cs index 30d5a60..4977b26 100644 --- a/ntfysh_client/SubscribeDialog.Designer.cs +++ b/ntfysh_client/SubscribeDialog.Designer.cs @@ -40,6 +40,8 @@ namespace ntfysh_client this.label3 = new System.Windows.Forms.Label(); this.password = new System.Windows.Forms.TextBox(); this.label4 = new System.Windows.Forms.Label(); + this.label5 = new System.Windows.Forms.Label(); + this.connectionType = new System.Windows.Forms.ComboBox(); this.panel1.SuspendLayout(); this.SuspendLayout(); // @@ -49,18 +51,19 @@ namespace ntfysh_client this.panel1.Controls.Add(this.button2); this.panel1.Controls.Add(this.button1); this.panel1.Dock = System.Windows.Forms.DockStyle.Bottom; - this.panel1.Location = new System.Drawing.Point(0, 175); + this.panel1.Location = new System.Drawing.Point(0, 244); + this.panel1.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); this.panel1.Name = "panel1"; - this.panel1.Size = new System.Drawing.Size(297, 44); + this.panel1.Size = new System.Drawing.Size(346, 51); this.panel1.TabIndex = 0; // // button2 // this.button2.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); - this.button2.Location = new System.Drawing.Point(131, 11); - this.button2.Margin = new System.Windows.Forms.Padding(10, 10, 3, 10); + this.button2.Location = new System.Drawing.Point(153, 13); + this.button2.Margin = new System.Windows.Forms.Padding(12, 12, 4, 12); this.button2.Name = "button2"; - this.button2.Size = new System.Drawing.Size(75, 23); + this.button2.Size = new System.Drawing.Size(88, 27); this.button2.TabIndex = 1; this.button2.Text = "Cancel"; this.button2.UseVisualStyleBackColor = true; @@ -69,10 +72,10 @@ namespace ntfysh_client // button1 // this.button1.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); - this.button1.Location = new System.Drawing.Point(212, 11); - this.button1.Margin = new System.Windows.Forms.Padding(3, 10, 10, 10); + this.button1.Location = new System.Drawing.Point(247, 13); + this.button1.Margin = new System.Windows.Forms.Padding(4, 12, 12, 12); this.button1.Name = "button1"; - this.button1.Size = new System.Drawing.Size(75, 23); + this.button1.Size = new System.Drawing.Size(88, 27); this.button1.TabIndex = 2; this.button1.Text = "Subscribe"; this.button1.UseVisualStyleBackColor = true; @@ -81,9 +84,10 @@ namespace ntfysh_client // label1 // this.label1.AutoSize = true; - this.label1.Location = new System.Drawing.Point(12, 9); + this.label1.Location = new System.Drawing.Point(14, 10); + this.label1.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); this.label1.Name = "label1"; - this.label1.Size = new System.Drawing.Size(51, 13); + this.label1.Size = new System.Drawing.Size(52, 15); this.label1.TabIndex = 1; this.label1.Text = "Topic ID:"; // @@ -91,9 +95,10 @@ namespace ntfysh_client // this.topicId.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); - this.topicId.Location = new System.Drawing.Point(12, 25); + this.topicId.Location = new System.Drawing.Point(14, 29); + this.topicId.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); this.topicId.Name = "topicId"; - this.topicId.Size = new System.Drawing.Size(273, 20); + this.topicId.Size = new System.Drawing.Size(318, 23); this.topicId.TabIndex = 0; this.topicId.KeyDown += new System.Windows.Forms.KeyEventHandler(this.topicId_KeyDown); // @@ -101,19 +106,21 @@ namespace ntfysh_client // this.serverUrl.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); - this.serverUrl.Location = new System.Drawing.Point(12, 64); + this.serverUrl.Location = new System.Drawing.Point(14, 74); + this.serverUrl.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); this.serverUrl.Name = "serverUrl"; - this.serverUrl.Size = new System.Drawing.Size(273, 20); + this.serverUrl.Size = new System.Drawing.Size(318, 23); this.serverUrl.TabIndex = 2; - this.serverUrl.Text = "https://ntfy.sh"; + this.serverUrl.Text = "wss://ntfy.sh"; this.serverUrl.KeyDown += new System.Windows.Forms.KeyEventHandler(this.serverUrl_KeyDown); // // label2 // this.label2.AutoSize = true; - this.label2.Location = new System.Drawing.Point(10, 48); + this.label2.Location = new System.Drawing.Point(12, 55); + this.label2.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); this.label2.Name = "label2"; - this.label2.Size = new System.Drawing.Size(66, 13); + this.label2.Size = new System.Drawing.Size(66, 15); this.label2.TabIndex = 3; this.label2.Text = "Server URL:"; // @@ -121,18 +128,20 @@ namespace ntfysh_client // this.username.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); - this.username.Location = new System.Drawing.Point(12, 103); + this.username.Location = new System.Drawing.Point(14, 119); + this.username.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); this.username.Name = "username"; - this.username.Size = new System.Drawing.Size(273, 20); + this.username.Size = new System.Drawing.Size(318, 23); this.username.TabIndex = 4; this.username.KeyDown += new System.Windows.Forms.KeyEventHandler(this.username_KeyDown); // // label3 // this.label3.AutoSize = true; - this.label3.Location = new System.Drawing.Point(10, 87); + this.label3.Location = new System.Drawing.Point(12, 100); + this.label3.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); this.label3.Name = "label3"; - this.label3.Size = new System.Drawing.Size(58, 13); + this.label3.Size = new System.Drawing.Size(63, 15); this.label3.TabIndex = 5; this.label3.Text = "Username:"; // @@ -140,9 +149,10 @@ namespace ntfysh_client // this.password.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); - this.password.Location = new System.Drawing.Point(12, 142); + this.password.Location = new System.Drawing.Point(14, 164); + this.password.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); this.password.Name = "password"; - this.password.Size = new System.Drawing.Size(273, 20); + this.password.Size = new System.Drawing.Size(318, 23); this.password.TabIndex = 6; this.password.UseSystemPasswordChar = true; this.password.KeyDown += new System.Windows.Forms.KeyEventHandler(this.password_KeyDown); @@ -150,18 +160,44 @@ namespace ntfysh_client // label4 // this.label4.AutoSize = true; - this.label4.Location = new System.Drawing.Point(10, 126); + this.label4.Location = new System.Drawing.Point(12, 145); + this.label4.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); this.label4.Name = "label4"; - this.label4.Size = new System.Drawing.Size(56, 13); + this.label4.Size = new System.Drawing.Size(60, 15); this.label4.TabIndex = 7; this.label4.Text = "Password:"; // + // label5 + // + this.label5.AutoSize = true; + this.label5.Location = new System.Drawing.Point(12, 190); + this.label5.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); + this.label5.Name = "label5"; + this.label5.Size = new System.Drawing.Size(99, 15); + this.label5.TabIndex = 9; + this.label5.Text = "Connection Type:"; + // + // connectionType + // + this.connectionType.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; + this.connectionType.FormattingEnabled = true; + this.connectionType.Items.AddRange(new object[] { + "Websockets (Recommended)", + "Long HTTP JSON (Robust)"}); + this.connectionType.Location = new System.Drawing.Point(14, 208); + this.connectionType.Name = "connectionType"; + this.connectionType.Size = new System.Drawing.Size(318, 23); + this.connectionType.TabIndex = 10; + this.connectionType.TextChanged += new System.EventHandler(this.connectionType_TextChanged); + // // SubscribeDialog // - this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.BackColor = System.Drawing.Color.White; - this.ClientSize = new System.Drawing.Size(297, 219); + this.ClientSize = new System.Drawing.Size(346, 295); + this.Controls.Add(this.connectionType); + this.Controls.Add(this.label5); this.Controls.Add(this.password); this.Controls.Add(this.label4); this.Controls.Add(this.username); @@ -172,6 +208,7 @@ namespace ntfysh_client this.Controls.Add(this.label1); this.Controls.Add(this.panel1); this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog; + this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); this.MaximizeBox = false; this.MinimizeBox = false; this.Name = "SubscribeDialog"; @@ -179,6 +216,7 @@ namespace ntfysh_client this.ShowInTaskbar = false; this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; this.Text = "Subscribe to new topic"; + this.Load += new System.EventHandler(this.SubscribeDialog_Load); this.panel1.ResumeLayout(false); this.ResumeLayout(false); this.PerformLayout(); @@ -199,5 +237,7 @@ namespace ntfysh_client private System.Windows.Forms.Label label3; private System.Windows.Forms.TextBox password; private System.Windows.Forms.Label label4; + private System.Windows.Forms.Label label5; + private System.Windows.Forms.ComboBox connectionType; } } \ No newline at end of file diff --git a/ntfysh_client/SubscribeDialog.cs b/ntfysh_client/SubscribeDialog.cs index e5233f6..bc890b7 100644 --- a/ntfysh_client/SubscribeDialog.cs +++ b/ntfysh_client/SubscribeDialog.cs @@ -17,12 +17,107 @@ namespace ntfysh_client public string Unique => $"{topicId.Text}@{serverUrl.Text}"; + public bool UseWebsockets + { + get + { + switch (connectionType.Text) + { + case "Websockets (Recommended)": + return true; + + case "Long HTTP JSON (Robust)": + return false; + + default: + throw new InvalidOperationException(); + } + } + } + public SubscribeDialog(ListBox notificationTopics) { _notificationTopics = notificationTopics; InitializeComponent(); } + private void SubscribeDialog_Load(object sender, EventArgs e) + { + connectionType.SelectedIndex = 0; + } + + private bool ReparseAddress() + { + //Separate schema and address + string[] parts = serverUrl.Text.Split("://", 2); + + //Validate the basic formatting is correct + if (parts.Length != 2) return false; + + //Take the schema aside for parsing + string schema = parts[0].ToLower(); + + //Ensure the schema is actually valid + switch (schema) + { + case "http": + case "https": + case "ws": + case "wss": + break; + + default: + return false; + } + + //Correct the schema based on connection type if required + if (UseWebsockets) + { + switch (schema) + { + case "http": + schema = "ws"; + break; + + case "https": + schema = "wss"; + break; + + case "ws": + case "wss": + break; + } + } + else + { + switch (schema) + { + case "ws": + schema = "http"; + break; + + case "wss": + schema = "https"; + break; + + case "http": + case "https": + break; + } + } + + //Reconstruct the address + string finalAddress = schema + "://" + parts[1]; + + //Validate the address + if (!Uri.IsWellFormedUriString(finalAddress, UriKind.Absolute)) return false; + + //Set the final address and OK it + serverUrl.Text = finalAddress; + + return true; + } + private void button1_Click(object sender, EventArgs e) { if (topicId.Text.Length < 1) @@ -35,7 +130,7 @@ namespace ntfysh_client if (serverUrl.Text.Length < 1) { - MessageBox.Show("You must specify a server URL. The default is https://ntfy.sh", "Server URL not specified", MessageBoxButtons.OK, MessageBoxIcon.Error); + MessageBox.Show("You must specify a server URL. The default is wss://ntfy.sh", "Server URL not specified", MessageBoxButtons.OK, MessageBoxIcon.Error); DialogResult = DialogResult.None; serverUrl.Focus(); return; @@ -65,6 +160,26 @@ namespace ntfysh_client return; } + try + { + if (!ReparseAddress()) + { + MessageBox.Show($"The specified server URL is invalid. Accepted schemas are: http:// https:// ws:// wss://", "Invalid Server URL", MessageBoxButtons.OK, MessageBoxIcon.Error); + DialogResult = DialogResult.None; + connectionType.Focus(); + return; + } + } + catch (InvalidOperationException) + { + MessageBox.Show($"The selected Connection Type '{connectionType.Text}' is invalid.", "Invalid Connection Type", MessageBoxButtons.OK, MessageBoxIcon.Error); + DialogResult = DialogResult.None; + connectionType.Focus(); + return; + } + + + DialogResult = DialogResult.OK; } @@ -108,5 +223,10 @@ namespace ntfysh_client e.SuppressKeyPress = true; } } + + private void connectionType_TextChanged(object sender, EventArgs e) + { + ReparseAddress(); + } } } diff --git a/ntfysh_client/SubscribeDialog.resx b/ntfysh_client/SubscribeDialog.resx index 1af7de1..f298a7b 100644 --- a/ntfysh_client/SubscribeDialog.resx +++ b/ntfysh_client/SubscribeDialog.resx @@ -1,64 +1,4 @@ - - - + From ecbde9509f4b5429263dc72c14503efab6bd0024 Mon Sep 17 00:00:00 2001 From: Alexander Horner <33007665+alexhorner@users.noreply.github.com> Date: Wed, 7 Dec 2022 23:20:36 +0000 Subject: [PATCH 12/14] Implement error handling and notification --- ntfysh_client/MainForm.cs | 26 +++- ntfysh_client/NotificationListener.cs | 215 +++++++++++++++++++------- ntfysh_client/SubscribedTopic.cs | 15 +- 3 files changed, 188 insertions(+), 68 deletions(-) diff --git a/ntfysh_client/MainForm.cs b/ntfysh_client/MainForm.cs index acf5b99..bd1bf9f 100644 --- a/ntfysh_client/MainForm.cs +++ b/ntfysh_client/MainForm.cs @@ -19,10 +19,29 @@ namespace ntfysh_client { _notificationListener = notificationListener; _notificationListener.OnNotificationReceive += OnNotificationReceive; + _notificationListener.OnConnectionMultiAttemptFailure += OnConnectionMultiAttemptFailure; + _notificationListener.OnConnectionCredentialsFailure += OnConnectionCredentialsFailure; InitializeComponent(); } + private void OnNotificationReceive(object sender, NotificationReceiveEventArgs e) + { + notifyIcon.ShowBalloonTip(3000, e.Title, e.Message, ToolTipIcon.Info); + } + + private void OnConnectionMultiAttemptFailure(NotificationListener sender, SubscribedTopic topic) + { + MessageBox.Show($"Connecting to topic ID '{topic.TopicId}' on server '{topic.ServerUrl}' failed after multiple attempts.\n\nThis topic ID will be ignored and you will not receive notifications for it until you restart the application.", "Connection Failure", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + + private void OnConnectionCredentialsFailure(NotificationListener sender, SubscribedTopic topic) + { + string reason = string.IsNullOrWhiteSpace(topic.Username) ? "credentials are required but were not provided" : "the entered credentials are incorrect"; + + MessageBox.Show($"Connecting to topic ID '{topic.TopicId}' on server '{topic.ServerUrl}' failed because {reason}.\n\nThis topic ID will be ignored and you will not receive notifications for it until you correct the credentials.", "Connection Authentication Failure", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + private void MainForm_Load(object sender, EventArgs e) => LoadTopics(); private void subscribeNewTopic_Click(object sender, EventArgs e) @@ -137,7 +156,7 @@ namespace ntfysh_client } //Assemble new format - List newTopics = legacyTopics.Select(lt => new SubscribedTopic(lt, "https://ntfy.sh", null, null, null, null)).ToList(); + List newTopics = legacyTopics.Select(lt => new SubscribedTopic(lt, "https://ntfy.sh", null, null)).ToList(); string newFormatSerialised = JsonConvert.SerializeObject(newTopics, Formatting.Indented); @@ -196,11 +215,6 @@ namespace ntfysh_client } } - private void OnNotificationReceive(object sender, NotificationReceiveEventArgs e) - { - notifyIcon.ShowBalloonTip(3000, e.Title, e.Message, ToolTipIcon.Info); - } - private void MainForm_FormClosed(object sender, FormClosedEventArgs e) { notifyIcon.Dispose(); diff --git a/ntfysh_client/NotificationListener.cs b/ntfysh_client/NotificationListener.cs index 9995599..bd90dde 100644 --- a/ntfysh_client/NotificationListener.cs +++ b/ntfysh_client/NotificationListener.cs @@ -15,124 +15,230 @@ using System.Web; namespace ntfysh_client { - public class NotificationListener : IDisposable + public class NotificationListener { - private readonly HttpClient _httpClient = new(); - private bool _isDisposed; - public readonly Dictionary SubscribedTopicsByUnique = new(); - public delegate void NotificationReceiveHandler(object sender, NotificationReceiveEventArgs e); + public delegate void NotificationReceiveHandler(NotificationListener sender, NotificationReceiveEventArgs e); public event NotificationReceiveHandler? OnNotificationReceive; + + public delegate void ConnectionErrorHandler(NotificationListener sender, SubscribedTopic topic); + public event ConnectionErrorHandler? OnConnectionMultiAttemptFailure; + public event ConnectionErrorHandler? OnConnectionCredentialsFailure; public NotificationListener() { - _httpClient.Timeout = TimeSpan.FromMilliseconds(Timeout.Infinite); ServicePointManager.DefaultConnectionLimit = 100; } - private async Task ListenToTopicWithHttpLongJsonAsync(HttpRequestMessage message, CancellationToken cancellationToken) + private async Task ListenToTopicWithHttpLongJsonAsync(HttpRequestMessage message, CancellationToken cancellationToken, SubscribedTopic topic) { - if (_isDisposed) throw new ObjectDisposedException(nameof(NotificationListener)); + int connectionAttempts = 0; while (!cancellationToken.IsCancellationRequested) { - using HttpResponseMessage response = await _httpClient.SendAsync(message, HttpCompletionOption.ResponseHeadersRead, cancellationToken); - await using Stream body = await response.Content.ReadAsStreamAsync(cancellationToken); - + //See if we have exceeded maximum attempts + if (connectionAttempts >= 10) + { + //10 connection failures (1 initial + 9 reattempts)! Do not retry + OnConnectionMultiAttemptFailure?.Invoke(this, topic); + return; + } + try { - StringBuilder mainBuffer = new(); + //Establish connection + using HttpClient client = new(); + client.Timeout = TimeSpan.FromMilliseconds(Timeout.Infinite); //This will not prevent us from failing to connect, luckily + using HttpResponseMessage response = await client.SendAsync(message, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + await using Stream body = await response.Content.ReadAsStreamAsync(cancellationToken); + + //Ensure successful connect + response.EnsureSuccessStatusCode(); + + //Reset connection attempts after a successful connect + connectionAttempts = 0; + + //Begin listening + StringBuilder mainBuffer = new(); + while (!cancellationToken.IsCancellationRequested) { //Read as much as possible byte[] buffer = new byte[8192]; int readBytes = await body.ReadAsync(buffer, 0, buffer.Length, cancellationToken); - + //Append it to our main buffer mainBuffer.Append(Encoding.UTF8.GetString(buffer, 0, readBytes)); - + List lines = mainBuffer.ToString().Split('\n').ToList(); //If we have not yet received a full line, meaning theres only 1 part, go back to reading if (lines.Count <= 1) continue; - + //We now have at least 1 line! Count how many full lines. There will always be a partial line at the end, even if that partial line is empty //Separate the partial line from the full lines int partialLineIndex = lines.Count - 1; string partialLine = lines[partialLineIndex]; lines.RemoveAt(partialLineIndex); - + //Process the full lines foreach (string line in lines) ProcessMessage(line); - + //Write back the partial line mainBuffer.Clear(); mainBuffer.Append(partialLine); } } - catch (Exception ex) + catch (HttpRequestException hre) { + if (hre.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden) + { + //Our credentials either aren't present when they need to be or are invalid + + //Credential Failure! Do not retry + OnConnectionCredentialsFailure?.Invoke(this, topic); + return; + } + #if DEBUG - Debug.WriteLine(ex); + Debug.WriteLine(hre); #endif - //Fall back to the outer loop to restart the listen, or cancel if requested + //We will not hit the finally block which will increment the connection failure counter and attempt a reconnect if applicable + } + catch (Exception e) + { + #if DEBUG + Debug.WriteLine(e); + #endif + + //We will not hit the finally block which will increment the connection failure counter and attempt a reconnect if applicable + } + finally + { + //We land here if we fail to connect or our connection gets closed (and if we are canceeling, but that gets ignored) + + if (!cancellationToken.IsCancellationRequested) + { + //Not cancelling, legitimate connection failure or termination + + if (connectionAttempts != 0) + { + //On our first reconnect attempt, try instantly. On consecutive, wait 3 seconds before each attempt + await Task.Delay(TimeSpan.FromSeconds(3), cancellationToken); + } + + //Increment attempts + connectionAttempts++; + + //Proceed to reattempt + } } } } - private async Task ListenToTopicWithWebsocketAsync(Uri uri, NetworkCredential credentials, CancellationToken cancellationToken) + private async Task ListenToTopicWithWebsocketAsync(Uri uri, NetworkCredential credentials, CancellationToken cancellationToken, SubscribedTopic topic) { - if (_isDisposed) throw new ObjectDisposedException(nameof(NotificationListener)); + int connectionAttempts = 0; while (!cancellationToken.IsCancellationRequested) { - using ClientWebSocket socket = new(); - socket.Options.Credentials = credentials; - + //See if we have exceeded maximum attempts + if (connectionAttempts >= 10) + { + //10 connection failures (1 initial + 9 reattempts)! Do not retry + OnConnectionMultiAttemptFailure?.Invoke(this, topic); + return; + } + try { - StringBuilder mainBuffer = new(); + //Establish connection + using ClientWebSocket socket = new(); + socket.Options.Credentials = credentials; await socket.ConnectAsync(uri, cancellationToken); + //Reset connection attempts after a successful connect + connectionAttempts = 0; + + //Begin listening + StringBuilder mainBuffer = new(); + while (!cancellationToken.IsCancellationRequested) { //Read as much as possible byte[] buffer = new byte[8192]; WebSocketReceiveResult? result = await socket.ReceiveAsync(new ArraySegment(buffer), cancellationToken); - + //Append it to our main buffer mainBuffer.Append(Encoding.UTF8.GetString(buffer, 0, result.Count)); - + List lines = mainBuffer.ToString().Split('\n').ToList(); //If we have not yet received a full line, meaning theres only 1 part, go back to reading if (lines.Count <= 1) continue; - + //We now have at least 1 line! Count how many full lines. There will always be a partial line at the end, even if that partial line is empty //Separate the partial line from the full lines int partialLineIndex = lines.Count - 1; string partialLine = lines[partialLineIndex]; lines.RemoveAt(partialLineIndex); - + //Process the full lines foreach (string line in lines) ProcessMessage(line); - + //Write back the partial line mainBuffer.Clear(); mainBuffer.Append(partialLine); } } - catch (Exception ex) + catch (WebSocketException wse) { + if (wse.WebSocketErrorCode is WebSocketError.NotAWebSocket) + { + //We haven't achieved a connection with a websocket. TODO Seems ntfy doesn't report unauthorised properly, and responds 200 + + //Credential Failure! Do not retry + OnConnectionCredentialsFailure?.Invoke(this, topic); + return; + } + #if DEBUG - Debug.WriteLine(ex); + Debug.WriteLine(wse); #endif - //Fall back to the outer loop to restart the listen, or cancel if requested + //We will not hit the finally block which will increment the connection failure counter and attempt a reconnect if applicable + } + catch (Exception e) + { + #if DEBUG + Debug.WriteLine(e); + #endif + + //We will not hit the finally block which will increment the connection failure counter and attempt a reconnect if applicable + } + finally + { + //We land here if we fail to connect or our connection gets closed (and if we are canceeling, but that gets ignored) + + if (!cancellationToken.IsCancellationRequested) + { + //Not cancelling, legitimate connection failure or termination + + if (connectionAttempts != 0) + { + //On our first reconnect attempt, try instantly. On consecutive, wait 3 seconds before each attempt + await Task.Delay(TimeSpan.FromSeconds(3), cancellationToken); + } + + //Increment attempts + connectionAttempts++; + + //Proceed to reattempt + } } } } @@ -156,8 +262,6 @@ namespace ntfysh_client public void SubscribeToTopicUsingLongHttpJson(string unique, string topicId, string serverUrl, string? username, string? password) { - if (_isDisposed) throw new ObjectDisposedException(nameof(NotificationListener)); - if (SubscribedTopicsByUnique.ContainsKey(unique)) throw new InvalidOperationException("A topic with this unique already exists"); if (string.IsNullOrWhiteSpace(username)) username = null; @@ -172,31 +276,35 @@ namespace ntfysh_client message.Headers.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(boundCredentialsBytes)); } - CancellationTokenSource listenCanceller = new(); - Task listenTask = ListenToTopicWithHttpLongJsonAsync(message, listenCanceller.Token); + SubscribedTopic newTopic = new(topicId, serverUrl, username, password); - SubscribedTopicsByUnique.Add(unique, new SubscribedTopic(topicId, serverUrl, username, password, listenTask, listenCanceller)); + CancellationTokenSource listenCanceller = new(); + Task listenTask = ListenToTopicWithHttpLongJsonAsync(message, listenCanceller.Token, newTopic); + + newTopic.SetAssociatedRunner(listenTask, listenCanceller); + + SubscribedTopicsByUnique.Add(unique, newTopic); } public void SubscribeToTopicUsingWebsocket(string unique, string topicId, string serverUrl, string? username, string? password) { - if (_isDisposed) throw new ObjectDisposedException(nameof(NotificationListener)); - if (SubscribedTopicsByUnique.ContainsKey(unique)) throw new InvalidOperationException("A topic with this unique already exists"); if (string.IsNullOrWhiteSpace(username)) username = null; if (string.IsNullOrWhiteSpace(password)) password = null; - Uri targetUri = new($"{serverUrl}/{HttpUtility.UrlEncode(topicId)}/ws"); + SubscribedTopic newTopic = new(topicId, serverUrl, username, password); + CancellationTokenSource listenCanceller = new(); - Task listenTask = ListenToTopicWithWebsocketAsync(targetUri, new NetworkCredential(username, password), listenCanceller.Token); - SubscribedTopicsByUnique.Add(unique, new SubscribedTopic(topicId, serverUrl, username, password, listenTask, listenCanceller)); + Task listenTask = ListenToTopicWithWebsocketAsync(new Uri($"{serverUrl}/{HttpUtility.UrlEncode(topicId)}/ws"), new NetworkCredential(username, password), listenCanceller.Token, newTopic); + + newTopic.SetAssociatedRunner(listenTask, listenCanceller); + + SubscribedTopicsByUnique.Add(unique, newTopic); } public async Task UnsubscribeFromTopicAsync(string topicUniqueString) { - if (_isDisposed) throw new ObjectDisposedException(nameof(NotificationListener)); - #if DEBUG Debug.WriteLine($"Removing topic {topicUniqueString}"); #endif @@ -208,12 +316,12 @@ namespace ntfysh_client if (!SubscribedTopicsByUnique.TryGetValue(topicUniqueString, out topic!)) return; //Cancel and dispose the task runner - topic.RunnerCanceller.Cancel(); + topic.RunnerCanceller?.Cancel(); //Wait for the task runner to shut down try { - await topic.Runner; + if (topic.Runner is not null) await topic.Runner; } catch (Exception) { @@ -221,19 +329,10 @@ namespace ntfysh_client } //Dispose task - topic.Runner.Dispose(); + topic.Runner?.Dispose(); //Remove the old topic SubscribedTopicsByUnique.Remove(topicUniqueString); } - - public void Dispose() - { - if (_isDisposed) return; - - _httpClient.Dispose(); - - _isDisposed = true; - } } } diff --git a/ntfysh_client/SubscribedTopic.cs b/ntfysh_client/SubscribedTopic.cs index f5b172b..b0593e1 100644 --- a/ntfysh_client/SubscribedTopic.cs +++ b/ntfysh_client/SubscribedTopic.cs @@ -1,4 +1,5 @@ -using System.Threading; +using System; +using System.Threading; using System.Threading.Tasks; using Newtonsoft.Json; @@ -6,12 +7,18 @@ namespace ntfysh_client { public class SubscribedTopic { - public SubscribedTopic(string topicId, string serverUrl, string? username, string? password, Task runner, CancellationTokenSource runnerCanceller) + public SubscribedTopic(string topicId, string serverUrl, string? username, string? password) { TopicId = topicId; ServerUrl = serverUrl; Username = username; Password = password; + } + + public void SetAssociatedRunner(Task runner, CancellationTokenSource runnerCanceller) + { + if (Runner is not null || RunnerCanceller is not null) throw new InvalidOperationException("Runner already associated"); + Runner = runner; RunnerCanceller = runnerCanceller; } @@ -22,9 +29,9 @@ namespace ntfysh_client public string? Password { get; } [JsonIgnore] - public Task Runner { get; } + public Task? Runner { get; private set; } [JsonIgnore] - public CancellationTokenSource RunnerCanceller { get; } + public CancellationTokenSource? RunnerCanceller { get; private set; } } } \ No newline at end of file From 2b298d6e190a2cf346eec08d16b411cea176a661 Mon Sep 17 00:00:00 2001 From: Alexander Horner <33007665+alexhorner@users.noreply.github.com> Date: Wed, 7 Dec 2022 23:28:09 +0000 Subject: [PATCH 13/14] Fix credentials on websocket --- ntfysh_client/NotificationListener.cs | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/ntfysh_client/NotificationListener.cs b/ntfysh_client/NotificationListener.cs index bd90dde..5e4d58c 100644 --- a/ntfysh_client/NotificationListener.cs +++ b/ntfysh_client/NotificationListener.cs @@ -140,7 +140,7 @@ namespace ntfysh_client } } - private async Task ListenToTopicWithWebsocketAsync(Uri uri, NetworkCredential credentials, CancellationToken cancellationToken, SubscribedTopic topic) + private async Task ListenToTopicWithWebsocketAsync(Uri uri, string? credentials, CancellationToken cancellationToken, SubscribedTopic topic) { int connectionAttempts = 0; @@ -158,8 +158,9 @@ namespace ntfysh_client { //Establish connection using ClientWebSocket socket = new(); - socket.Options.Credentials = credentials; - + + if (!string.IsNullOrWhiteSpace(credentials)) socket.Options.SetRequestHeader("Authorization", "Basic " + credentials); + await socket.ConnectAsync(uri, cancellationToken); //Reset connection attempts after a successful connect @@ -269,7 +270,7 @@ namespace ntfysh_client HttpRequestMessage message = new HttpRequestMessage(HttpMethod.Get, $"{serverUrl}/{HttpUtility.UrlEncode(topicId)}/json"); - if (username != null && password != null) + if (username is not null && password is not null) { byte[] boundCredentialsBytes = Encoding.UTF8.GetBytes($"{username}:{password}"); @@ -294,9 +295,18 @@ namespace ntfysh_client if (string.IsNullOrWhiteSpace(password)) password = null; SubscribedTopic newTopic = new(topicId, serverUrl, username, password); + + string? credentials = null; + + if (username is not null && password is not null) + { + byte[] boundCredentialsBytes = Encoding.UTF8.GetBytes($"{username}:{password}"); + + credentials = Convert.ToBase64String(boundCredentialsBytes); + } CancellationTokenSource listenCanceller = new(); - Task listenTask = ListenToTopicWithWebsocketAsync(new Uri($"{serverUrl}/{HttpUtility.UrlEncode(topicId)}/ws"), new NetworkCredential(username, password), listenCanceller.Token, newTopic); + Task listenTask = ListenToTopicWithWebsocketAsync(new Uri($"{serverUrl}/{HttpUtility.UrlEncode(topicId)}/ws"), credentials, listenCanceller.Token, newTopic); newTopic.SetAssociatedRunner(listenTask, listenCanceller); From 2a8a7e3c0c17f4582cd58258831ba96bfffedf0e Mon Sep 17 00:00:00 2001 From: Alexander Horner <33007665+alexhorner@users.noreply.github.com> Date: Wed, 7 Dec 2022 23:31:13 +0000 Subject: [PATCH 14/14] Adjust subscribe window tab index --- ntfysh_client/SubscribeDialog.Designer.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/ntfysh_client/SubscribeDialog.Designer.cs b/ntfysh_client/SubscribeDialog.Designer.cs index 4977b26..9f1e66c 100644 --- a/ntfysh_client/SubscribeDialog.Designer.cs +++ b/ntfysh_client/SubscribeDialog.Designer.cs @@ -55,7 +55,7 @@ namespace ntfysh_client this.panel1.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); this.panel1.Name = "panel1"; this.panel1.Size = new System.Drawing.Size(346, 51); - this.panel1.TabIndex = 0; + this.panel1.TabIndex = 8; // // button2 // @@ -64,7 +64,7 @@ namespace ntfysh_client this.button2.Margin = new System.Windows.Forms.Padding(12, 12, 4, 12); this.button2.Name = "button2"; this.button2.Size = new System.Drawing.Size(88, 27); - this.button2.TabIndex = 1; + this.button2.TabIndex = 7; this.button2.Text = "Cancel"; this.button2.UseVisualStyleBackColor = true; this.button2.Click += new System.EventHandler(this.button2_Click); @@ -76,7 +76,7 @@ namespace ntfysh_client this.button1.Margin = new System.Windows.Forms.Padding(4, 12, 12, 12); this.button1.Name = "button1"; this.button1.Size = new System.Drawing.Size(88, 27); - this.button1.TabIndex = 2; + this.button1.TabIndex = 6; this.button1.Text = "Subscribe"; this.button1.UseVisualStyleBackColor = true; this.button1.Click += new System.EventHandler(this.button1_Click); @@ -99,7 +99,7 @@ namespace ntfysh_client this.topicId.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); this.topicId.Name = "topicId"; this.topicId.Size = new System.Drawing.Size(318, 23); - this.topicId.TabIndex = 0; + this.topicId.TabIndex = 1; this.topicId.KeyDown += new System.Windows.Forms.KeyEventHandler(this.topicId_KeyDown); // // serverUrl @@ -132,7 +132,7 @@ namespace ntfysh_client this.username.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); this.username.Name = "username"; this.username.Size = new System.Drawing.Size(318, 23); - this.username.TabIndex = 4; + this.username.TabIndex = 3; this.username.KeyDown += new System.Windows.Forms.KeyEventHandler(this.username_KeyDown); // // label3 @@ -153,7 +153,7 @@ namespace ntfysh_client this.password.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); this.password.Name = "password"; this.password.Size = new System.Drawing.Size(318, 23); - this.password.TabIndex = 6; + this.password.TabIndex = 4; this.password.UseSystemPasswordChar = true; this.password.KeyDown += new System.Windows.Forms.KeyEventHandler(this.password_KeyDown); // @@ -187,7 +187,7 @@ namespace ntfysh_client this.connectionType.Location = new System.Drawing.Point(14, 208); this.connectionType.Name = "connectionType"; this.connectionType.Size = new System.Drawing.Size(318, 23); - this.connectionType.TabIndex = 10; + this.connectionType.TabIndex = 5; this.connectionType.TextChanged += new System.EventHandler(this.connectionType_TextChanged); // // SubscribeDialog