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