Skip to content

Commit

Permalink
Replace cachedShipInfo with actual MemoryCache
Browse files Browse the repository at this point in the history
  • Loading branch information
smallketchup82 committed Sep 5, 2024
1 parent 4849ed6 commit 7146115
Show file tree
Hide file tree
Showing 4 changed files with 75 additions and 57 deletions.
83 changes: 44 additions & 39 deletions ketchupbot-framework/API/ApiManager.cs
Original file line number Diff line number Diff line change
@@ -1,70 +1,75 @@
using System.Reflection;
using ketchupbot_framework.Types;
using Microsoft.Extensions.Caching.Memory;
using Newtonsoft.Json;

namespace ketchupbot_framework.API;

/// <summary>
/// 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.
/// </summary>
/// <param name="galaxyInfoApi"></param>
public class ApiManager(string galaxyInfoApi)
public class ApiManager(string galaxyInfoApi, IMemoryCache cache)
{
/// <summary>
/// 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.
/// </summary>
private Dictionary<string, Dictionary<string, string>>? _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"}");
}

/// <summary>
/// Get ship data from the Galaxy Info API
/// Get ship data from the Galaxy Info API
/// </summary>
/// <param name="returnCachedOnError">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 <see cref="HttpRequestException"/> will be raised on error.</param>
/// <returns>A list of ships from the Galaxy Info API with their respective data attributes</returns>
public async Task<Dictionary<string, Dictionary<string, string>>> GetShipsData(bool returnCachedOnError = true)
public async Task<Dictionary<string, Dictionary<string, string>>> 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<string, Dictionary<string, string>> cachedData) return cachedData;

string jsonResponse = await response.Content.ReadAsStringAsync();

// Deserialize the response into a Dictionary<string, Dictionary<string, string>> because the json is formatted as:
// {
// "shiptitle":
// {
// "attribute": "value",
// }
// }

Dictionary<string, Dictionary<string, string>> deserializedResponse =
JsonConvert.DeserializeObject<Dictionary<string, Dictionary<string, string>>>(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<string, Dictionary<string, string>> because the json is formatted as:
// {
// "shiptitle":
// {
// "attribute": "value",
// }
// }
var deserializedResponse = JsonConvert.DeserializeObject<Dictionary<string, Dictionary<string, string>>>(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<string, Dictionary<string, string>> cachedData) return cachedData;

throw new InvalidOperationException("Failed to fetch ship data from the Galaxy Info API", e);
}
}

/// <summary>
/// Get turret data from the upstream API
/// Get turret data from the upstream API
/// </summary>
/// <returns>A dictionary with the turret data</returns>
public async Task<Dictionary<string, TurretData>?> GetTurretData()
Expand Down
43 changes: 26 additions & 17 deletions ketchupbot-framework/API/MediaWikiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,24 +20,25 @@ public class MediaWikiClient
private readonly string _baseUrl;

/// <summary>
/// Client for interacting with the MediaWiki API
/// Client for interacting with the MediaWiki API
/// </summary>
/// <param name="username">Username to use for logging in</param>
/// <param name="password">Password to use for logging in</param>
/// <param name="baseUrl"></param>
/// <exception cref="InvalidOperationException"></exception>
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();
}

/// <summary>
///
/// </summary>
/// <param name="username"></param>
/// <param name="password"></param>
Expand Down Expand Up @@ -72,11 +73,12 @@ private async Task LogIn(string username, string password)

string loginJson = await loginRequest.Content.ReadAsStringAsync();
dynamic? loginData = JsonConvert.DeserializeObject<dynamic>(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);
}

/// <summary>
/// Check whether the MediaWikiClient is currently logged in or not
/// Check whether the MediaWikiClient is currently logged in or not
/// </summary>
/// <returns></returns>
public async Task<bool> IsLoggedIn()
Expand All @@ -93,7 +95,7 @@ public async Task<bool> IsLoggedIn()
}

/// <summary>
/// Get the content of a single article
/// Get the content of a single article
/// </summary>
/// <param name="title"></param>
/// <returns></returns>
Expand All @@ -105,7 +107,7 @@ public async Task<string> GetArticle(string title)
}

/// <summary>
/// Get the content of multiple articles
/// Get the content of multiple articles
/// </summary>
/// <param name="titles"></param>
/// <returns>An array of page contents. Or an empty array if no pages were found</returns>
Expand All @@ -119,7 +121,8 @@ public async Task<Dictionary<string, string>> GetArticles(string[] titles)

string jsonResponse = await response.Content.ReadAsStringAsync();

dynamic data = JsonConvert.DeserializeObject<dynamic>(jsonResponse) ?? throw new InvalidOperationException("Failed to deserialize response");
dynamic data = JsonConvert.DeserializeObject<dynamic>(jsonResponse) ??
throw new InvalidOperationException("Failed to deserialize response");

JArray? pages = data.query?.pages;

Expand Down Expand Up @@ -151,7 +154,7 @@ public async Task<Dictionary<string, string>> GetArticles(string[] titles)
}

/// <summary>
/// Edit an article on the wiki with the provided content
/// Edit an article on the wiki with the provided content
/// </summary>
/// <param name="title">The title of the page to edit</param>
/// <param name="newContent">The new content of the page</param>
Expand All @@ -162,25 +165,30 @@ 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<dynamic>(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<string, string>
{
{ "action", "edit" },
{ "format", "json" },
{ "title", title },
{ "text", newContent },
{ "bot", "true"},
{ "summary", summary},
{ "bot", "true" },
{ "summary", summary },
{ "md5", newContentHash },
{ "token", csrfToken }
})
Expand All @@ -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<dynamic>(editJson);
dynamic editData = JsonConvert.DeserializeObject<dynamic>(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);
}
}
1 change: 1 addition & 0 deletions ketchupbot-framework/ketchupbot-framework.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ This project is a framework/library that provides the essential tools for updati

<ItemGroup>
<PackageReference Include="JsonDiffPatch.Net" Version="2.3.0" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="8.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Serilog" Version="4.0.1" />
</ItemGroup>
Expand Down
5 changes: 4 additions & 1 deletion ketchupbot-updater/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -111,6 +112,7 @@ private static async Task<int> Main(string[] args)
applicationBuilder.Services.AddQuartz();
applicationBuilder.Services.AddQuartzHostedService();
applicationBuilder.Services.AddSerilog();
applicationBuilder.Services.AddMemoryCache();
applicationBuilder.Configuration.AddUserSecrets<Program>()
.AddEnvironmentVariables()
.AddJsonFile("appsettings.json",
Expand Down Expand Up @@ -150,7 +152,8 @@ private static async Task<int> Main(string[] args)
{
provider.GetRequiredService<IConfiguration>();
return new ApiManager(provider.GetRequiredService<IConfiguration>()["GIAPI_URL"] ??
throw new InvalidOperationException("GIAPI_URL not set"));
throw new InvalidOperationException("GIAPI_URL not set"),
provider.GetRequiredService<IMemoryCache>());
});

applicationBuilder.Services.AddSingleton<ShipUpdater>(provider => new ShipUpdater(
Expand Down

0 comments on commit 7146115

Please sign in to comment.