diff --git a/ketchupbot-framework/API/ApiManager.cs b/ketchupbot-framework/API/ApiManager.cs
index 78712bf..0610df2 100644
--- a/ketchupbot-framework/API/ApiManager.cs
+++ b/ketchupbot-framework/API/ApiManager.cs
@@ -1,70 +1,75 @@
using System.Reflection;
using ketchupbot_framework.Types;
+using Microsoft.Extensions.Caching.Memory;
using Newtonsoft.Json;
namespace ketchupbot_framework.API;
///
-/// API Manager for interacting with the Galaxy Info API. Responsible for fetching, processing, and returning properly formatted data from the Galaxy Info API.
+/// API Manager for interacting with the Galaxy Info API. Responsible for fetching, processing, and returning properly
+/// formatted data from the Galaxy Info API.
///
///
-public class ApiManager(string galaxyInfoApi)
+public class ApiManager(string galaxyInfoApi, IMemoryCache cache)
{
- ///
- /// Cached ship data from the last successful run.
- /// TODO: This probably isn't enough. We should probably add persistence to this. Might also be wise to have
- /// GetShipsData always return the cached value, and only update the cached value once per hour.
- ///
- private Dictionary>? _cachedShipData;
-
protected static readonly HttpClient HttpClient = new();
static ApiManager()
{
- HttpClient.DefaultRequestHeaders.Add("User-Agent", $"KetchupBot-Updater/{Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "1.0"}");
+ HttpClient.DefaultRequestHeaders.Add("User-Agent",
+ $"KetchupBot-Updater/{Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "1.0"}");
}
///
- /// Get ship data from the Galaxy Info API
+ /// Get ship data from the Galaxy Info API
///
- /// Whether to return an internally cached version (from the last successful run, if any), if the request fails. If false, or if there is no cached version available, an will be raised on error.
/// A list of ships from the Galaxy Info API with their respective data attributes
- public async Task>> GetShipsData(bool returnCachedOnError = true)
+ public async Task>> GetShipsData()
{
using HttpResponseMessage response = await HttpClient.GetAsync($"{galaxyInfoApi.Trim()}/api/v2/galaxypedia");
- switch (response.IsSuccessStatusCode)
+ try
{
- case false when returnCachedOnError && _cachedShipData != null:
- // Status code is not successful, return cached data option is enabled, and there is cached data available
- return _cachedShipData;
- case false:
- // Status code is not successful, and either the return cached data option is disabled or there is no cached data available
- throw new HttpRequestException(
- $"Failed to fetch ship data from the Galaxy Info API: {response.ReasonPhrase}");
- default:
+ response.EnsureSuccessStatusCode();
+
+ // If the data is already cached, return the cached data
+ if (cache.TryGetValue("ShipData", out object? value) &&
+ value is Dictionary> cachedData) return cachedData;
+
+ string jsonResponse = await response.Content.ReadAsStringAsync();
+
+ // Deserialize the response into a Dictionary> because the json is formatted as:
+ // {
+ // "shiptitle":
+ // {
+ // "attribute": "value",
+ // }
+ // }
+
+ Dictionary> deserializedResponse =
+ JsonConvert.DeserializeObject>>(jsonResponse) ??
+ throw new InvalidOperationException("Failed to deserialize ship data from the Galaxy Info API");
+
+ cache.Set("ShipData", deserializedResponse, new MemoryCacheEntryOptions
{
- // Status code is successful, update the cached data
- string jsonResponse = await response.Content.ReadAsStringAsync();
-
- // Deserialize the response into a Dictionary> because the json is formatted as:
- // {
- // "shiptitle":
- // {
- // "attribute": "value",
- // }
- // }
- var deserializedResponse = JsonConvert.DeserializeObject>>(jsonResponse);
-
- _cachedShipData = deserializedResponse ?? throw new HttpRequestException("Failed to deserialize ship data from the Galaxy Info API");
-
- return deserializedResponse;
- }
+ // Cache the data for 30 minutes before refreshing
+ AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30)
+ });
+
+ return deserializedResponse;
+ }
+ catch (Exception e)
+ {
+ // If the data is already cached, return the cached data (since the API request failed)
+ if (cache.TryGetValue("ShipData", out object? value) &&
+ value is Dictionary> cachedData) return cachedData;
+
+ throw new InvalidOperationException("Failed to fetch ship data from the Galaxy Info API", e);
}
}
///
- /// Get turret data from the upstream API
+ /// Get turret data from the upstream API
///
/// A dictionary with the turret data
public async Task?> GetTurretData()
diff --git a/ketchupbot-framework/API/MediaWikiClient.cs b/ketchupbot-framework/API/MediaWikiClient.cs
index a410b67..65576f5 100644
--- a/ketchupbot-framework/API/MediaWikiClient.cs
+++ b/ketchupbot-framework/API/MediaWikiClient.cs
@@ -20,24 +20,25 @@ public class MediaWikiClient
private readonly string _baseUrl;
///
- /// Client for interacting with the MediaWiki API
+ /// Client for interacting with the MediaWiki API
///
/// Username to use for logging in
/// Password to use for logging in
///
///
- public MediaWikiClient(string? username = null, string? password = null, string baseUrl = "https://galaxypedia.org/api.php")
+ public MediaWikiClient(string? username = null, string? password = null,
+ string baseUrl = "https://galaxypedia.org/api.php")
{
_baseUrl = baseUrl;
- Client.DefaultRequestHeaders.Add("User-Agent", $"KetchupBot-Updater/{Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "1.0"}");
+ Client.DefaultRequestHeaders.Add("User-Agent",
+ $"KetchupBot-Updater/{Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "1.0"}");
if (username != null && password != null)
LogIn(username, password).GetAwaiter().GetResult();
}
///
- ///
///
///
///
@@ -72,11 +73,12 @@ private async Task LogIn(string username, string password)
string loginJson = await loginRequest.Content.ReadAsStringAsync();
dynamic? loginData = JsonConvert.DeserializeObject(loginJson);
- if (loginData?.login.result != "Success") throw new InvalidOperationException("Failed to log in to the wiki: " + loginData?.login.reason);
+ if (loginData?.login.result != "Success")
+ throw new InvalidOperationException("Failed to log in to the wiki: " + loginData?.login.reason);
}
///
- /// Check whether the MediaWikiClient is currently logged in or not
+ /// Check whether the MediaWikiClient is currently logged in or not
///
///
public async Task IsLoggedIn()
@@ -93,7 +95,7 @@ public async Task IsLoggedIn()
}
///
- /// Get the content of a single article
+ /// Get the content of a single article
///
///
///
@@ -105,7 +107,7 @@ public async Task GetArticle(string title)
}
///
- /// Get the content of multiple articles
+ /// Get the content of multiple articles
///
///
/// An array of page contents. Or an empty array if no pages were found
@@ -119,7 +121,8 @@ public async Task> GetArticles(string[] titles)
string jsonResponse = await response.Content.ReadAsStringAsync();
- dynamic data = JsonConvert.DeserializeObject(jsonResponse) ?? throw new InvalidOperationException("Failed to deserialize response");
+ dynamic data = JsonConvert.DeserializeObject(jsonResponse) ??
+ throw new InvalidOperationException("Failed to deserialize response");
JArray? pages = data.query?.pages;
@@ -151,7 +154,7 @@ public async Task> GetArticles(string[] titles)
}
///
- /// Edit an article on the wiki with the provided content
+ /// Edit an article on the wiki with the provided content
///
/// The title of the page to edit
/// The new content of the page
@@ -162,16 +165,21 @@ public async Task EditArticle(string title, string newContent, string summary)
if (await IsLoggedIn() == false)
throw new InvalidOperationException("Not logged in");
- // Get MD5 hash of the new content to use for validation
- string newContentHash = BitConverter.ToString(MD5.HashData(Encoding.UTF8.GetBytes(newContent))).Replace("-", "").ToLower();
+ #region Hashing
- using HttpResponseMessage csrfTokenRequest = await Client.GetAsync($"{_baseUrl}?action=query&format=json&meta=tokens");
+ string newContentHash = BitConverter.ToString(MD5.HashData(Encoding.UTF8.GetBytes(newContent))).Replace("-", "")
+ .ToLower();
+
+ using HttpResponseMessage csrfTokenRequest =
+ await Client.GetAsync($"{_baseUrl}?action=query&format=json&meta=tokens");
csrfTokenRequest.EnsureSuccessStatusCode();
string csrfTokenJson = await csrfTokenRequest.Content.ReadAsStringAsync();
dynamic? csrfTokenData = JsonConvert.DeserializeObject(csrfTokenJson);
string? csrfToken = csrfTokenData?.query.tokens.csrftoken;
if (csrfToken == null) throw new InvalidOperationException("Failed to fetch CSRF token");
+ #endregion
+
using HttpResponseMessage editRequest = await Client.PostAsync(_baseUrl, new FormUrlEncodedContent(
new Dictionary
{
@@ -179,8 +187,8 @@ public async Task EditArticle(string title, string newContent, string summary)
{ "format", "json" },
{ "title", title },
{ "text", newContent },
- { "bot", "true"},
- { "summary", summary},
+ { "bot", "true" },
+ { "summary", summary },
{ "md5", newContentHash },
{ "token", csrfToken }
})
@@ -189,9 +197,10 @@ public async Task EditArticle(string title, string newContent, string summary)
editRequest.EnsureSuccessStatusCode();
string editJson = await editRequest.Content.ReadAsStringAsync();
- dynamic? editData = JsonConvert.DeserializeObject(editJson);
+ dynamic editData = JsonConvert.DeserializeObject(editJson) ??
+ throw new InvalidOperationException("Failed to deserialize edit response");
- if (editData?.edit?.result != "Success")
+ if (editData.edit?.result != "Success")
throw new InvalidOperationException("Failed to edit article: " + editData?.edit?.result);
}
}
\ No newline at end of file
diff --git a/ketchupbot-framework/ketchupbot-framework.csproj b/ketchupbot-framework/ketchupbot-framework.csproj
index 75ca01d..393fd07 100644
--- a/ketchupbot-framework/ketchupbot-framework.csproj
+++ b/ketchupbot-framework/ketchupbot-framework.csproj
@@ -17,6 +17,7 @@ This project is a framework/library that provides the essential tools for updati
+
diff --git a/ketchupbot-updater/Program.cs b/ketchupbot-updater/Program.cs
index eb6e2ca..d25d64d 100644
--- a/ketchupbot-updater/Program.cs
+++ b/ketchupbot-updater/Program.cs
@@ -3,6 +3,7 @@
using ketchupbot_framework;
using ketchupbot_framework.API;
using ketchupbot_updater.Jobs;
+using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
@@ -111,6 +112,7 @@ private static async Task Main(string[] args)
applicationBuilder.Services.AddQuartz();
applicationBuilder.Services.AddQuartzHostedService();
applicationBuilder.Services.AddSerilog();
+ applicationBuilder.Services.AddMemoryCache();
applicationBuilder.Configuration.AddUserSecrets()
.AddEnvironmentVariables()
.AddJsonFile("appsettings.json",
@@ -150,7 +152,8 @@ private static async Task Main(string[] args)
{
provider.GetRequiredService();
return new ApiManager(provider.GetRequiredService()["GIAPI_URL"] ??
- throw new InvalidOperationException("GIAPI_URL not set"));
+ throw new InvalidOperationException("GIAPI_URL not set"),
+ provider.GetRequiredService());
});
applicationBuilder.Services.AddSingleton(provider => new ShipUpdater(