From d3f0c8e4d2d9ada099cba67c359c5df1d69a1257 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 24 Sep 2017 01:10:17 -0400 Subject: add support for update checks from the Chucklefish mod site (#336) --- release-notes.md | 2 +- .../Controllers/ModsController.cs | 11 ++- .../Framework/ConfigModels/ModUpdateCheckConfig.cs | 18 ++++- .../ModRepositories/ChucklefishRepository.cs | 94 ++++++++++++++++++++++ .../Framework/ModRepositories/GitHubRepository.cs | 4 +- .../StardewModdingAPI.Web.csproj | 1 + src/StardewModdingAPI.Web/appsettings.json | 7 +- src/StardewModdingAPI/Framework/Models/Manifest.cs | 3 + src/StardewModdingAPI/IManifest.cs | 7 +- src/StardewModdingAPI/Program.cs | 2 + 10 files changed, 140 insertions(+), 9 deletions(-) create mode 100644 src/StardewModdingAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs diff --git a/release-notes.md b/release-notes.md index a07de71f..41d946c7 100644 --- a/release-notes.md +++ b/release-notes.md @@ -14,7 +14,7 @@ For players: For mod developers: * Added new APIs to edit, inject, and reload XNB assets loaded by the game at any time. _This let mods do anything previously only possible with XNB mods, plus enables new mod scenarios (e.g. seasonal textures, NPC clothing that depend on the weather or location, etc)._ -* Added new manifest fields to enable automatic update checks. +* Added support for automatic update checks from Chucklefish, GitHub, or Nexus Mods. * Added new input events. _The new `InputEvents` combine keyboard + mouse + controller input into one event for easy handling, add metadata like the cursor position and grab tile to support click handling, and add an option to suppress input from the game to enable new scenarios like action highjacking and UI overlays._ * Added support for optional dependencies. diff --git a/src/StardewModdingAPI.Web/Controllers/ModsController.cs b/src/StardewModdingAPI.Web/Controllers/ModsController.cs index 8fc2cb51..c5c79600 100644 --- a/src/StardewModdingAPI.Web/Controllers/ModsController.cs +++ b/src/StardewModdingAPI.Web/Controllers/ModsController.cs @@ -5,9 +5,9 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; +using StardewModdingAPI.Models; using StardewModdingAPI.Web.Framework.ConfigModels; using StardewModdingAPI.Web.Framework.ModRepositories; -using StardewModdingAPI.Models; namespace StardewModdingAPI.Web.Controllers { @@ -41,14 +41,21 @@ namespace StardewModdingAPI.Web.Controllers this.Cache = cache; this.CacheMinutes = config.CacheMinutes; + 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: config.GitHubUserAgent, + userAgent: string.Format(config.GitHubUserAgent, version), acceptHeader: config.GitHubAcceptHeader, username: config.GitHubUsername, password: config.GitHubPassword diff --git a/src/StardewModdingAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs b/src/StardewModdingAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs index 5d55ba18..8ca555a3 100644 --- a/src/StardewModdingAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs +++ b/src/StardewModdingAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs @@ -12,13 +12,29 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels /// The number of minutes update checks should be cached before refetching them. public int CacheMinutes { get; set; } + /**** + ** Chucklefish mod site + ****/ + /// The repository key for the Chucklefish mod site. + public string ChucklefishKey { get; set; } + + /// The user agent for the Chucklefish API client, where {0} is the SMAPI version. + public string ChucklefishUserAgent { get; set; } + + /// The base URL for the Chucklefish mod site. + public string ChucklefishBaseUrl { get; set; } + + /// The URL for a mod page on the Chucklefish mod site excluding the , where {0} is the mod ID. + public string ChucklefishModPageUrlFormat { get; set; } + + /**** ** GitHub ****/ /// The repository key for Nexus Mods. public string GitHubKey { get; set; } - /// The user agent for the GitHub API client. + /// The user agent for the GitHub API client, where {0} is the SMAPI version. public string GitHubUserAgent { get; set; } /// The base URL for the GitHub API. diff --git a/src/StardewModdingAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs b/src/StardewModdingAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs new file mode 100644 index 00000000..59d7f3ba --- /dev/null +++ b/src/StardewModdingAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs @@ -0,0 +1,94 @@ +using System; +using System.Net; +using System.Threading.Tasks; +using HtmlAgilityPack; +using Pathoschild.Http.Client; +using StardewModdingAPI.Models; + +namespace StardewModdingAPI.Web.Framework.ModRepositories +{ + /// An HTTP client for fetching mod metadata from the Chucklefish mod site. + internal class ChucklefishRepository : IModRepository + { + /********* + ** Properties + *********/ + /// The underlying HTTP client. + private readonly IClient Client; + + + /********* + ** Accessors + *********/ + /// The unique key for this vendor. + public string VendorKey { get; } + + /// The base URL for the Chucklefish mod site. + public string BaseUrl { get; } + + /// The URL for a mod page excluding the base URL, where {0} is the mod ID. + public string ModPageUrlFormat { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The unique key for this vendor. + /// The user agent for the API client. + /// The base URL for the Chucklefish mod site. + /// The URL for a mod page excluding the , where {0} is the mod ID. + public ChucklefishRepository(string vendorKey, string userAgent, string baseUrl, string modPageUrlFormat) + { + this.VendorKey = vendorKey; + this.BaseUrl = baseUrl; + this.ModPageUrlFormat = modPageUrlFormat; + this.Client = new FluentClient(baseUrl).SetUserAgent(userAgent); + } + + /// Get metadata about a mod in the repository. + /// The mod ID in this repository. + public async Task GetModInfoAsync(string id) + { + 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) + { + 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, version, url); + } + catch (Exception ex) + { + return new ModInfoModel(ex.ToString()); + } + } + + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + public void Dispose() + { + this.Client.Dispose(); + } + } +} diff --git a/src/StardewModdingAPI.Web/Framework/ModRepositories/GitHubRepository.cs b/src/StardewModdingAPI.Web/Framework/ModRepositories/GitHubRepository.cs index 421220de..b08e8b4d 100644 --- a/src/StardewModdingAPI.Web/Framework/ModRepositories/GitHubRepository.cs +++ b/src/StardewModdingAPI.Web/Framework/ModRepositories/GitHubRepository.cs @@ -33,7 +33,7 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories /// The unique key for this vendor. /// The base URL for the Nexus Mods API. /// The URL for a Nexus Mods API query excluding the , where {0} is the mod ID. - /// The user agent for the GitHub API client. + /// The user agent for the API client. /// The Accept header value expected by the GitHub API. /// The username with which to authenticate to the GitHub API. /// The password with which to authenticate to the GitHub API. @@ -43,7 +43,7 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories this.ReleaseUrlFormat = releaseUrlFormat; this.Client = new FluentClient(baseUrl) - .SetUserAgent(string.Format(userAgent, this.GetType().Assembly.GetName().Version)) + .SetUserAgent(userAgent) .AddDefault(req => req.WithHeader("Accept", acceptHeader)); if (!string.IsNullOrWhiteSpace(username)) this.Client = this.Client.SetBasicAuthentication(username, password); diff --git a/src/StardewModdingAPI.Web/StardewModdingAPI.Web.csproj b/src/StardewModdingAPI.Web/StardewModdingAPI.Web.csproj index c30abc55..bf67449b 100644 --- a/src/StardewModdingAPI.Web/StardewModdingAPI.Web.csproj +++ b/src/StardewModdingAPI.Web/StardewModdingAPI.Web.csproj @@ -5,6 +5,7 @@ + diff --git a/src/StardewModdingAPI.Web/appsettings.json b/src/StardewModdingAPI.Web/appsettings.json index 29fb195e..a0d4d078 100644 --- a/src/StardewModdingAPI.Web/appsettings.json +++ b/src/StardewModdingAPI.Web/appsettings.json @@ -8,6 +8,11 @@ "ModUpdateCheck": { "CacheMinutes": 60, + "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", @@ -20,5 +25,5 @@ "NexusUserAgent": "Nexus Client v0.63.15", "NexusBaseUrl": "http://www.nexusmods.com/stardewvalley", "NexusModUrlFormat": "mods/{0}" - } + } } diff --git a/src/StardewModdingAPI/Framework/Models/Manifest.cs b/src/StardewModdingAPI/Framework/Models/Manifest.cs index 2e9566bf..c891644f 100644 --- a/src/StardewModdingAPI/Framework/Models/Manifest.cs +++ b/src/StardewModdingAPI/Framework/Models/Manifest.cs @@ -34,6 +34,9 @@ namespace StardewModdingAPI.Framework.Models [JsonConverter(typeof(SFieldConverter))] public IManifestDependency[] Dependencies { get; set; } + /// The mod's unique ID in the Chucklefish mod site (if any), used for update checks. + public string ChucklefishID { get; set; } + /// The mod's unique ID in Nexus Mods (if any), used for update checks. public string NexusID { get; set; } diff --git a/src/StardewModdingAPI/IManifest.cs b/src/StardewModdingAPI/IManifest.cs index 28f6570c..9513834a 100644 --- a/src/StardewModdingAPI/IManifest.cs +++ b/src/StardewModdingAPI/IManifest.cs @@ -32,11 +32,14 @@ namespace StardewModdingAPI /// The other mods that must be loaded before this mod. IManifestDependency[] Dependencies { get; } + /// The mod's unique ID in the Chucklefish mod site (if any), used for update checks. + string ChucklefishID { get; } + /// The mod's unique ID in Nexus Mods (if any), used for update checks. - string NexusID { get; set; } + string NexusID { get; } /// The mod's organisation and project name on GitHub (if any), used for update checks. - string GitHubProject { get; set; } + string GitHubProject { get; } /// Any manifest fields which didn't match a valid field. IDictionary ExtraFields { get; } diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 4860968c..f821b559 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -522,6 +522,8 @@ namespace StardewModdingAPI IDictionary modsByKey = new Dictionary(StringComparer.InvariantCultureIgnoreCase); foreach (IModMetadata mod in mods) { + if (!string.IsNullOrWhiteSpace(mod.Manifest.ChucklefishID)) + modsByKey[$"Chucklefish:{mod.Manifest.ChucklefishID}"] = mod; if (!string.IsNullOrWhiteSpace(mod.Manifest.NexusID)) modsByKey[$"Nexus:{mod.Manifest.NexusID}"] = mod; if (!string.IsNullOrWhiteSpace(mod.Manifest.GitHubProject)) -- cgit