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