private bool ValidateInaraCredentials()
        {
            CommunityGoalsOptions options = _options.CurrentValue;

            return(!string.IsNullOrWhiteSpace(options.InaraApiKey) &&
                   !string.IsNullOrWhiteSpace(options.InaraAppName));
        }
        private async Task <IEnumerable <CommunityGoal> > QueryCommunityGoalsAsync(CancellationToken cancellationToken = default)
        {
            CommunityGoalsOptions options = _options.CurrentValue;

            // use cache if it's too early for retrieving again
            if ((DateTime.UtcNow - _cacheUpdateTimeUtc) < options.CacheLifetime)
            {
                _log.LogTrace("CG cache is recent, not updating");
                return(_cgCache);
            }

            // build query content
            const string eventName = "getCommunityGoalsRecent";
            JObject      query     = new JObject();

            query.Add("header", new JObject(
                          new JProperty("appName", options.InaraAppName),
                          new JProperty("appVersion", options.InaraAppVersion ?? BotInfoUtility.GetVersion()),
                          new JProperty("isDeveloped", options.InaraAppInDevelopment),
                          new JProperty("APIkey", options.InaraApiKey)));
            JObject eventParams = new JObject(
                new JProperty("eventName", eventName),
                new JProperty("eventTimestamp", DateTimeOffset.Now),
                new JProperty("eventData", new JArray()));

            query.Add("events", new JArray(eventParams));

            // send query and get results
            _log.LogDebug("Sending {EventName} event to Inara", eventName);
            HttpClient client = _httpClientFactory.CreateClient();

            using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, options.InaraURL);
            request.Headers.Add("User-Agent", options.InaraAppName);
            request.Content = new StringContent(query.ToString(Newtonsoft.Json.Formatting.None), Encoding.UTF8, "application/json");
            using HttpResponseMessage response = await client.SendAsync(request, cancellationToken).ConfigureAwait(false);

            response.EnsureSuccessStatusCode();

            // return results
            IEnumerable <JToken> responseObjectsArray = JObject.Parse(await response.Content.ReadAsStringAsync().ConfigureAwait(false))["events"][0]?["eventData"]?.Children() ?? Enumerable.Empty <JToken>();

            _log.LogDebug("Retrieved {ObjectsCount} JSON event data objects from Inara", responseObjectsArray.Count());


            DateTime minDate = DateTime.UtcNow.Add(-options.MaxAge);
            IEnumerable <CommunityGoal> results = responseObjectsArray
                                                  .Select(cgJson => cgJson.ToObject <CommunityGoal>())
                                                  .Where(cg => !cg.IsCompleted || cg.ExpirationTime.Date >= minDate);

            _cgCache            = results;
            _cacheUpdateTimeUtc = DateTime.UtcNow;
            return(results);
        }
        private void StartAutomaticNewsPosting()
        {
            if (_autoModeCTS != null)
            {
                return;
            }

            _autoModeCTS = new CancellationTokenSource();
            Task autoTask = Task.Run(async() =>
            {
                using IDisposable context           = _log.UseSource("Elite CGs");
                CancellationToken cancellationToken = _autoModeCTS.Token;
                // wait 5 seconds to let the client get connection state in check
                await Task.Delay(5 * 1000, cancellationToken).ConfigureAwait(false);
                _log.LogDebug("Starting automatic ED CG checker");
                DateTime _lastRetrievalTime = DateTime.MinValue;
                try
                {
                    while (!cancellationToken.IsCancellationRequested)
                    {
                        if (!this._enabled)
                        {
                            _log.LogWarning("Inara credentials missing. Elite Dangerous Community Goals feature will be disabled");
                            return;
                        }

                        TimeSpan nextUpdateIn = (_lastRetrievalTime + _options.CurrentValue.AutoNewsInterval) - DateTime.UtcNow;
                        // if still waiting, await time, and repeat iteration
                        if (nextUpdateIn > TimeSpan.Zero)
                        {
                            _log.LogTrace("Next update in: {TimeRemaining}", nextUpdateIn);
                            // this will not reflect on updates to options monitor, but that's ok
                            await Task.Delay(nextUpdateIn, cancellationToken).ConfigureAwait(false);
                            continue;
                        }

                        CommunityGoalsOptions options = _options.CurrentValue;

                        // get guild channel
                        if (!(_client.GetChannel(options.AutoNewsChannelID) is SocketTextChannel guildChannel))
                        {
                            throw new InvalidOperationException($"Channel {options.AutoNewsChannelID} is not a valid guild text channel.");
                        }

                        // retrieve CG data, take only new or finished ones, and then update cache
                        IEnumerable <CommunityGoal> allCGs         = await QueryCommunityGoalsAsync(cancellationToken).ConfigureAwait(false);
                        IList <CommunityGoal> newOrJustFinishedCGs = new List <CommunityGoal>(allCGs.Count());
                        foreach (CommunityGoal cg in allCGs)
                        {
                            CommunityGoal historyCg = await _cgHistoryStore.GetAsync(cg.ID, cancellationToken).ConfigureAwait(false);
                            if (historyCg == null || historyCg.IsCompleted != cg.IsCompleted)
                            {
                                newOrJustFinishedCGs.Add(cg);
                                await _cgHistoryStore.SetAsync(cg, cancellationToken).ConfigureAwait(false);
                            }
                        }
                        _log.LogTrace("New or just finished CGs count: {Count}", newOrJustFinishedCGs.Count);

                        // post all CGs
                        _log.LogTrace("Sending CGs");
                        foreach (CommunityGoal cg in newOrJustFinishedCGs)
                        {
                            await guildChannel.SendMessageAsync(null, false, CommunityGoalToEmbed(cg).Build(), cancellationToken).ConfigureAwait(false);
                        }
                        _lastRetrievalTime = DateTime.UtcNow;
                    }
                }
                catch (OperationCanceledException) { }
                catch (Exception ex) when(ex.LogAsError(_log, "Error occured in automatic ED CG checker loop"))
                {
                }
                finally
                {
                    _log.LogDebug("Stopping automatic ED CG checker");
                    // clear CTS on exiting if it wasn't cleared yet
                    if (_autoModeCTS?.Token == cancellationToken)
                    {
                        _autoModeCTS = null;
                    }
                }
            }, _autoModeCTS.Token);
        }