Winify – Blame information for rev 66
?pathlinks?
Rev | Author | Line No. | Line |
---|---|---|---|
1 | office | 1 | using System; |
66 | office | 2 | using System.Collections.Concurrent; |
61 | office | 3 | using System.Collections.Generic; |
4 | using System.Diagnostics; |
||
1 | office | 5 | using System.Drawing; |
61 | office | 6 | using System.Globalization; |
1 | office | 7 | using System.IO; |
61 | office | 8 | using System.Linq; |
50 | office | 9 | using System.Net; |
1 | office | 10 | using System.Net.Http; |
11 | using System.Net.Http.Headers; |
||
61 | office | 12 | using System.Runtime.Caching; |
13 | using System.Runtime.CompilerServices; |
||
50 | office | 14 | using System.Security.Authentication; |
46 | office | 15 | using System.Security.Cryptography.X509Certificates; |
1 | office | 16 | using System.Text; |
17 | using System.Threading; |
||
18 | using System.Threading.Tasks; |
||
61 | office | 19 | using System.Threading.Tasks.Dataflow; |
20 | using System.Windows.Forms; |
||
1 | office | 21 | using Newtonsoft.Json; |
66 | office | 22 | using Org.BouncyCastle.Asn1.Pkcs; |
18 | office | 23 | using Serilog; |
24 | office | 24 | using Servers; |
44 | office | 25 | using WebSocketSharp; |
50 | office | 26 | using WebSocketSharp.Net; |
61 | office | 27 | using Winify.Utilities; |
44 | office | 28 | using ErrorEventArgs = WebSocketSharp.ErrorEventArgs; |
50 | office | 29 | using NetworkCredential = System.Net.NetworkCredential; |
1 | office | 30 | |
31 | namespace Winify.Gotify |
||
32 | { |
||
33 | public class GotifyConnection : IDisposable |
||
34 | { |
||
35 | #region Public Events & Delegates |
||
36 | |||
37 | public event EventHandler<GotifyNotificationEventArgs> GotifyNotification; |
||
38 | |||
39 | #endregion |
||
40 | |||
41 | #region Private Delegates, Events, Enums, Properties, Indexers and Fields |
||
42 | |||
25 | office | 43 | private readonly Server _server; |
44 | |||
1 | office | 45 | private CancellationToken _cancellationToken; |
46 | |||
47 | private CancellationTokenSource _cancellationTokenSource; |
||
48 | |||
66 | office | 49 | private Task _heartBeatTask; |
1 | office | 50 | |
39 | office | 51 | private HttpClient _httpClient; |
52 | |||
53 | private readonly Uri _webSocketsUri; |
||
54 | |||
55 | private readonly Uri _httpUri; |
||
44 | office | 56 | private WebSocket _webSocketSharp; |
59 | office | 57 | private readonly Configuration.Configuration _configuration; |
61 | office | 58 | private IDisposable _tplRetrievePastMessagesLink; |
59 | private IDisposable _tplWebSocketsBufferBlockTransformLink; |
||
60 | private IDisposable _tplWebSocketsTransformActionLink; |
||
61 | private IDisposable _tplWebSocketsTransformActionNullLink; |
||
66 | office | 62 | private readonly BufferBlock<GotifyConnectionData> _webSocketMessageBufferBlock; |
61 | office | 63 | private readonly Stopwatch _webSocketsClientPingStopWatch; |
64 | private readonly ScheduledContinuation _webSocketsServerResponseScheduledContinuation; |
||
66 | office | 65 | |
66 | private Task _retrievePastMessagesTask; |
||
67 | private static JsonSerializer _jsonSerializer; |
||
39 | office | 68 | |
25 | office | 69 | #endregion |
1 | office | 70 | |
25 | office | 71 | #region Constructors, Destructors and Finalizers |
24 | office | 72 | |
44 | office | 73 | private GotifyConnection() |
24 | office | 74 | { |
66 | office | 75 | _jsonSerializer = new JsonSerializer(); |
61 | office | 76 | _webSocketsServerResponseScheduledContinuation = new ScheduledContinuation(); |
77 | _webSocketsClientPingStopWatch = new Stopwatch(); |
||
78 | |||
66 | office | 79 | _webSocketMessageBufferBlock = new BufferBlock<GotifyConnectionData>( |
80 | new DataflowBlockOptions |
||
81 | { |
||
82 | CancellationToken = _cancellationToken |
||
83 | }); |
||
84 | |||
85 | var webSocketTransformBlock = new TransformBlock<GotifyConnectionData, GotifyMessage>(data => |
||
61 | office | 86 | { |
66 | office | 87 | if (data.Payload == null || data.Payload.Length == 0) |
61 | office | 88 | { |
89 | return null; |
||
90 | } |
||
91 | |||
92 | GotifyMessage gotifyNotification; |
||
93 | |||
94 | try |
||
95 | { |
||
66 | office | 96 | var message = Encoding.UTF8.GetString(data.Payload, 0, data.Payload.Length); |
97 | |||
61 | office | 98 | gotifyNotification = JsonConvert.DeserializeObject<GotifyMessage>(message); |
99 | |||
66 | office | 100 | if (gotifyNotification == null) |
101 | { |
||
102 | throw new ArgumentNullException(); |
||
103 | } |
||
104 | |||
105 | gotifyNotification.Server = data.Server; |
||
61 | office | 106 | } |
66 | office | 107 | catch (Exception exception) |
61 | office | 108 | { |
66 | office | 109 | Log.Warning(exception, "Could not deserialize notification."); |
61 | office | 110 | |
111 | return null; |
||
112 | } |
||
113 | |||
114 | return gotifyNotification; |
||
115 | |||
116 | }, new ExecutionDataflowBlockOptions { CancellationToken = _cancellationToken }); |
||
117 | |||
118 | var webSocketActionBlock = new ActionBlock<GotifyMessage>(async message => |
||
119 | { |
||
66 | office | 120 | using var imageStream = await RetrieveGotifyApplicationImage(message.AppId, _cancellationToken); |
121 | if (imageStream == null || imageStream.Length == 0) |
||
61 | office | 122 | { |
66 | office | 123 | Log.Warning("Could not find any application image for notification."); |
64 | office | 124 | |
61 | office | 125 | return; |
126 | } |
||
127 | |||
66 | office | 128 | var image = new Bitmap(imageStream); |
61 | office | 129 | |
66 | office | 130 | GotifyNotification?.Invoke(this, |
131 | new GotifyNotificationEventArgs(message, image)); |
||
61 | office | 132 | |
133 | Log.Debug($"Notification message received: {message.Message}"); |
||
134 | |||
135 | }, new ExecutionDataflowBlockOptions { CancellationToken = _cancellationToken }); |
||
136 | |||
137 | _tplWebSocketsBufferBlockTransformLink = _webSocketMessageBufferBlock.LinkTo(webSocketTransformBlock, |
||
138 | new DataflowLinkOptions { PropagateCompletion = true }); |
||
139 | _tplWebSocketsTransformActionLink = webSocketTransformBlock.LinkTo(webSocketActionBlock, |
||
140 | new DataflowLinkOptions { PropagateCompletion = true }, message => message != null); |
||
141 | _tplWebSocketsTransformActionNullLink = webSocketTransformBlock.LinkTo(DataflowBlock.NullTarget<GotifyMessage>(), |
||
142 | new DataflowLinkOptions() { PropagateCompletion = true }); |
||
39 | office | 143 | } |
144 | |||
59 | office | 145 | public GotifyConnection(Server server, Configuration.Configuration configuration) : this() |
39 | office | 146 | { |
24 | office | 147 | _server = server; |
44 | office | 148 | _configuration = configuration; |
39 | office | 149 | |
54 | office | 150 | ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12; |
59 | office | 151 | var httpClientHandler = new HttpClientHandler |
50 | office | 152 | { |
54 | office | 153 | // mono does not implement this |
154 | //SslProtocols = SslProtocols.Tls12 |
||
50 | office | 155 | }; |
156 | |||
44 | office | 157 | _httpClient = new HttpClient(httpClientHandler); |
158 | if (_configuration.IgnoreSelfSignedCertificates) |
||
159 | httpClientHandler.ServerCertificateCustomValidationCallback = |
||
46 | office | 160 | (httpRequestMessage, cert, cetChain, policyErrors) => true; |
39 | office | 161 | |
50 | office | 162 | if (_configuration.Proxy.Enable) |
163 | httpClientHandler.Proxy = new WebProxy(_configuration.Proxy.Url, false, new string[] { }, |
||
164 | new NetworkCredential(_configuration.Proxy.Username, _configuration.Proxy.Password)); |
||
165 | |||
47 | office | 166 | _httpClient = new HttpClient(httpClientHandler); |
167 | if (!string.IsNullOrEmpty(_server.Username) && !string.IsNullOrEmpty(_server.Password)) |
||
168 | _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", |
||
169 | Convert.ToBase64String(Encoding.Default.GetBytes($"{_server.Username}:{_server.Password}"))); |
||
44 | office | 170 | |
47 | office | 171 | if (!Uri.TryCreate(_server.Url, UriKind.Absolute, out _httpUri)) |
39 | office | 172 | { |
51 | office | 173 | Log.Error($"No HTTP URL could be built out of the supplied server URI {_server.Url}"); |
47 | office | 174 | return; |
175 | } |
||
44 | office | 176 | |
47 | office | 177 | // Build the web sockets URI. |
178 | var webSocketsUriBuilder = new UriBuilder(_httpUri); |
||
179 | switch (webSocketsUriBuilder.Scheme.ToUpperInvariant()) |
||
180 | { |
||
181 | case "HTTP": |
||
182 | webSocketsUriBuilder.Scheme = "ws"; |
||
183 | break; |
||
184 | case "HTTPS": |
||
185 | webSocketsUriBuilder.Scheme = "wss"; |
||
186 | break; |
||
187 | } |
||
188 | |||
66 | office | 189 | if (!Uri.TryCreate(webSocketsUriBuilder.Uri, "stream", out var combinedUri)) |
47 | office | 190 | { |
66 | office | 191 | Log.Error($"No WebSockets URL could be built from the provided URL {_server.Url}."); |
39 | office | 192 | } |
47 | office | 193 | |
66 | office | 194 | _webSocketsUri = combinedUri; |
24 | office | 195 | } |
196 | |||
1 | office | 197 | public void Dispose() |
198 | { |
||
199 | if (_cancellationTokenSource != null) |
||
200 | { |
||
201 | _cancellationTokenSource.Dispose(); |
||
202 | _cancellationTokenSource = null; |
||
203 | } |
||
39 | office | 204 | |
61 | office | 205 | if (_tplWebSocketsBufferBlockTransformLink != null) |
206 | { |
||
207 | _tplWebSocketsBufferBlockTransformLink.Dispose(); |
||
208 | _tplWebSocketsBufferBlockTransformLink = null; |
||
209 | } |
||
210 | |||
211 | if (_tplWebSocketsTransformActionLink != null) |
||
212 | { |
||
213 | _tplWebSocketsTransformActionLink.Dispose(); |
||
214 | _tplWebSocketsTransformActionLink = null; |
||
215 | } |
||
216 | |||
217 | if (_tplWebSocketsTransformActionNullLink != null) |
||
218 | { |
||
219 | _tplWebSocketsTransformActionNullLink.Dispose(); |
||
220 | _tplWebSocketsTransformActionNullLink = null; |
||
221 | } |
||
222 | |||
223 | if (_tplRetrievePastMessagesLink != null) |
||
224 | { |
||
225 | _tplRetrievePastMessagesLink.Dispose(); |
||
226 | _tplRetrievePastMessagesLink = null; |
||
227 | } |
||
228 | |||
48 | office | 229 | if (_webSocketSharp != null) |
230 | { |
||
231 | _webSocketSharp.Close(); |
||
232 | _webSocketSharp = null; |
||
233 | } |
||
44 | office | 234 | |
48 | office | 235 | if (_httpClient != null) |
236 | { |
||
237 | _httpClient.Dispose(); |
||
238 | _httpClient = null; |
||
239 | } |
||
1 | office | 240 | } |
241 | |||
242 | #endregion |
||
243 | |||
244 | #region Public Methods |
||
245 | |||
25 | office | 246 | public void Start() |
1 | office | 247 | { |
47 | office | 248 | if (_webSocketsUri == null || _httpUri == null) |
249 | { |
||
51 | office | 250 | Log.Error("Could not start connection to server due to unreadable URLs"); |
47 | office | 251 | return; |
252 | } |
||
253 | |||
1 | office | 254 | _cancellationTokenSource = new CancellationTokenSource(); |
255 | _cancellationToken = _cancellationTokenSource.Token; |
||
256 | |||
66 | office | 257 | if (_webSocketSharp != null) |
59 | office | 258 | { |
66 | office | 259 | return; |
59 | office | 260 | } |
261 | |||
44 | office | 262 | _webSocketSharp = new WebSocket(_webSocketsUri.AbsoluteUri); |
61 | office | 263 | _webSocketSharp.EmitOnPing = true; |
264 | _webSocketSharp.WaitTime = TimeSpan.FromMinutes(1); |
||
50 | office | 265 | _webSocketSharp.SslConfiguration = new ClientSslConfiguration(_webSocketsUri.Host, |
266 | new X509CertificateCollection(new X509Certificate[] { }), SslProtocols.Tls12, false); |
||
267 | if (_configuration.Proxy.Enable) |
||
59 | office | 268 | _webSocketSharp.SetProxy(_configuration.Proxy.Url, _configuration.Proxy.Username, |
269 | _configuration.Proxy.Password); |
||
50 | office | 270 | |
47 | office | 271 | if (!string.IsNullOrEmpty(_server.Username) && !string.IsNullOrEmpty(_server.Password)) |
272 | _webSocketSharp.SetCredentials(_server.Username, _server.Password, true); |
||
273 | |||
44 | office | 274 | if (_configuration.IgnoreSelfSignedCertificates) |
275 | _webSocketSharp.SslConfiguration.ServerCertificateValidationCallback += |
||
276 | (sender, certificate, chain, errors) => true; |
||
277 | |||
51 | office | 278 | _webSocketSharp.Log.Output = (logData, s) => |
279 | { |
||
280 | Log.Information($"WebSockets low level logging reported: {logData.Message}"); |
||
281 | }; |
||
282 | |||
44 | office | 283 | _webSocketSharp.OnMessage += WebSocketSharp_OnMessage; |
284 | _webSocketSharp.OnError += WebSocketSharp_OnError; |
||
285 | _webSocketSharp.OnOpen += WebSocketSharp_OnOpen; |
||
286 | _webSocketSharp.OnClose += WebSocketSharp_OnClose; |
||
59 | office | 287 | |
66 | office | 288 | _webSocketSharp.Connect(); |
289 | _heartBeatTask = HeartBeat(_cancellationToken); |
||
290 | |||
291 | if (_configuration.RetrievePastNotificationHours != 0) |
||
292 | { |
||
293 | _retrievePastMessagesTask = RetrievePastMessages(_cancellationToken); |
||
294 | } |
||
1 | office | 295 | } |
296 | |||
44 | office | 297 | private void WebSocketSharp_OnClose(object sender, CloseEventArgs e) |
298 | { |
||
59 | office | 299 | Log.Information( |
300 | $"WebSockets connection to server {_webSocketsUri.AbsoluteUri} closed with reason {e.Reason}"); |
||
44 | office | 301 | } |
1 | office | 302 | |
44 | office | 303 | private void WebSocketSharp_OnOpen(object sender, EventArgs e) |
304 | { |
||
51 | office | 305 | Log.Information($"WebSockets connection to server {_webSocketsUri.AbsoluteUri} is now open"); |
61 | office | 306 | |
307 | _webSocketsServerResponseScheduledContinuation.Schedule(TimeSpan.FromMinutes(1), OnUnresponsiveServer, _cancellationToken); |
||
44 | office | 308 | } |
1 | office | 309 | |
61 | office | 310 | private void OnUnresponsiveServer() |
311 | { |
||
312 | Log.Warning($"Server {_server} has not responded in a long while..."); |
||
313 | } |
||
314 | |||
44 | office | 315 | private async void WebSocketSharp_OnError(object sender, ErrorEventArgs e) |
1 | office | 316 | { |
59 | office | 317 | Log.Error( |
318 | $"Connection to WebSockets server {_webSocketsUri.AbsoluteUri} terminated unexpectedly with message {e.Message}", |
||
319 | e.Exception); |
||
48 | office | 320 | |
44 | office | 321 | await Task.Delay(TimeSpan.FromSeconds(1), _cancellationToken); |
61 | office | 322 | |
51 | office | 323 | Log.Information($"Reconnecting to websocket server {_webSocketsUri.AbsoluteUri}"); |
18 | office | 324 | |
66 | office | 325 | await Stop().ContinueWith(task => Start(), CancellationToken.None); |
44 | office | 326 | } |
1 | office | 327 | |
44 | office | 328 | private async void WebSocketSharp_OnMessage(object sender, MessageEventArgs e) |
329 | { |
||
61 | office | 330 | if (e.IsPing) |
44 | office | 331 | { |
61 | office | 332 | Log.Information($"Server {_server} sent PING message"); |
1 | office | 333 | |
61 | office | 334 | _webSocketsServerResponseScheduledContinuation.Schedule(TimeSpan.FromMinutes(1), OnUnresponsiveServer, _cancellationToken); |
44 | office | 335 | return; |
336 | } |
||
12 | office | 337 | |
66 | office | 338 | await _webSocketMessageBufferBlock.SendAsync(new GotifyConnectionData(e.RawData, _server), _cancellationToken); |
44 | office | 339 | } |
12 | office | 340 | |
66 | office | 341 | public async Task Stop() |
44 | office | 342 | { |
59 | office | 343 | |
66 | office | 344 | if (_webSocketSharp == null || _cancellationTokenSource == null) |
345 | { |
||
346 | return; |
||
347 | } |
||
348 | |||
59 | office | 349 | _cancellationTokenSource.Cancel(); |
66 | office | 350 | |
351 | await _heartBeatTask; |
||
352 | await _retrievePastMessagesTask; |
||
353 | |||
354 | if (_webSocketSharp != null) |
||
355 | { |
||
356 | _webSocketSharp.OnMessage -= WebSocketSharp_OnMessage; |
||
357 | _webSocketSharp.OnError -= WebSocketSharp_OnError; |
||
358 | _webSocketSharp.OnOpen -= WebSocketSharp_OnOpen; |
||
359 | _webSocketSharp.OnClose -= WebSocketSharp_OnClose; |
||
360 | |||
361 | _webSocketSharp.Close(); |
||
362 | _webSocketSharp = null; |
||
363 | } |
||
364 | |||
365 | _cancellationTokenSource.Dispose(); |
||
366 | _cancellationTokenSource = null; |
||
44 | office | 367 | } |
368 | |||
369 | #endregion |
||
370 | |||
371 | #region Private Methods |
||
372 | |||
59 | office | 373 | private async Task RetrievePastMessages(CancellationToken cancellationToken) |
374 | { |
||
66 | office | 375 | var gotifyApplicationBufferBlock = new BufferBlock<GotifyConnectionApplication>(new DataflowBlockOptions { CancellationToken = cancellationToken }); |
376 | var gotifyApplicationActionBlock = new ActionBlock<GotifyConnectionApplication>(async gotifyConnectionApplication => |
||
59 | office | 377 | { |
66 | office | 378 | if (!Uri.TryCreate(_httpUri, $"application/{gotifyConnectionApplication.Application.Id}/message", out var combinedUri)) |
59 | office | 379 | { |
66 | office | 380 | Log.Error($"Could not get application message Uri {gotifyConnectionApplication.Application.Id}."); |
59 | office | 381 | |
61 | office | 382 | return; |
59 | office | 383 | } |
384 | |||
66 | office | 385 | GotifyMessageQuery gotifyMessageQuery; |
61 | office | 386 | try |
387 | { |
||
66 | office | 388 | using var messageStream = await _httpClient.GetStreamAsync(combinedUri); |
389 | using var streamReader = new StreamReader(messageStream); |
||
390 | using var jsonTextReader = new JsonTextReader(streamReader); |
||
391 | |||
392 | gotifyMessageQuery = _jsonSerializer.Deserialize<GotifyMessageQuery>(jsonTextReader); |
||
61 | office | 393 | } |
66 | office | 394 | catch (HttpRequestException exception) |
61 | office | 395 | { |
66 | office | 396 | Log.Warning(exception, $"Could not get application {gotifyConnectionApplication.Application.Id}."); |
59 | office | 397 | |
61 | office | 398 | return; |
399 | } |
||
66 | office | 400 | catch (JsonSerializationException exception) |
401 | { |
||
402 | Log.Warning(exception,$"Could not deserialize the message response for application {gotifyConnectionApplication.Application.Id}."); |
||
59 | office | 403 | |
66 | office | 404 | return; |
405 | } |
||
61 | office | 406 | |
66 | office | 407 | if (gotifyMessageQuery == null || gotifyMessageQuery.Messages == null) |
59 | office | 408 | { |
66 | office | 409 | Log.Warning("Invalid application messages deserialized deserialized."); |
59 | office | 410 | |
61 | office | 411 | return; |
59 | office | 412 | } |
413 | |||
66 | office | 414 | foreach (var message in gotifyMessageQuery.Messages) |
59 | office | 415 | { |
66 | office | 416 | if (message.Date < DateTime.Now - TimeSpan.FromHours(_configuration.RetrievePastNotificationHours)) |
61 | office | 417 | { |
66 | office | 418 | continue; |
61 | office | 419 | } |
66 | office | 420 | |
61 | office | 421 | using var imageStream = await RetrieveGotifyApplicationImage(message.AppId, _cancellationToken); |
422 | if (imageStream == null || imageStream.Length == 0) |
||
423 | { |
||
66 | office | 424 | Log.Warning("Could not find any application image for notification."); |
425 | |||
59 | office | 426 | continue; |
61 | office | 427 | } |
59 | office | 428 | |
66 | office | 429 | var image = new Bitmap(imageStream); |
430 | message.Server = gotifyConnectionApplication.Server; |
||
59 | office | 431 | |
61 | office | 432 | GotifyNotification?.Invoke(this, |
433 | new GotifyNotificationEventArgs(message, image)); |
||
434 | } |
||
59 | office | 435 | |
61 | office | 436 | }, new ExecutionDataflowBlockOptions { CancellationToken = cancellationToken }); |
437 | |||
438 | gotifyApplicationBufferBlock.LinkTo(gotifyApplicationActionBlock, |
||
439 | new DataflowLinkOptions { PropagateCompletion = true }); |
||
440 | |||
66 | office | 441 | var tasks = new ConcurrentBag<Task>(); |
61 | office | 442 | await foreach (var application in RetrieveGotifyApplications(cancellationToken)) |
443 | { |
||
66 | office | 444 | var gotifyConnectionApplication = new GotifyConnectionApplication(_server, application); |
445 | tasks.Add(gotifyApplicationBufferBlock.SendAsync(gotifyConnectionApplication, cancellationToken)); |
||
59 | office | 446 | } |
61 | office | 447 | |
66 | office | 448 | await Task.WhenAll(tasks); |
61 | office | 449 | gotifyApplicationBufferBlock.Complete(); |
450 | await gotifyApplicationActionBlock.Completion; |
||
451 | |||
59 | office | 452 | } |
453 | |||
61 | office | 454 | private async Task HeartBeat(CancellationToken cancellationToken) |
44 | office | 455 | { |
456 | try |
||
457 | { |
||
458 | do |
||
459 | { |
||
61 | office | 460 | await Task.Delay(TimeSpan.FromMinutes(1), cancellationToken); |
461 | |||
462 | _webSocketsClientPingStopWatch.Restart(); |
||
463 | if (!_webSocketSharp.Ping()) |
||
464 | { |
||
465 | Log.Warning($"Server {_server} did not respond to PING message."); |
||
466 | continue; |
||
467 | } |
||
468 | |||
469 | var delta = _webSocketsClientPingStopWatch.ElapsedMilliseconds; |
||
470 | |||
471 | Log.Information($"PING response latency for {_server} is {delta}ms"); |
||
472 | |||
473 | _webSocketsServerResponseScheduledContinuation.Schedule(TimeSpan.FromMinutes(1), OnUnresponsiveServer, _cancellationToken); |
||
1 | office | 474 | } while (!cancellationToken.IsCancellationRequested); |
475 | } |
||
59 | office | 476 | catch (Exception exception) when (exception is OperationCanceledException || |
477 | exception is ObjectDisposedException) |
||
1 | office | 478 | { |
479 | } |
||
44 | office | 480 | catch (Exception exception) |
1 | office | 481 | { |
61 | office | 482 | Log.Warning(exception, $"Heartbeat for server {_server} has failed due to {exception.Message}"); |
1 | office | 483 | } |
484 | } |
||
485 | |||
61 | office | 486 | private async IAsyncEnumerable<GotifyApplication> RetrieveGotifyApplications([EnumeratorCancellation] CancellationToken cancellationToken) |
59 | office | 487 | { |
66 | office | 488 | if (!Uri.TryCreate(_httpUri, "application", out var combinedUri)) |
59 | office | 489 | { |
66 | office | 490 | Log.Error($"No application URL could be built for {_server.Url}."); |
61 | office | 491 | |
492 | yield break; |
||
59 | office | 493 | } |
494 | |||
66 | office | 495 | GotifyApplication[] gotifyApplications; |
59 | office | 496 | try |
497 | { |
||
66 | office | 498 | using var messageStream = await _httpClient.GetStreamAsync(combinedUri); |
499 | using var streamReader = new StreamReader(messageStream); |
||
500 | using var jsonTextReader = new JsonTextReader(streamReader); |
||
59 | office | 501 | |
66 | office | 502 | gotifyApplications = _jsonSerializer.Deserialize<GotifyApplication[]>(jsonTextReader); |
59 | office | 503 | |
66 | office | 504 | if (gotifyApplications == null) |
505 | { |
||
506 | throw new ArgumentNullException(); |
||
507 | } |
||
44 | office | 508 | } |
66 | office | 509 | catch (Exception exception) |
44 | office | 510 | { |
66 | office | 511 | Log.Warning(exception,"Could not deserialize the list of applications from the server."); |
25 | office | 512 | |
61 | office | 513 | yield break; |
39 | office | 514 | } |
25 | office | 515 | |
39 | office | 516 | foreach (var application in gotifyApplications) |
517 | { |
||
61 | office | 518 | yield return application; |
519 | } |
||
520 | } |
||
521 | |||
522 | private async Task<Stream> RetrieveGotifyApplicationImage(int appId, CancellationToken cancellationToken) |
||
523 | { |
||
524 | await foreach (var application in RetrieveGotifyApplications(cancellationToken)) |
||
525 | { |
||
66 | office | 526 | if (application.Id != appId) |
527 | { |
||
528 | continue; |
||
529 | } |
||
25 | office | 530 | |
61 | office | 531 | if (!Uri.TryCreate(Path.Combine($"{_httpUri}", $"{application.Image}"), UriKind.Absolute, out var applicationImageUri)) |
25 | office | 532 | { |
51 | office | 533 | Log.Warning("Could not build URL path to application icon"); |
66 | office | 534 | |
39 | office | 535 | continue; |
536 | } |
||
25 | office | 537 | |
61 | office | 538 | try |
539 | { |
||
66 | office | 540 | var imageResponse = await _httpClient.GetAsync(applicationImageUri, cancellationToken); |
541 | |||
542 | var memoryStream = new MemoryStream(); |
||
543 | |||
544 | await imageResponse.Content.CopyToAsync(memoryStream); |
||
545 | |||
546 | return memoryStream; |
||
61 | office | 547 | } |
548 | catch (Exception exception) |
||
549 | { |
||
550 | Log.Error($"Could not retrieve application image: {exception.Message}"); |
||
551 | } |
||
66 | office | 552 | } |
61 | office | 553 | |
66 | office | 554 | return new MemoryStream(); |
555 | } |
||
25 | office | 556 | |
66 | office | 557 | #endregion |
44 | office | 558 | |
66 | office | 559 | private class GotifyConnectionApplication |
560 | { |
||
561 | public GotifyApplication Application { get; } |
||
562 | public Server Server { get; } |
||
61 | office | 563 | |
66 | office | 564 | public GotifyConnectionApplication(Server server, GotifyApplication application) |
565 | { |
||
566 | Server = server; |
||
567 | Application = application; |
||
25 | office | 568 | } |
66 | office | 569 | } |
25 | office | 570 | |
66 | office | 571 | private class GotifyConnectionData |
572 | { |
||
573 | public byte[] Payload { get; } |
||
574 | public Server Server { get; } |
||
575 | |||
576 | public GotifyConnectionData(byte[] payload, Server server) |
||
577 | { |
||
578 | Payload = payload; |
||
579 | Server = server; |
||
580 | } |
||
25 | office | 581 | } |
1 | office | 582 | } |
583 | } |