Winify – Blame information for rev 69
?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 | |
67 | office | 90 | using var imageStream = await RetrieveGotifyApplicationImage(gotifyMessage.AppId, _cancellationToken); |
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); |
||
231 | if (_configuration.Proxy.Enable) |
||
59 | office | 232 | _webSocketSharp.SetProxy(_configuration.Proxy.Url, _configuration.Proxy.Username, |
233 | _configuration.Proxy.Password); |
||
50 | office | 234 | |
47 | office | 235 | if (!string.IsNullOrEmpty(_server.Username) && !string.IsNullOrEmpty(_server.Password)) |
236 | _webSocketSharp.SetCredentials(_server.Username, _server.Password, true); |
||
237 | |||
44 | office | 238 | if (_configuration.IgnoreSelfSignedCertificates) |
239 | _webSocketSharp.SslConfiguration.ServerCertificateValidationCallback += |
||
240 | (sender, certificate, chain, errors) => true; |
||
241 | |||
51 | office | 242 | _webSocketSharp.Log.Output = (logData, s) => |
243 | { |
||
244 | Log.Information($"WebSockets low level logging reported: {logData.Message}"); |
||
245 | }; |
||
246 | |||
44 | office | 247 | _webSocketSharp.OnMessage += WebSocketSharp_OnMessage; |
248 | _webSocketSharp.OnError += WebSocketSharp_OnError; |
||
249 | _webSocketSharp.OnOpen += WebSocketSharp_OnOpen; |
||
250 | _webSocketSharp.OnClose += WebSocketSharp_OnClose; |
||
59 | office | 251 | |
66 | office | 252 | _webSocketSharp.Connect(); |
253 | _heartBeatTask = HeartBeat(_cancellationToken); |
||
254 | |||
255 | if (_configuration.RetrievePastNotificationHours != 0) |
||
256 | { |
||
257 | _retrievePastMessagesTask = RetrievePastMessages(_cancellationToken); |
||
258 | } |
||
1 | office | 259 | } |
260 | |||
44 | office | 261 | private void WebSocketSharp_OnClose(object sender, CloseEventArgs e) |
262 | { |
||
59 | office | 263 | Log.Information( |
264 | $"WebSockets connection to server {_webSocketsUri.AbsoluteUri} closed with reason {e.Reason}"); |
||
44 | office | 265 | } |
1 | office | 266 | |
44 | office | 267 | private void WebSocketSharp_OnOpen(object sender, EventArgs e) |
268 | { |
||
51 | office | 269 | Log.Information($"WebSockets connection to server {_webSocketsUri.AbsoluteUri} is now open"); |
61 | office | 270 | |
271 | _webSocketsServerResponseScheduledContinuation.Schedule(TimeSpan.FromMinutes(1), OnUnresponsiveServer, _cancellationToken); |
||
44 | office | 272 | } |
1 | office | 273 | |
61 | office | 274 | private void OnUnresponsiveServer() |
275 | { |
||
67 | office | 276 | Log.Warning($"Server {_server.Name} has not responded in a long while..."); |
61 | office | 277 | } |
278 | |||
44 | office | 279 | private async void WebSocketSharp_OnError(object sender, ErrorEventArgs e) |
1 | office | 280 | { |
59 | office | 281 | Log.Error( |
282 | $"Connection to WebSockets server {_webSocketsUri.AbsoluteUri} terminated unexpectedly with message {e.Message}", |
||
283 | e.Exception); |
||
48 | office | 284 | |
44 | office | 285 | await Task.Delay(TimeSpan.FromSeconds(1), _cancellationToken); |
61 | office | 286 | |
51 | office | 287 | Log.Information($"Reconnecting to websocket server {_webSocketsUri.AbsoluteUri}"); |
18 | office | 288 | |
66 | office | 289 | await Stop().ContinueWith(task => Start(), CancellationToken.None); |
44 | office | 290 | } |
1 | office | 291 | |
44 | office | 292 | private async void WebSocketSharp_OnMessage(object sender, MessageEventArgs e) |
293 | { |
||
61 | office | 294 | if (e.IsPing) |
44 | office | 295 | { |
67 | office | 296 | Log.Information($"Server {_server.Name} sent PING message"); |
1 | office | 297 | |
61 | office | 298 | _webSocketsServerResponseScheduledContinuation.Schedule(TimeSpan.FromMinutes(1), OnUnresponsiveServer, _cancellationToken); |
44 | office | 299 | return; |
300 | } |
||
12 | office | 301 | |
66 | office | 302 | await _webSocketMessageBufferBlock.SendAsync(new GotifyConnectionData(e.RawData, _server), _cancellationToken); |
44 | office | 303 | } |
12 | office | 304 | |
66 | office | 305 | public async Task Stop() |
44 | office | 306 | { |
59 | office | 307 | |
66 | office | 308 | if (_webSocketSharp == null || _cancellationTokenSource == null) |
309 | { |
||
310 | return; |
||
311 | } |
||
312 | |||
59 | office | 313 | _cancellationTokenSource.Cancel(); |
66 | office | 314 | |
68 | office | 315 | if (_heartBeatTask != null) |
316 | { |
||
317 | await _heartBeatTask; |
||
318 | } |
||
66 | office | 319 | |
68 | office | 320 | if (_retrievePastMessagesTask != null) |
321 | { |
||
322 | await _retrievePastMessagesTask; |
||
323 | } |
||
324 | |||
66 | office | 325 | if (_webSocketSharp != null) |
326 | { |
||
327 | _webSocketSharp.OnMessage -= WebSocketSharp_OnMessage; |
||
328 | _webSocketSharp.OnError -= WebSocketSharp_OnError; |
||
329 | _webSocketSharp.OnOpen -= WebSocketSharp_OnOpen; |
||
330 | _webSocketSharp.OnClose -= WebSocketSharp_OnClose; |
||
331 | |||
332 | _webSocketSharp.Close(); |
||
333 | _webSocketSharp = null; |
||
334 | } |
||
335 | |||
336 | _cancellationTokenSource.Dispose(); |
||
337 | _cancellationTokenSource = null; |
||
44 | office | 338 | } |
339 | |||
340 | #endregion |
||
341 | |||
342 | #region Private Methods |
||
343 | |||
59 | office | 344 | private async Task RetrievePastMessages(CancellationToken cancellationToken) |
345 | { |
||
66 | office | 346 | var gotifyApplicationBufferBlock = new BufferBlock<GotifyConnectionApplication>(new DataflowBlockOptions { CancellationToken = cancellationToken }); |
347 | var gotifyApplicationActionBlock = new ActionBlock<GotifyConnectionApplication>(async gotifyConnectionApplication => |
||
59 | office | 348 | { |
66 | office | 349 | if (!Uri.TryCreate(_httpUri, $"application/{gotifyConnectionApplication.Application.Id}/message", out var combinedUri)) |
59 | office | 350 | { |
66 | office | 351 | Log.Error($"Could not get application message Uri {gotifyConnectionApplication.Application.Id}."); |
59 | office | 352 | |
61 | office | 353 | return; |
59 | office | 354 | } |
355 | |||
61 | office | 356 | try |
357 | { |
||
66 | office | 358 | using var messageStream = await _httpClient.GetStreamAsync(combinedUri); |
359 | using var streamReader = new StreamReader(messageStream); |
||
360 | using var jsonTextReader = new JsonTextReader(streamReader); |
||
361 | |||
67 | office | 362 | var gotifyMessageQuery = _jsonSerializer.Deserialize<GotifyMessageQuery>(jsonTextReader) ?? |
363 | throw new ArgumentNullException(); |
||
364 | |||
365 | if (gotifyMessageQuery.Messages == null) |
||
366 | { |
||
367 | Log.Warning("Invalid application messages deserialized deserialized."); |
||
368 | |||
369 | return; |
||
370 | } |
||
371 | |||
372 | foreach (var message in gotifyMessageQuery.Messages) |
||
373 | { |
||
374 | if (message.Date < |
||
375 | DateTime.Now - TimeSpan.FromHours(_configuration.RetrievePastNotificationHours)) |
||
376 | { |
||
377 | continue; |
||
378 | } |
||
379 | |||
380 | using var imageStream = await RetrieveGotifyApplicationImage(message.AppId, _cancellationToken); |
||
381 | if (imageStream == null || imageStream.Length == 0) |
||
382 | { |
||
383 | Log.Warning("Could not find any application image for notification."); |
||
384 | |||
385 | continue; |
||
386 | } |
||
387 | |||
388 | var image = new Bitmap(imageStream); |
||
389 | message.Server = gotifyConnectionApplication.Server; |
||
390 | |||
391 | GotifyMessage?.Invoke(this, |
||
392 | new GotifyMessageEventArgs(message, image)); |
||
393 | } |
||
61 | office | 394 | } |
66 | office | 395 | catch (HttpRequestException exception) |
61 | office | 396 | { |
66 | office | 397 | Log.Warning(exception, $"Could not get application {gotifyConnectionApplication.Application.Id}."); |
61 | office | 398 | } |
66 | office | 399 | catch (JsonSerializationException exception) |
400 | { |
||
67 | office | 401 | Log.Warning(exception, |
402 | $"Could not deserialize the message response for application {gotifyConnectionApplication.Application.Id}."); |
||
66 | office | 403 | } |
67 | office | 404 | catch (Exception exception) |
59 | office | 405 | { |
67 | office | 406 | Log.Warning(exception, "Generic failure."); |
59 | office | 407 | } |
61 | office | 408 | }, new ExecutionDataflowBlockOptions { CancellationToken = cancellationToken }); |
409 | |||
410 | gotifyApplicationBufferBlock.LinkTo(gotifyApplicationActionBlock, |
||
411 | new DataflowLinkOptions { PropagateCompletion = true }); |
||
412 | |||
66 | office | 413 | var tasks = new ConcurrentBag<Task>(); |
61 | office | 414 | await foreach (var application in RetrieveGotifyApplications(cancellationToken)) |
415 | { |
||
66 | office | 416 | var gotifyConnectionApplication = new GotifyConnectionApplication(_server, application); |
417 | tasks.Add(gotifyApplicationBufferBlock.SendAsync(gotifyConnectionApplication, cancellationToken)); |
||
59 | office | 418 | } |
61 | office | 419 | |
66 | office | 420 | await Task.WhenAll(tasks); |
61 | office | 421 | gotifyApplicationBufferBlock.Complete(); |
422 | await gotifyApplicationActionBlock.Completion; |
||
59 | office | 423 | } |
424 | |||
61 | office | 425 | private async Task HeartBeat(CancellationToken cancellationToken) |
44 | office | 426 | { |
427 | try |
||
428 | { |
||
429 | do |
||
430 | { |
||
61 | office | 431 | await Task.Delay(TimeSpan.FromMinutes(1), cancellationToken); |
432 | |||
433 | _webSocketsClientPingStopWatch.Restart(); |
||
434 | if (!_webSocketSharp.Ping()) |
||
435 | { |
||
67 | office | 436 | Log.Warning($"Server {_server.Name} did not respond to PING message."); |
61 | office | 437 | continue; |
438 | } |
||
439 | |||
440 | var delta = _webSocketsClientPingStopWatch.ElapsedMilliseconds; |
||
441 | |||
67 | office | 442 | Log.Information($"PING response latency for {_server.Name} is {delta}ms"); |
61 | office | 443 | |
444 | _webSocketsServerResponseScheduledContinuation.Schedule(TimeSpan.FromMinutes(1), OnUnresponsiveServer, _cancellationToken); |
||
1 | office | 445 | } while (!cancellationToken.IsCancellationRequested); |
446 | } |
||
59 | office | 447 | catch (Exception exception) when (exception is OperationCanceledException || |
448 | exception is ObjectDisposedException) |
||
1 | office | 449 | { |
450 | } |
||
44 | office | 451 | catch (Exception exception) |
1 | office | 452 | { |
67 | office | 453 | Log.Warning(exception, $"Heartbeat for server {_server.Name} has failed."); |
1 | office | 454 | } |
455 | } |
||
456 | |||
61 | office | 457 | private async IAsyncEnumerable<GotifyApplication> RetrieveGotifyApplications([EnumeratorCancellation] CancellationToken cancellationToken) |
59 | office | 458 | { |
66 | office | 459 | if (!Uri.TryCreate(_httpUri, "application", out var combinedUri)) |
59 | office | 460 | { |
66 | office | 461 | Log.Error($"No application URL could be built for {_server.Url}."); |
61 | office | 462 | |
463 | yield break; |
||
59 | office | 464 | } |
465 | |||
66 | office | 466 | GotifyApplication[] gotifyApplications; |
59 | office | 467 | try |
468 | { |
||
66 | office | 469 | using var messageStream = await _httpClient.GetStreamAsync(combinedUri); |
470 | using var streamReader = new StreamReader(messageStream); |
||
471 | using var jsonTextReader = new JsonTextReader(streamReader); |
||
59 | office | 472 | |
66 | office | 473 | gotifyApplications = _jsonSerializer.Deserialize<GotifyApplication[]>(jsonTextReader); |
59 | office | 474 | |
66 | office | 475 | if (gotifyApplications == null) |
476 | { |
||
477 | throw new ArgumentNullException(); |
||
478 | } |
||
44 | office | 479 | } |
66 | office | 480 | catch (Exception exception) |
44 | office | 481 | { |
66 | office | 482 | Log.Warning(exception,"Could not deserialize the list of applications from the server."); |
25 | office | 483 | |
61 | office | 484 | yield break; |
39 | office | 485 | } |
25 | office | 486 | |
39 | office | 487 | foreach (var application in gotifyApplications) |
488 | { |
||
61 | office | 489 | yield return application; |
490 | } |
||
491 | } |
||
492 | |||
493 | private async Task<Stream> RetrieveGotifyApplicationImage(int appId, CancellationToken cancellationToken) |
||
494 | { |
||
495 | await foreach (var application in RetrieveGotifyApplications(cancellationToken)) |
||
496 | { |
||
66 | office | 497 | if (application.Id != appId) |
498 | { |
||
499 | continue; |
||
500 | } |
||
25 | office | 501 | |
61 | office | 502 | if (!Uri.TryCreate(Path.Combine($"{_httpUri}", $"{application.Image}"), UriKind.Absolute, out var applicationImageUri)) |
25 | office | 503 | { |
51 | office | 504 | Log.Warning("Could not build URL path to application icon"); |
66 | office | 505 | |
39 | office | 506 | continue; |
507 | } |
||
25 | office | 508 | |
61 | office | 509 | try |
510 | { |
||
67 | office | 511 | var imageResponse = await _httpClient.GetStreamAsync(applicationImageUri); |
66 | office | 512 | |
513 | var memoryStream = new MemoryStream(); |
||
514 | |||
67 | office | 515 | await imageResponse.CopyToAsync(memoryStream); |
66 | office | 516 | |
517 | return memoryStream; |
||
61 | office | 518 | } |
519 | catch (Exception exception) |
||
520 | { |
||
67 | office | 521 | Log.Error(exception,"Could not retrieve application image."); |
61 | office | 522 | } |
66 | office | 523 | } |
61 | office | 524 | |
66 | office | 525 | return new MemoryStream(); |
526 | } |
||
25 | office | 527 | |
66 | office | 528 | #endregion |
44 | office | 529 | |
66 | office | 530 | private class GotifyConnectionApplication |
531 | { |
||
532 | public GotifyApplication Application { get; } |
||
533 | public Server Server { get; } |
||
61 | office | 534 | |
66 | office | 535 | public GotifyConnectionApplication(Server server, GotifyApplication application) |
536 | { |
||
537 | Server = server; |
||
538 | Application = application; |
||
25 | office | 539 | } |
66 | office | 540 | } |
25 | office | 541 | |
66 | office | 542 | private class GotifyConnectionData |
543 | { |
||
544 | public byte[] Payload { get; } |
||
545 | public Server Server { get; } |
||
546 | |||
547 | public GotifyConnectionData(byte[] payload, Server server) |
||
548 | { |
||
549 | Payload = payload; |
||
550 | Server = server; |
||
551 | } |
||
25 | office | 552 | } |
1 | office | 553 | } |
554 | } |