public void SendQueuedAPIEvents(bool eddiIsBeta) { List <InaraAPIEvent> queue = new List <InaraAPIEvent>(); // The `GetConsumingEnumerable` method blocks the thread while the underlying collection is empty // If we haven't extracted events to send to Inara, this will wait / pause background sync until `queuedAPIEvents` is no longer empty. foreach (var pendingEvent in queuedAPIEvents.GetConsumingEnumerable()) { queue.Add(pendingEvent); if (queue.Count > 0 && queuedAPIEvents.Count == 0) { break; } } InaraConfiguration inaraConfiguration = InaraConfiguration.FromFile(); if (checkAPIcredentialsOk(inaraConfiguration)) { var responses = SendEventBatch(queue, inaraConfiguration, eddiIsBeta); if (responses != null && responses.Count > 0) { inaraConfiguration.lastSync = queue.Max(e => e.eventTimeStamp); inaraConfiguration.ToFile(); } } }
/// <summary> /// Obtain credentials from a file. If the file name is not supplied the the default /// path of Constants.Data_DIR\inara.json is used /// </summary> public static InaraConfiguration FromFile(string filename = null) { if (filename == null) { filename = Constants.DATA_DIR + @"\inara.json"; } InaraConfiguration configuration = new InaraConfiguration(); if (File.Exists(filename)) { try { string data = Files.Read(filename); if (data != null) { configuration = JsonConvert.DeserializeObject <InaraConfiguration>(data); } } catch (Exception ex) { Logging.Debug("Failed to read Inara configuration", ex); } } if (configuration == null) { configuration = new InaraConfiguration(); } configuration.dataPath = filename; return(configuration); }
public void SendAPIEvents(List <InaraAPIEvent> queue) { InaraConfiguration inaraConfiguration = InaraConfiguration.FromFile(); if (checkAPIcredentialsOk(inaraConfiguration)) { var responses = SendEventBatch(queue, inaraConfiguration); if (responses != null && responses.Count > 0) { inaraConfiguration.lastSync = queue.Max(e => e.eventTimeStamp); inaraConfiguration.ToFile(); } } }
public async void SendQueuedAPIEventsAsync() { List <InaraAPIEvent> queue = new List <InaraAPIEvent>(); while (Instance.queuedAPIEvents.TryDequeue(out InaraAPIEvent pendingEvent)) { queue.Add(pendingEvent); } if (queue.Count > 0) { await Task.Run(() => Instance.SendEventBatch(ref queue)); Instance.lastSync = queue.Max(e => e.eventTimeStamp); InaraConfiguration inaraConfiguration = InaraConfiguration.FromFile(); inaraConfiguration.lastSync = Instance.lastSync; inaraConfiguration.ToFile(); } }
private bool checkAPIcredentialsOk(InaraConfiguration inaraConfiguration) { if (!inaraConfiguration.isAPIkeyValid) { Logging.Warn("Background sync skipped: API key is invalid."); invalidAPIkey?.Invoke(inaraConfiguration, new EventArgs()); return(false); } if (string.IsNullOrEmpty(inaraConfiguration.apiKey)) { Logging.Info("Background sync skipped: API key not set."); return(false); } if (string.IsNullOrEmpty(inaraConfiguration.commanderName)) { Logging.Debug("Background sync skipped: Commander name not set."); return(false); } return(true); }
protected InaraService() { // Set up the Inara service InaraConfiguration inaraCredentials = InaraConfiguration.FromFile(); if (inaraCredentials == null) { return; } // commanderName: In-game CMDR name of user (not set by user, get this from journals or // cAPI to ensure it is a correct in-game name to avoid future problems). It is recommended // to be always set when no generic API key is used (otherwise some events may not work). commanderName = inaraCredentials.commanderName; // commanderFrontierID: Commander's unique Frontier ID (is provided by journals since 3.3) // in the format: 'F123456'. When not known, set nothing. commanderFrontierID = inaraCredentials.commanderFrontierID; lastSync = inaraCredentials.lastSync; apiKey = inaraCredentials.apiKey; if (!string.IsNullOrEmpty(apiKey) && !string.IsNullOrEmpty(commanderName)) { // fully configured Logging.Info("Configuring EDDI access to Inara profile data"); } else { apiKey = readonlyAPIkey; if (string.IsNullOrEmpty(inaraCredentials.apiKey)) { Logging.Info("Configuring Inara service for limited access: API key not set."); } if (string.IsNullOrEmpty(commanderName)) { Logging.Info("Configuring Inara service for limited access: Commander name not detected."); } } }
public List <InaraCmdr> GetCommanderProfiles(IEnumerable <string> cmdrNames) { List <InaraCmdr> cmdrs = new List <InaraCmdr>(); List <InaraAPIEvent> events = new List <InaraAPIEvent>(); foreach (string cmdrName in cmdrNames) { events.Add(new InaraAPIEvent(DateTime.UtcNow, "getCommanderProfile", new Dictionary <string, object>() { { "searchName", cmdrName } })); } var responses = SendEventBatch(events, InaraConfiguration.FromFile()); foreach (InaraResponse inaraResponse in responses) { string jsonCmdr = JsonConvert.SerializeObject(inaraResponse.eventData); cmdrs.Add(JsonConvert.DeserializeObject <InaraCmdr>(jsonCmdr)); } return(cmdrs); }
private bool validateResponse(InaraResponse inaraResponse, List <InaraAPIEvent> indexedEvents, bool header = false) { // 200 - Ok if (inaraResponse.eventStatus == 200) { return(true); } // Anything else - something is wrong. Dictionary <string, object> data = new Dictionary <string, object>() { { "InaraAPIEvent", indexedEvents.Find(e => e.eventCustomID == inaraResponse.eventCustomID) }, { "InaraResponse", inaraResponse }, { "Stacktrace", new StackTrace() } }; try { // 202 - Warning (everything is OK, but there may be multiple results for the input properties, etc.) // 204 - 'Soft' error (everything was formally OK, but there are no results for the properties set, etc.) if (inaraResponse.eventStatus == 202 || inaraResponse.eventStatus == 204) { Logging.Warn("Inara responded with: " + (inaraResponse.eventStatusText ?? "(No response)"), JsonConvert.SerializeObject(data)); } // Other errors else if (!string.IsNullOrEmpty(inaraResponse.eventStatusText)) { if (header) { Logging.Warn("Inara responded with: " + (inaraResponse.eventStatusText ?? "(No response)"), JsonConvert.SerializeObject(data)); if (inaraResponse.eventStatusText.Contains("Invalid API key")) { ReEnqueueAPIEvents(indexedEvents); // The Inara API key has been rejected. We'll note and remember that. InaraConfiguration inaraConfiguration = InaraConfiguration.FromFile(); inaraConfiguration.isAPIkeyValid = false; inaraConfiguration.ToFile(); // Send internal events to the Inara Responder and the UI to handle the invalid API key appropriately invalidAPIkey?.Invoke(inaraConfiguration, new EventArgs()); } else if (inaraResponse.eventStatusText.Contains("access to API was temporarily revoked")) { // Note: This can be thrown by over-use of the readonly API key. ReEnqueueAPIEvents(indexedEvents); tooManyRequests = true; } } else { // There may be an issue with a specific API event. // We'll add that API event to a list and omit sending that event again in this instance. Logging.Error("Inara responded with: " + inaraResponse.eventStatusText, data); invalidAPIEvents.Add(indexedEvents.Find(e => e.eventCustomID == inaraResponse.eventCustomID).eventName); } } else { // Inara responded, but no status text description was given. Logging.Error("Inara responded with: ", data); } return(false); } catch (Exception e) { data.Add("Exception", e); Logging.Error("Failed to handle Inara server response", data); return(false); } }
private List <InaraAPIEvent> IndexAndFilterAPIEvents(List <InaraAPIEvent> events, InaraConfiguration inaraConfiguration) { // Flag each event with a unique ID we can use when processing responses List <InaraAPIEvent> indexedEvents = new List <InaraAPIEvent>(); for (int i = 0; i < events.Count; i++) { InaraAPIEvent indexedEvent = events[i]; indexedEvent.eventCustomID = i; // Exclude and discard events with issues that have returned a code 400 error in this instance. if (invalidAPIEvents.Contains(indexedEvent.eventName)) { continue; } // Exclude and discard old / stale events if (inaraConfiguration?.lastSync > indexedEvent.eventTimeStamp) { continue; } // Inara will ignore the "setCommunityGoal" event while EDDI is in development mode (i.e. beta). if (indexedEvent.eventName == "setCommunityGoal" && eddiIsBeta) { continue; } // Note: the Inara Responder does not queue events while the game is in beta. indexedEvents.Add(indexedEvent); } return(indexedEvents); }
// If you need to do some testing on Inara's API, please set the `isDeveloped` boolean header property to true. public List <InaraResponse> SendEventBatch(List <InaraAPIEvent> events, InaraConfiguration inaraConfiguration) { // We always want to return a list from this method (even if it's an empty list) rather than a null value. List <InaraResponse> inaraResponses = new List <InaraResponse>(); try { if (inaraConfiguration is null) { inaraConfiguration = InaraConfiguration.FromFile(); } List <InaraAPIEvent> indexedEvents = IndexAndFilterAPIEvents(events, inaraConfiguration); if (indexedEvents.Count > 0) { var client = new RestClient("https://inara.cz/inapi/v1/"); var request = new RestRequest(Method.POST); InaraSendJson inaraRequest = new InaraSendJson() { header = new Dictionary <string, object>() { // Per private conversation with Artie and per Inara API docs, the `isDeveloped` property // should (counterintuitively) be set to true when the an application is in development. // Quote: `it's "true" because the app "is (being) developed"` // Quote: `isDeveloped is meant as "the app is currently being developed and may be broken` { "appName", "EDDI" }, { "appVersion", Constants.EDDI_VERSION.ToString() }, { "isDeveloped", eddiIsBeta }, { "commanderName", !string.IsNullOrEmpty(inaraConfiguration?.commanderName) ? inaraConfiguration.commanderName : (eddiIsBeta ? "TestCmdr" : null) }, { "commanderFrontierID", !string.IsNullOrEmpty(inaraConfiguration?.commanderFrontierID) ? inaraConfiguration.commanderFrontierID : null }, { "APIkey", !string.IsNullOrEmpty(inaraConfiguration?.apiKey) ? inaraConfiguration.apiKey : readonlyAPIkey } }, events = indexedEvents }; request.RequestFormat = DataFormat.Json; request.AddBody(inaraRequest); // uses JsonSerializer Logging.Debug("Sending to Inara: " + client.BuildUri(request).AbsoluteUri); var clientResponse = client.Execute <InaraResponses>(request); if (clientResponse.IsSuccessful) { InaraResponses response = clientResponse.Data; if (validateResponse(response.header, indexedEvents, true)) { foreach (InaraResponse inaraResponse in response.events) { if (validateResponse(inaraResponse, indexedEvents)) { inaraResponses.Add(inaraResponse); } } } } else { // Inara may return null as it undergoes a nightly maintenance cycle where the servers go offline temporarily. Logging.Warn("Unable to connect to the Inara server.", clientResponse.ErrorMessage); ReEnqueueAPIEvents(events); } } } catch (Exception ex) { Logging.Error("Sending data to the Inara server failed.", ex); } return(inaraResponses); }
private List <InaraAPIEvent> IndexAndFilterAPIEvents(List <InaraAPIEvent> events, InaraConfiguration inaraConfiguration) { // If we don't have a commander name then only use `get` events and re-enqueue the rest. if (string.IsNullOrEmpty(inaraConfiguration.commanderName)) { var commanderUpdateEvents = events.Where(e => !e.eventName.StartsWith("get")).ToList(); ReEnqueueAPIEvents(commanderUpdateEvents); events = events.Except(commanderUpdateEvents).ToList(); } // Flag each event with a unique ID we can use when processing responses List <InaraAPIEvent> indexedEvents = new List <InaraAPIEvent>(); for (int i = 0; i < events.Count; i++) { InaraAPIEvent indexedEvent = events[i]; indexedEvent.eventCustomID = i; // Exclude and discard events with issues that have returned a code 400 error in this instance. if (invalidAPIEvents.Contains(indexedEvent.eventName)) { continue; } // Exclude and discard old / stale events if (inaraConfiguration?.lastSync > indexedEvent.eventTimestamp) { continue; } // Inara will ignore the "setCommunityGoal" event while EDDI is in development mode (i.e. beta). if (indexedEvent.eventName == "setCommunityGoal" && eddiIsBeta) { continue; } // Note: the Inara Responder does not queue events while the game is in beta. indexedEvents.Add(indexedEvent); } return(indexedEvents); }