Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace cachedShipInfo with actual MemoryCache #126

Merged
merged 1 commit into from
Sep 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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