summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/SMAPI.Web/Controllers/ModsApiController.cs34
-rw-r--r--src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishClient.cs83
-rw-r--r--src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishMod.cs18
-rw-r--r--src/SMAPI.Web/Framework/Clients/Chucklefish/IChucklefishClient.cs17
-rw-r--r--src/SMAPI.Web/Framework/Clients/GitHub/GitHubClient.cs70
-rw-r--r--src/SMAPI.Web/Framework/Clients/GitHub/GitRelease.cs19
-rw-r--r--src/SMAPI.Web/Framework/Clients/GitHub/IGitHubClient.cs17
-rw-r--r--src/SMAPI.Web/Framework/Clients/Nexus/INexusClient.cs17
-rw-r--r--src/SMAPI.Web/Framework/Clients/Nexus/NexusClient.cs48
-rw-r--r--src/SMAPI.Web/Framework/Clients/Nexus/NexusMod.cs21
-rw-r--r--src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs56
-rw-r--r--src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs49
-rw-r--r--src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs50
-rw-r--r--src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs59
-rw-r--r--src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs51
-rw-r--r--src/SMAPI.Web/Startup.cs30
-rw-r--r--src/SMAPI.Web/appsettings.Development.json2
-rw-r--r--src/SMAPI.Web/appsettings.json19
18 files changed, 447 insertions, 213 deletions
diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs
index a600662c..dcb4ec52 100644
--- a/src/SMAPI.Web/Controllers/ModsApiController.cs
+++ b/src/SMAPI.Web/Controllers/ModsApiController.cs
@@ -7,6 +7,9 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using StardewModdingAPI.Common.Models;
+using StardewModdingAPI.Web.Framework.Clients.Chucklefish;
+using StardewModdingAPI.Web.Framework.Clients.GitHub;
+using StardewModdingAPI.Web.Framework.Clients.Nexus;
using StardewModdingAPI.Web.Framework.ConfigModels;
using StardewModdingAPI.Web.Framework.ModRepositories;
@@ -39,39 +42,22 @@ namespace StardewModdingAPI.Web.Controllers
/// <summary>Construct an instance.</summary>
/// <param name="cache">The cache in which to store mod metadata.</param>
/// <param name="configProvider">The config settings for mod update checks.</param>
- public ModsApiController(IMemoryCache cache, IOptions<ModUpdateCheckConfig> configProvider)
+ /// <param name="chucklefish">The Chucklefish API client.</param>
+ /// <param name="github">The GitHub API client.</param>
+ /// <param name="nexus">The Nexus API client.</param>
+ public ModsApiController(IMemoryCache cache, IOptions<ModUpdateCheckConfig> configProvider, IChucklefishClient chucklefish, IGitHubClient github, INexusClient nexus)
{
ModUpdateCheckConfig config = configProvider.Value;
this.Cache = cache;
this.CacheMinutes = config.CacheMinutes;
this.VersionRegex = config.SemanticVersionRegex;
-
- string version = this.GetType().Assembly.GetName().Version.ToString(3);
this.Repositories =
new IModRepository[]
{
- new ChucklefishRepository(
- vendorKey: config.ChucklefishKey,
- userAgent: string.Format(config.ChucklefishUserAgent, version),
- baseUrl: config.ChucklefishBaseUrl,
- modPageUrlFormat: config.ChucklefishModPageUrlFormat
- ),
- new GitHubRepository(
- vendorKey: config.GitHubKey,
- baseUrl: config.GitHubBaseUrl,
- releaseUrlFormat: config.GitHubReleaseUrlFormat,
- userAgent: string.Format(config.GitHubUserAgent, version),
- acceptHeader: config.GitHubAcceptHeader,
- username: config.GitHubUsername,
- password: config.GitHubPassword
- ),
- new NexusRepository(
- vendorKey: config.NexusKey,
- userAgent: config.NexusUserAgent,
- baseUrl: config.NexusBaseUrl,
- modUrlFormat: config.NexusModUrlFormat
- )
+ new ChucklefishRepository(config.ChucklefishKey, chucklefish),
+ new GitHubRepository(config.GitHubKey, github),
+ new NexusRepository(config.NexusKey, nexus)
}
.ToDictionary(p => p.VendorKey, StringComparer.CurrentCultureIgnoreCase);
}
diff --git a/src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishClient.cs b/src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishClient.cs
new file mode 100644
index 00000000..6772672c
--- /dev/null
+++ b/src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishClient.cs
@@ -0,0 +1,83 @@
+using System;
+using System.Net;
+using System.Threading.Tasks;
+using HtmlAgilityPack;
+using Pathoschild.Http.Client;
+
+namespace StardewModdingAPI.Web.Framework.Clients.Chucklefish
+{
+ /// <summary>An HTTP client for fetching mod metadata from the Chucklefish mod site.</summary>
+ internal class ChucklefishClient : IChucklefishClient
+ {
+ /*********
+ ** Properties
+ *********/
+ /// <summary>The base URL for the Chucklefish mod site.</summary>
+ private readonly string BaseUrl;
+
+ /// <summary>The URL for a mod page excluding the base URL, where {0} is the mod ID.</summary>
+ private readonly string ModPageUrlFormat;
+
+ /// <summary>The underlying HTTP client.</summary>
+ private readonly IClient Client;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="userAgent">The user agent for the API client.</param>
+ /// <param name="baseUrl">The base URL for the Chucklefish mod site.</param>
+ /// <param name="modPageUrlFormat">The URL for a mod page excluding the <paramref name="baseUrl"/>, where {0} is the mod ID.</param>
+ public ChucklefishClient(string userAgent, string baseUrl, string modPageUrlFormat)
+ {
+ this.BaseUrl = baseUrl;
+ this.ModPageUrlFormat = modPageUrlFormat;
+ this.Client = new FluentClient(baseUrl).SetUserAgent(userAgent);
+ }
+
+ /// <summary>Get metadata about a mod.</summary>
+ /// <param name="id">The Chucklefish mod ID.</param>
+ /// <returns>Returns the mod info if found, else <c>null</c>.</returns>
+ public async Task<ChucklefishMod> GetModAsync(uint id)
+ {
+ // fetch HTML
+ string html;
+ try
+ {
+ html = await this.Client
+ .GetAsync(string.Format(this.ModPageUrlFormat, id))
+ .AsString();
+ }
+ catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound)
+ {
+ return null;
+ }
+
+ // parse HTML
+ var doc = new HtmlDocument();
+ doc.LoadHtml(html);
+
+ // extract mod info
+ string url = new UriBuilder(new Uri(this.BaseUrl)) { Path = string.Format(this.ModPageUrlFormat, id) }.Uri.ToString();
+ string name = doc.DocumentNode.SelectSingleNode("//meta[@name='twitter:title']").Attributes["content"].Value;
+ if (name.StartsWith("[SMAPI] "))
+ name = name.Substring("[SMAPI] ".Length);
+ string version = doc.DocumentNode.SelectSingleNode("//h1/span").InnerText;
+
+ // create model
+ return new ChucklefishMod
+ {
+ Name = name,
+ Version = version,
+ Url = url
+ };
+ }
+
+ /// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
+ public void Dispose()
+ {
+ this.Client?.Dispose();
+ }
+ }
+}
diff --git a/src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishMod.cs b/src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishMod.cs
new file mode 100644
index 00000000..fd0101d4
--- /dev/null
+++ b/src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishMod.cs
@@ -0,0 +1,18 @@
+namespace StardewModdingAPI.Web.Framework.Clients.Chucklefish
+{
+ /// <summary>Mod metadata from the Chucklefish mod site.</summary>
+ internal class ChucklefishMod
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The mod name.</summary>
+ public string Name { get; set; }
+
+ /// <summary>The mod's semantic version number.</summary>
+ public string Version { get; set; }
+
+ /// <summary>The mod's web URL.</summary>
+ public string Url { get; set; }
+ }
+}
diff --git a/src/SMAPI.Web/Framework/Clients/Chucklefish/IChucklefishClient.cs b/src/SMAPI.Web/Framework/Clients/Chucklefish/IChucklefishClient.cs
new file mode 100644
index 00000000..1d8b256e
--- /dev/null
+++ b/src/SMAPI.Web/Framework/Clients/Chucklefish/IChucklefishClient.cs
@@ -0,0 +1,17 @@
+using System;
+using System.Threading.Tasks;
+
+namespace StardewModdingAPI.Web.Framework.Clients.Chucklefish
+{
+ /// <summary>An HTTP client for fetching mod metadata from the Chucklefish mod site.</summary>
+ internal interface IChucklefishClient : IDisposable
+ {
+ /*********
+ ** Methods
+ *********/
+ /// <summary>Get metadata about a mod.</summary>
+ /// <param name="id">The Chucklefish mod ID.</param>
+ /// <returns>Returns the mod info if found, else <c>null</c>.</returns>
+ Task<ChucklefishMod> GetModAsync(uint id);
+ }
+}
diff --git a/src/SMAPI.Web/Framework/Clients/GitHub/GitHubClient.cs b/src/SMAPI.Web/Framework/Clients/GitHub/GitHubClient.cs
new file mode 100644
index 00000000..0b205660
--- /dev/null
+++ b/src/SMAPI.Web/Framework/Clients/GitHub/GitHubClient.cs
@@ -0,0 +1,70 @@
+using System;
+using System.Net;
+using System.Threading.Tasks;
+using Pathoschild.Http.Client;
+
+namespace StardewModdingAPI.Web.Framework.Clients.GitHub
+{
+ /// <summary>An HTTP client for fetching metadata from GitHub.</summary>
+ internal class GitHubClient : IGitHubClient
+ {
+ /*********
+ ** Properties
+ *********/
+ /// <summary>The URL for a GitHub releases API query excluding the base URL, where {0} is the repository owner and name.</summary>
+ private readonly string ReleaseUrlFormat;
+
+ /// <summary>The underlying HTTP client.</summary>
+ private readonly IClient Client;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="baseUrl">The base URL for the GitHub API.</param>
+ /// <param name="releaseUrlFormat">The URL for a GitHub releases API query excluding the <paramref name="baseUrl"/>, where {0} is the repository owner and name.</param>
+ /// <param name="userAgent">The user agent for the API client.</param>
+ /// <param name="acceptHeader">The Accept header value expected by the GitHub API.</param>
+ /// <param name="username">The username with which to authenticate to the GitHub API.</param>
+ /// <param name="password">The password with which to authenticate to the GitHub API.</param>
+ public GitHubClient(string baseUrl, string releaseUrlFormat, string userAgent, string acceptHeader, string username, string password)
+ {
+ this.ReleaseUrlFormat = releaseUrlFormat;
+
+ this.Client = new FluentClient(baseUrl)
+ .SetUserAgent(userAgent)
+ .AddDefault(req => req.WithHeader("Accept", acceptHeader));
+ if (!string.IsNullOrWhiteSpace(username))
+ this.Client = this.Client.SetBasicAuthentication(username, password);
+ }
+
+ /// <summary>Get the latest release for a GitHub repository.</summary>
+ /// <param name="repo">The repository key (like <c>Pathoschild/SMAPI</c>).</param>
+ /// <returns>Returns the latest release if found, else <c>null</c>.</returns>
+ public async Task<GitRelease> GetLatestReleaseAsync(string repo)
+ {
+ // validate key format
+ if (!repo.Contains("/") || repo.IndexOf("/", StringComparison.InvariantCultureIgnoreCase) != repo.LastIndexOf("/", StringComparison.InvariantCultureIgnoreCase))
+ throw new ArgumentException($"The value '{repo}' isn't a valid GitHub repository key, must be a username and project name like 'Pathoschild/SMAPI'.", nameof(repo));
+
+ // fetch info
+ try
+ {
+ return await this.Client
+ .GetAsync(string.Format(this.ReleaseUrlFormat, repo))
+ .As<GitRelease>();
+ }
+ catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound)
+ {
+ return null;
+ }
+ }
+
+ /// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
+ public void Dispose()
+ {
+ this.Client?.Dispose();
+ }
+ }
+}
diff --git a/src/SMAPI.Web/Framework/Clients/GitHub/GitRelease.cs b/src/SMAPI.Web/Framework/Clients/GitHub/GitRelease.cs
new file mode 100644
index 00000000..0a47f3b4
--- /dev/null
+++ b/src/SMAPI.Web/Framework/Clients/GitHub/GitRelease.cs
@@ -0,0 +1,19 @@
+using Newtonsoft.Json;
+
+namespace StardewModdingAPI.Web.Framework.Clients.GitHub
+{
+ /// <summary>A GitHub project release.</summary>
+ internal class GitRelease
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The display name.</summary>
+ [JsonProperty("name")]
+ public string Name { get; set; }
+
+ /// <summary>The semantic version string.</summary>
+ [JsonProperty("tag_name")]
+ public string Tag { get; set; }
+ }
+}
diff --git a/src/SMAPI.Web/Framework/Clients/GitHub/IGitHubClient.cs b/src/SMAPI.Web/Framework/Clients/GitHub/IGitHubClient.cs
new file mode 100644
index 00000000..6e8eadff
--- /dev/null
+++ b/src/SMAPI.Web/Framework/Clients/GitHub/IGitHubClient.cs
@@ -0,0 +1,17 @@
+using System;
+using System.Threading.Tasks;
+
+namespace StardewModdingAPI.Web.Framework.Clients.GitHub
+{
+ /// <summary>An HTTP client for fetching metadata from GitHub.</summary>
+ internal interface IGitHubClient : IDisposable
+ {
+ /*********
+ ** Methods
+ *********/
+ /// <summary>Get the latest release for a GitHub repository.</summary>
+ /// <param name="repo">The repository key (like <c>Pathoschild/SMAPI</c>).</param>
+ /// <returns>Returns the latest release if found, else <c>null</c>.</returns>
+ Task<GitRelease> GetLatestReleaseAsync(string repo);
+ }
+}
diff --git a/src/SMAPI.Web/Framework/Clients/Nexus/INexusClient.cs b/src/SMAPI.Web/Framework/Clients/Nexus/INexusClient.cs
new file mode 100644
index 00000000..e56e7af4
--- /dev/null
+++ b/src/SMAPI.Web/Framework/Clients/Nexus/INexusClient.cs
@@ -0,0 +1,17 @@
+using System;
+using System.Threading.Tasks;
+
+namespace StardewModdingAPI.Web.Framework.Clients.Nexus
+{
+ /// <summary>An HTTP client for fetching mod metadata from Nexus Mods.</summary>
+ internal interface INexusClient : IDisposable
+ {
+ /*********
+ ** Methods
+ *********/
+ /// <summary>Get metadata about a mod.</summary>
+ /// <param name="id">The Nexus mod ID.</param>
+ /// <returns>Returns the mod info if found, else <c>null</c>.</returns>
+ Task<NexusMod> GetModAsync(uint id);
+ }
+}
diff --git a/src/SMAPI.Web/Framework/Clients/Nexus/NexusClient.cs b/src/SMAPI.Web/Framework/Clients/Nexus/NexusClient.cs
new file mode 100644
index 00000000..1a7011e2
--- /dev/null
+++ b/src/SMAPI.Web/Framework/Clients/Nexus/NexusClient.cs
@@ -0,0 +1,48 @@
+using System.Threading.Tasks;
+using Pathoschild.Http.Client;
+
+namespace StardewModdingAPI.Web.Framework.Clients.Nexus
+{
+ /// <summary>An HTTP client for fetching mod metadata from Nexus Mods.</summary>
+ internal class NexusClient : INexusClient
+ {
+ /*********
+ ** Properties
+ *********/
+ /// <summary>The URL for a Nexus Mods API query excluding the base URL, where {0} is the mod ID.</summary>
+ private readonly string ModUrlFormat;
+
+ /// <summary>The underlying HTTP client.</summary>
+ private readonly IClient Client;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="userAgent">The user agent for the Nexus Mods API client.</param>
+ /// <param name="baseUrl">The base URL for the Nexus Mods API.</param>
+ /// <param name="modUrlFormat">The URL for a Nexus Mods API query excluding the <paramref name="baseUrl"/>, where {0} is the mod ID.</param>
+ public NexusClient(string userAgent, string baseUrl, string modUrlFormat)
+ {
+ this.ModUrlFormat = modUrlFormat;
+ this.Client = new FluentClient(baseUrl).SetUserAgent(userAgent);
+ }
+
+ /// <summary>Get metadata about a mod.</summary>
+ /// <param name="id">The Nexus mod ID.</param>
+ /// <returns>Returns the mod info if found, else <c>null</c>.</returns>
+ public async Task<NexusMod> GetModAsync(uint id)
+ {
+ return await this.Client
+ .GetAsync(string.Format(this.ModUrlFormat, id))
+ .As<NexusMod>();
+ }
+
+ /// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
+ public void Dispose()
+ {
+ this.Client?.Dispose();
+ }
+ }
+}
diff --git a/src/SMAPI.Web/Framework/Clients/Nexus/NexusMod.cs b/src/SMAPI.Web/Framework/Clients/Nexus/NexusMod.cs
new file mode 100644
index 00000000..2b04104f
--- /dev/null
+++ b/src/SMAPI.Web/Framework/Clients/Nexus/NexusMod.cs
@@ -0,0 +1,21 @@
+using Newtonsoft.Json;
+
+namespace StardewModdingAPI.Web.Framework.Clients.Nexus
+{
+ /// <summary>Mod metadata from Nexus Mods.</summary>
+ internal class NexusMod
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The mod name.</summary>
+ public string Name { get; set; }
+
+ /// <summary>The mod's semantic version number.</summary>
+ public string Version { get; set; }
+
+ /// <summary>The mod's web URL.</summary>
+ [JsonProperty("mod_page_uri")]
+ public string Url { get; set; }
+ }
+}
diff --git a/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs
new file mode 100644
index 00000000..19794920
--- /dev/null
+++ b/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs
@@ -0,0 +1,56 @@
+namespace StardewModdingAPI.Web.Framework.ConfigModels
+{
+ /// <summary>The config settings for the API clients.</summary>
+ internal class ApiClientsConfig
+ {
+ /*********
+ ** Accessors
+ *********/
+ /****
+ ** Generic
+ ****/
+ /// <summary>The user agent for API clients, where {0} is the SMAPI version.</summary>
+ public string UserAgent { get; set; }
+
+
+ /****
+ ** Chucklefish
+ ****/
+ /// <summary>The base URL for the Chucklefish mod site.</summary>
+ public string ChucklefishBaseUrl { get; set; }
+
+ /// <summary>The URL for a mod page on the Chucklefish mod site excluding the <see cref="GitHubBaseUrl"/>, where {0} is the mod ID.</summary>
+ public string ChucklefishModPageUrlFormat { get; set; }
+
+
+ /****
+ ** GitHub
+ ****/
+ /// <summary>The base URL for the GitHub API.</summary>
+ public string GitHubBaseUrl { get; set; }
+
+ /// <summary>The URL for a GitHub API latest-release query excluding the <see cref="GitHubBaseUrl"/>, where {0} is the organisation and project name.</summary>
+ public string GitHubReleaseUrlFormat { get; set; }
+
+ /// <summary>The Accept header value expected by the GitHub API.</summary>
+ public string GitHubAcceptHeader { get; set; }
+
+ /// <summary>The username with which to authenticate to the GitHub API (if any).</summary>
+ public string GitHubUsername { get; set; }
+
+ /// <summary>The password with which to authenticate to the GitHub API (if any).</summary>
+ public string GitHubPassword { get; set; }
+
+ /****
+ ** Nexus Mods
+ ****/
+ /// <summary>The user agent for the Nexus Mods API client.</summary>
+ public string NexusUserAgent { get; set; }
+
+ /// <summary>The base URL for the Nexus Mods API.</summary>
+ public string NexusBaseUrl { get; set; }
+
+ /// <summary>The URL for a Nexus Mods API query excluding the <see cref="NexusBaseUrl"/>, where {0} is the mod ID.</summary>
+ public string NexusModUrlFormat { get; set; }
+ }
+}
diff --git a/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs
index 2fb5b97e..58c3a100 100644
--- a/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs
+++ b/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs
@@ -6,9 +6,6 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels
/*********
** Accessors
*********/
- /****
- ** General
- ****/
/// <summary>The number of minutes update checks should be cached before refetching them.</summary>
public int CacheMinutes { get; set; }
@@ -16,59 +13,13 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels
/// <remarks>Derived from SMAPI's SemanticVersion implementation.</remarks>
public string SemanticVersionRegex { get; set; }
- /****
- ** Chucklefish mod site
- ****/
/// <summary>The repository key for the Chucklefish mod site.</summary>
public string ChucklefishKey { get; set; }
- /// <summary>The user agent for the Chucklefish API client, where {0} is the SMAPI version.</summary>
- public string ChucklefishUserAgent { get; set; }
-
- /// <summary>The base URL for the Chucklefish mod site.</summary>
- public string ChucklefishBaseUrl { get; set; }
-
- /// <summary>The URL for a mod page on the Chucklefish mod site excluding the <see cref="GitHubBaseUrl"/>, where {0} is the mod ID.</summary>
- public string ChucklefishModPageUrlFormat { get; set; }
-
-
- /****
- ** GitHub
- ****/
/// <summary>The repository key for Nexus Mods.</summary>
public string GitHubKey { get; set; }
- /// <summary>The user agent for the GitHub API client, where {0} is the SMAPI version.</summary>
- public string GitHubUserAgent { get; set; }
-
- /// <summary>The base URL for the GitHub API.</summary>
- public string GitHubBaseUrl { get; set; }
-
- /// <summary>The URL for a GitHub API latest-release query excluding the <see cref="GitHubBaseUrl"/>, where {0} is the organisation and project name.</summary>
- public string GitHubReleaseUrlFormat { get; set; }
-
- /// <summary>The Accept header value expected by the GitHub API.</summary>
- public string GitHubAcceptHeader { get; set; }
-
- /// <summary>The username with which to authenticate to the GitHub API (if any).</summary>
- public string GitHubUsername { get; set; }
-
- /// <summary>The password with which to authenticate to the GitHub API (if any).</summary>
- public string GitHubPassword { get; set; }
-
- /****
- ** Nexus Mods
- ****/
/// <summary>The repository key for Nexus Mods.</summary>
public string NexusKey { get; set; }
-
- /// <summary>The user agent for the Nexus Mods API client.</summary>
- public string NexusUserAgent { get; set; }
-
- /// <summary>The base URL for the Nexus Mods API.</summary>
- public string NexusBaseUrl { get; set; }
-
- /// <summary>The URL for a Nexus Mods API query excluding the <see cref="NexusBaseUrl"/>, where {0} is the mod ID.</summary>
- public string NexusModUrlFormat { get; set; }
}
}
diff --git a/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs
index 06ec58ed..266055a6 100644
--- a/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs
+++ b/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs
@@ -1,9 +1,7 @@
using System;
-using System.Net;
using System.Threading.Tasks;
-using HtmlAgilityPack;
-using Pathoschild.Http.Client;
using StardewModdingAPI.Common.Models;
+using StardewModdingAPI.Web.Framework.Clients.Chucklefish;
namespace StardewModdingAPI.Web.Framework.ModRepositories
{
@@ -13,14 +11,8 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
/*********
** Properties
*********/
- /// <summary>The base URL for the Chucklefish mod site.</summary>
- private readonly string BaseUrl;
-
- /// <summary>The URL for a mod page excluding the base URL, where {0} is the mod ID.</summary>
- private readonly string ModPageUrlFormat;
-
/// <summary>The underlying HTTP client.</summary>
- private readonly IClient Client;
+ private readonly IChucklefishClient Client;
/*********
@@ -28,15 +20,11 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
*********/
/// <summary>Construct an instance.</summary>
/// <param name="vendorKey">The unique key for this vendor.</param>
- /// <param name="userAgent">The user agent for the API client.</param>
- /// <param name="baseUrl">The base URL for the Chucklefish mod site.</param>
- /// <param name="modPageUrlFormat">The URL for a mod page excluding the <paramref name="baseUrl"/>, where {0} is the mod ID.</param>
- public ChucklefishRepository(string vendorKey, string userAgent, string baseUrl, string modPageUrlFormat)
+ /// <param name="client">The underlying HTTP client.</param>
+ public ChucklefishRepository(string vendorKey, IChucklefishClient client)
: base(vendorKey)
{
- this.BaseUrl = baseUrl;
- this.ModPageUrlFormat = modPageUrlFormat;
- this.Client = new FluentClient(baseUrl).SetUserAgent(userAgent);
+ this.Client = client;
}
/// <summary>Get metadata about a mod in the repository.</summary>
@@ -44,38 +32,18 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
public override async Task<ModInfoModel> GetModInfoAsync(string id)
{
// validate ID format
- if (!uint.TryParse(id, out uint _))
+ if (!uint.TryParse(id, out uint realID))
return new ModInfoModel($"The value '{id}' isn't a valid Chucklefish mod ID, must be an integer ID.");
// fetch info
try
{
- // fetch HTML
- string html;
- try
- {
- html = await this.Client
- .GetAsync(string.Format(this.ModPageUrlFormat, id))
- .AsString();
- }
- catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound)
- {
+ var mod = await this.Client.GetModAsync(realID);
+ if (mod == null)
return new ModInfoModel("Found no mod with this ID.");
- }
-
- // parse HTML
- var doc = new HtmlDocument();
- doc.LoadHtml(html);
-
- // extract mod info
- string url = new UriBuilder(new Uri(this.BaseUrl)) { Path = string.Format(this.ModPageUrlFormat, id) }.Uri.ToString();
- string name = doc.DocumentNode.SelectSingleNode("//meta[@name='twitter:title']").Attributes["content"].Value;
- if (name.StartsWith("[SMAPI] "))
- name = name.Substring("[SMAPI] ".Length);
- string version = doc.DocumentNode.SelectSingleNode("//h1/span").InnerText;
// create model
- return new ModInfoModel(name, this.NormaliseVersion(version), url);
+ return new ModInfoModel(mod.Name, this.NormaliseVersion(mod.Version), mod.Url);
}
catch (Exception ex)
{
diff --git a/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs
index 9d43adf0..7bad6127 100644
--- a/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs
+++ b/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs
@@ -1,9 +1,7 @@
using System;
-using System.Net;
using System.Threading.Tasks;
-using Newtonsoft.Json;
-using Pathoschild.Http.Client;
using StardewModdingAPI.Common.Models;
+using StardewModdingAPI.Web.Framework.Clients.GitHub;
namespace StardewModdingAPI.Web.Framework.ModRepositories
{
@@ -13,11 +11,8 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
/*********
** Properties
*********/
- /// <summary>The URL for a Nexus Mods API query excluding the base URL, where {0} is the mod ID.</summary>
- private readonly string ReleaseUrlFormat;
-
- /// <summary>The underlying HTTP client.</summary>
- private readonly IClient Client;
+ /// <summary>The underlying GitHub API client.</summary>
+ private readonly IGitHubClient Client;
/*********
@@ -25,22 +20,11 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
*********/
/// <summary>Construct an instance.</summary>
/// <param name="vendorKey">The unique key for this vendor.</param>
- /// <param name="baseUrl">The base URL for the Nexus Mods API.</param>
- /// <param name="releaseUrlFormat">The URL for a Nexus Mods API query excluding the <paramref name="baseUrl"/>, where {0} is the mod ID.</param>
- /// <param name="userAgent">The user agent for the API client.</param>
- /// <param name="acceptHeader">The Accept header value expected by the GitHub API.</param>
- /// <param name="username">The username with which to authenticate to the GitHub API.</param>
- /// <param name="password">The password with which to authenticate to the GitHub API.</param>
- public GitHubRepository(string vendorKey, string baseUrl, string releaseUrlFormat, string userAgent, string acceptHeader, string username, string password)
+ /// <param name="client">The underlying GitHub API client.</param>
+ public GitHubRepository(string vendorKey, IGitHubClient client)
: base(vendorKey)
{
- this.ReleaseUrlFormat = releaseUrlFormat;
-
- this.Client = new FluentClient(baseUrl)
- .SetUserAgent(userAgent)
- .AddDefault(req => req.WithHeader("Accept", acceptHeader));
- if (!string.IsNullOrWhiteSpace(username))
- this.Client = this.Client.SetBasicAuthentication(username, password);
+ this.Client = client;
}
/// <summary>Get metadata about a mod in the repository.</summary>
@@ -54,14 +38,10 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
// fetch info
try
{
- GitRelease release = await this.Client
- .GetAsync(string.Format(this.ReleaseUrlFormat, id))
- .As<GitRelease>();
- return new ModInfoModel(id, this.NormaliseVersion(release.Tag), $"https://github.com/{id}/releases");
- }
- catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound)
- {
- return new ModInfoModel("Found no mod with this ID.");
+ GitRelease release = await this.Client.GetLatestReleaseAsync(id);
+ return release != null
+ ? new ModInfoModel(id, this.NormaliseVersion(release.Tag), $"https://github.com/{id}/releases")
+ : new ModInfoModel("Found no mod with this ID.");
}
catch (Exception ex)
{
@@ -74,24 +54,5 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
{
this.Client.Dispose();
}
-
-
- /*********
- ** Private models
- *********/
- /// <summary>Metadata about a GitHub release tag.</summary>
- private class GitRelease
- {
- /*********
- ** Accessors
- *********/
- /// <summary>The display name.</summary>
- [JsonProperty("name")]
- public string Name { get; set; }
-
- /// <summary>The semantic version string.</summary>
- [JsonProperty("tag_name")]
- public string Tag { get; set; }
- }
}
}
diff --git a/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs
index 8a4bb0d8..cfa757ab 100644
--- a/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs
+++ b/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs
@@ -1,8 +1,7 @@
using System;
using System.Threading.Tasks;
-using Newtonsoft.Json;
-using Pathoschild.Http.Client;
using StardewModdingAPI.Common.Models;
+using StardewModdingAPI.Web.Framework.Clients.Nexus;
namespace StardewModdingAPI.Web.Framework.ModRepositories
{
@@ -12,11 +11,8 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
/*********
** Properties
*********/
- /// <summary>The URL for a Nexus Mods API query excluding the base URL, where {0} is the mod ID.</summary>
- private readonly string ModUrlFormat;
-
- /// <summary>The underlying HTTP client.</summary>
- private readonly IClient Client;
+ /// <summary>The underlying Nexus Mods API client.</summary>
+ private readonly INexusClient Client;
/*********
@@ -24,14 +20,11 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
*********/
/// <summary>Construct an instance.</summary>
/// <param name="vendorKey">The unique key for this vendor.</param>
- /// <param name="userAgent">The user agent for the Nexus Mods API client.</param>
- /// <param name="baseUrl">The base URL for the Nexus Mods API.</param>
- /// <param name="modUrlFormat">The URL for a Nexus Mods API query excluding the <paramref name="baseUrl"/>, where {0} is the mod ID.</param>
- public NexusRepository(string vendorKey, string userAgent, string baseUrl, string modUrlFormat)
+ /// <param name="client">The underlying Nexus Mods API client.</param>
+ public NexusRepository(string vendorKey, INexusClient client)
: base(vendorKey)
{
- this.ModUrlFormat = modUrlFormat;
- this.Client = new FluentClient(baseUrl).SetUserAgent(userAgent);
+ this.Client = client;
}
/// <summary>Get metadata about a mod in the repository.</summary>
@@ -39,18 +32,15 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
public override async Task<ModInfoModel> GetModInfoAsync(string id)
{
// validate ID format
- if (!uint.TryParse(id, out uint _))
+ if (!uint.TryParse(id, out uint nexusID))
return new ModInfoModel($"The value '{id}' isn't a valid Nexus mod ID, must be an integer ID.");
// fetch info
try
{
- NexusResponseModel response = await this.Client
- .GetAsync(string.Format(this.ModUrlFormat, id))
- .As<NexusResponseModel>();
-
- return response != null
- ? new ModInfoModel(response.Name, this.NormaliseVersion(response.Version), response.Url)
+ NexusMod mod = await this.Client.GetModAsync(nexusID);
+ return mod != null
+ ? new ModInfoModel(mod.Name, this.NormaliseVersion(mod.Version), mod.Url)
: new ModInfoModel("Found no mod with this ID.");
}
catch (Exception ex)
@@ -64,26 +54,5 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
{
this.Client.Dispose();
}
-
-
- /*********
- ** Private models
- *********/
- /// <summary>A mod metadata response from Nexus Mods.</summary>
- private class NexusResponseModel
- {
- /*********
- ** Accessors
- *********/
- /// <summary>The mod name.</summary>
- public string Name { get; set; }
-
- /// <summary>The mod's semantic version number.</summary>
- public string Version { get; set; }
-
- /// <summary>The mod's web URL.</summary>
- [JsonProperty("mod_page_uri")]
- public string Url { get; set; }
- }
}
}
diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs
index 2f2b0d11..7938520a 100644
--- a/src/SMAPI.Web/Startup.cs
+++ b/src/SMAPI.Web/Startup.cs
@@ -7,6 +7,9 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using StardewModdingAPI.Web.Framework;
+using StardewModdingAPI.Web.Framework.Clients.Chucklefish;
+using StardewModdingAPI.Web.Framework.Clients.GitHub;
+using StardewModdingAPI.Web.Framework.Clients.Nexus;
using StardewModdingAPI.Web.Framework.ConfigModels;
using StardewModdingAPI.Web.Framework.RewriteRules;
@@ -41,6 +44,7 @@ namespace StardewModdingAPI.Web
/// <param name="services">The service injection container.</param>
public void ConfigureServices(IServiceCollection services)
{
+ // init configuration
services
.Configure<ModUpdateCheckConfig>(this.Configuration.GetSection("ModUpdateCheck"))
.Configure<LogParserConfig>(this.Configuration.GetSection("LogParser"))
@@ -53,6 +57,32 @@ namespace StardewModdingAPI.Web
options.SerializerSettings.Formatting = Formatting.Indented;
options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
});
+
+ // init API clients
+ {
+ ApiClientsConfig api = this.Configuration.GetSection("ApiClients").Get<ApiClientsConfig>();
+ string version = this.GetType().Assembly.GetName().Version.ToString(3);
+ string userAgent = string.Format(api.UserAgent, version);
+
+ services.AddSingleton<IChucklefishClient>(new ChucklefishClient(
+ userAgent: userAgent,
+ baseUrl: api.ChucklefishBaseUrl,
+ modPageUrlFormat: api.ChucklefishModPageUrlFormat
+ ));
+ services.AddSingleton<IGitHubClient>(new GitHubClient(
+ baseUrl: api.GitHubBaseUrl,
+ releaseUrlFormat: api.GitHubReleaseUrlFormat,
+ userAgent: userAgent,
+ acceptHeader: api.GitHubAcceptHeader,
+ username: api.GitHubUsername,
+ password: api.GitHubPassword
+ ));
+ services.AddSingleton<INexusClient>(new NexusClient(
+ userAgent: api.NexusUserAgent,
+ baseUrl: api.NexusBaseUrl,
+ modUrlFormat: api.NexusModUrlFormat
+ ));
+ }
}
/// <summary>The method called by the runtime to configure the HTTP request pipeline.</summary>
diff --git a/src/SMAPI.Web/appsettings.Development.json b/src/SMAPI.Web/appsettings.Development.json
index 87c35ca9..4602d1ed 100644
--- a/src/SMAPI.Web/appsettings.Development.json
+++ b/src/SMAPI.Web/appsettings.Development.json
@@ -16,7 +16,7 @@
"Microsoft": "Information"
}
},
- "ModUpdateCheck": {
+ "ApiClients": {
"GitHubUsername": null,
"GitHubPassword": null
},
diff --git a/src/SMAPI.Web/appsettings.json b/src/SMAPI.Web/appsettings.json
index eb6ecc9b..4f70b41b 100644
--- a/src/SMAPI.Web/appsettings.json
+++ b/src/SMAPI.Web/appsettings.json
@@ -13,28 +13,31 @@
"Default": "Warning"
}
},
- "ModUpdateCheck": {
- "CacheMinutes": 60,
- "SemanticVersionRegex": "^(?>(?<major>0|[1-9]\\d*))\\.(?>(?<minor>0|[1-9]\\d*))(?>(?:\\.(?<patch>0|[1-9]\\d*))?)(?:-(?<prerelease>(?>[a-z0-9]+[\\-\\.]?)+))?$",
+ "ApiClients": {
+ "UserAgent": "SMAPI/{0} (+https://github.com/Pathoschild/SMAPI)",
- "ChucklefishKey": "Chucklefish",
- "ChucklefishUserAgent": "SMAPI/{0} (+https://github.com/Pathoschild/SMAPI)",
"ChucklefishBaseUrl": "https://community.playstarbound.com",
"ChucklefishModPageUrlFormat": "resources/{0}",
- "GitHubKey": "GitHub",
- "GitHubUserAgent": "SMAPI/{0} (+https://github.com/Pathoschild/SMAPI)",
"GitHubBaseUrl": "https://api.github.com",
"GitHubReleaseUrlFormat": "repos/{0}/releases/latest",
"GitHubAcceptHeader": "application/vnd.github.v3+json",
"GitHubUsername": null, // see top note
"GitHubPassword": null, // see top note
- "NexusKey": "Nexus",
"NexusUserAgent": "Nexus Client v0.63.15",
"NexusBaseUrl": "http://www.nexusmods.com/stardewvalley",
"NexusModUrlFormat": "mods/{0}"
},
+
+ "ModUpdateCheck": {
+ "CacheMinutes": 60,
+ "SemanticVersionRegex": "^(?>(?<major>0|[1-9]\\d*))\\.(?>(?<minor>0|[1-9]\\d*))(?>(?:\\.(?<patch>0|[1-9]\\d*))?)(?:-(?<prerelease>(?>[a-z0-9]+[\\-\\.]?)+))?$",
+
+ "ChucklefishKey": "Chucklefish",
+ "GitHubKey": "GitHub",
+ "NexusKey": "Nexus"
+ },
"LogParser": {
"SectionUrl": null, // see top note
"PastebinBaseUrl": "https://pastebin.com/",