diff options
6 files changed, 226 insertions, 1 deletions
diff --git a/src/SMAPI.Web/StardewModdingAPI.Web.csproj b/src/SMAPI.Web/StardewModdingAPI.Web.csproj index 202a8376..9210565a 100644 --- a/src/SMAPI.Web/StardewModdingAPI.Web.csproj +++ b/src/SMAPI.Web/StardewModdingAPI.Web.csproj @@ -17,7 +17,7 @@ <PackageReference Include="Microsoft.AspNetCore.Rewrite" Version="2.0.2" /> <PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.0.2" /> <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="2.0.1" /> - <PackageReference Include="Pathoschild.Http.FluentClient" Version="3.1.0" /> + <PackageReference Include="Pathoschild.Http.FluentClient" Version="3.2.0" /> </ItemGroup> <ItemGroup> <DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="1.0.1" /> diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityClient.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityClient.cs new file mode 100644 index 00000000..be03a120 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityClient.cs @@ -0,0 +1,142 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading.Tasks; +using HtmlAgilityPack; +using Pathoschild.Http.Client; + +namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki +{ + /// <summary>An HTTP client for fetching mod metadata from the wiki compatibility list.</summary> + public class WikiCompatibilityClient : IDisposable + { + /********* + ** Properties + *********/ + /// <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 wiki API.</param> + /// <param name="baseUrl">The base URL for the wiki API.</param> + public WikiCompatibilityClient(string userAgent, string baseUrl = "https://stardewvalleywiki.com/mediawiki/api.php") + { + this.Client = new FluentClient(baseUrl).SetUserAgent(userAgent); + } + + /// <summary>Fetch mod compatibility entries.</summary> + public async Task<WikiCompatibilityEntry[]> FetchAsync() + { + // fetch HTML + ResponseModel response = await this.Client + .GetAsync("") + .WithArguments(new + { + action = "parse", + page = "Modding:SMAPI_compatibility", + format = "json" + }) + .As<ResponseModel>(); + string html = response.Parse.Text["*"]; + + // parse HTML + var doc = new HtmlDocument(); + doc.LoadHtml(html); + + // find mod entries + HtmlNodeCollection modNodes = doc.DocumentNode.SelectNodes("table[@id='mod-list']//tr[@class='mod']"); + if (modNodes == null) + throw new InvalidOperationException("Can't parse wiki compatibility list, no mods found."); + + // parse + return this.ParseEntries(modNodes).ToArray(); + } + + /// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary> + public void Dispose() + { + this.Client?.Dispose(); + } + + + /********* + ** Private methods + *********/ + /// <summary>Parse valid mod compatibility entries.</summary> + /// <param name="nodes">The HTML compatibility entries.</param> + private IEnumerable<WikiCompatibilityEntry> ParseEntries(IEnumerable<HtmlNode> nodes) + { + foreach (HtmlNode node in nodes) + { + // parse status + WikiCompatibilityStatus status; + { + string rawStatus = node.GetAttributeValue("data-status", null); + if (rawStatus == null) + continue; // not a mod node? + if (!Enum.TryParse(rawStatus, true, out status)) + throw new InvalidOperationException($"Unknown status '{rawStatus}' when parsing compatibility list."); + } + + // parse unofficial version + ISemanticVersion unofficialVersion = null; + { + string rawUnofficialVersion = node.GetAttributeValue("data-unofficial-version", null); + SemanticVersion.TryParse(rawUnofficialVersion, out unofficialVersion); + } + + // parse other fields + int? nexusID = this.GetNullableIntAttribute(node, "data-nexus-id"); + int? chucklefishID = this.GetNullableIntAttribute(node, "data-chucklefish-id"); + string githubRepo = node.GetAttributeValue("data-github", null); + string customSourceUrl = node.GetAttributeValue("data-custom-source", null); + + // yield model + yield return new WikiCompatibilityEntry + { + Status = status, + NexusID = nexusID, + ChucklefishID = chucklefishID, + GitHubRepo = githubRepo, + CustomSourceUrl = customSourceUrl, + UnofficialVersion = unofficialVersion + }; + } + } + + /// <summary>Get a nullable integer attribute value.</summary> + /// <param name="node">The HTML node.</param> + /// <param name="attributeName">The attribute name.</param> + private int? GetNullableIntAttribute(HtmlNode node, string attributeName) + { + string raw = node.GetAttributeValue(attributeName, null); + if (raw != null && int.TryParse(raw, out int value)) + return value; + return null; + } + + /// <summary>The response model for the MediaWiki parse API.</summary> + [SuppressMessage("ReSharper", "ClassNeverInstantiated.Local")] + [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local")] + private class ResponseModel + { + /// <summary>The parse API results.</summary> + public ResponseParseModel Parse { get; set; } + } + + /// <summary>The inner response model for the MediaWiki parse API.</summary> + [SuppressMessage("ReSharper", "ClassNeverInstantiated.Local")] + [SuppressMessage("ReSharper", "CollectionNeverUpdated.Local")] + [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local")] + private class ResponseParseModel + { + /// <summary>The parsed text.</summary> + public IDictionary<string, string> Text { get; set; } + } + } +} diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityEntry.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityEntry.cs new file mode 100644 index 00000000..ceeb2de5 --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityEntry.cs @@ -0,0 +1,24 @@ +namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki +{ + /// <summary>An entry in the mod compatibility list.</summary> + public class WikiCompatibilityEntry + { + /// <summary>The mod ID on Nexus.</summary> + public int? NexusID { get; set; } + + /// <summary>The mod ID in the Chucklefish mod repo.</summary> + public int? ChucklefishID { get; set; } + + /// <summary>The GitHub repository in the form 'owner/repo'.</summary> + public string GitHubRepo { get; set; } + + /// <summary>The URL to a non-GitHub source repo.</summary> + public string CustomSourceUrl { get; set; } + + /// <summary>The version of the latest unofficial update, if applicable.</summary> + public ISemanticVersion UnofficialVersion { get; set; } + + /// <summary>The compatibility status.</summary> + public WikiCompatibilityStatus Status { get; set; } + } +} diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityStatus.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityStatus.cs new file mode 100644 index 00000000..06252b2f --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityStatus.cs @@ -0,0 +1,24 @@ +namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki +{ + /// <summary>The compatibility status for a mod.</summary> + public enum WikiCompatibilityStatus + { + /// <summary>The mod is compatible.</summary> + Ok = 0, + + /// <summary>The mod is compatible if you use an optional official download.</summary> + Optional = 1, + + /// <summary>The mod isn't compatible, but the player can fix it or there's a good alternative.</summary> + Workaround = 2, + + /// <summary>The mod isn't compatible.</summary> + Broken = 3, + + /// <summary>The mod is no longer maintained by the author, and an unofficial update or continuation is unlikely.</summary> + Abandoned = 4, + + /// <summary>The mod is no longer needed and should be removed.</summary> + Obsolete = 5 + } +} diff --git a/src/StardewModdingAPI.Toolkit/ModToolkit.cs b/src/StardewModdingAPI.Toolkit/ModToolkit.cs new file mode 100644 index 00000000..6136186e --- /dev/null +++ b/src/StardewModdingAPI.Toolkit/ModToolkit.cs @@ -0,0 +1,33 @@ +using System.Threading.Tasks; +using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; + +namespace StardewModdingAPI.Toolkit +{ + /// <summary>A convenience wrapper for the various tools.</summary> + public class ModToolkit + { + /********* + ** Properties + *********/ + /// <summary>The default HTTP user agent for the toolkit.</summary> + private readonly string UserAgent; + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + public ModToolkit() + { + ISemanticVersion version = new SemanticVersion(this.GetType().Assembly.GetName().Version); + this.UserAgent = $"SMAPI Mod Handler Toolkit/{version}"; + } + + /// <summary>Extract mod metadata from the wiki compatibility list.</summary> + public async Task<WikiCompatibilityEntry[]> GetWikiCompatibilityListAsync() + { + var client = new WikiCompatibilityClient(this.UserAgent); + return await client.FetchAsync(); + } + } +} diff --git a/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj b/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj index 6859d825..6688a2a1 100644 --- a/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj +++ b/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj @@ -10,7 +10,9 @@ </PropertyGroup> <ItemGroup> + <PackageReference Include="HtmlAgilityPack" Version="1.7.2" /> <PackageReference Include="Newtonsoft.Json" Version="11.0.2" /> + <PackageReference Include="Pathoschild.Http.FluentClient" Version="3.2.0" /> </ItemGroup> <Import Project="..\..\build\common.targets" /> |