add support for update checks from the Chucklefish mod site (#336)

This commit is contained in:
Jesse Plamondon-Willard 2017-09-24 01:10:17 -04:00
parent 0863f9b7e5
commit d3f0c8e4d2
10 changed files with 140 additions and 9 deletions

View File

@ -14,7 +14,7 @@ For players:
For mod developers: For mod developers:
* Added new APIs to edit, inject, and reload XNB assets loaded by the game at any time. * Added new APIs to edit, inject, and reload XNB assets loaded by the game at any time.
<small>_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)._</small> <small>_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)._</small>
* 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. * Added new input events.
<small>_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._</small> <small>_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._</small>
* Added support for optional dependencies. * Added support for optional dependencies.

View File

@ -5,9 +5,9 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using StardewModdingAPI.Models;
using StardewModdingAPI.Web.Framework.ConfigModels; using StardewModdingAPI.Web.Framework.ConfigModels;
using StardewModdingAPI.Web.Framework.ModRepositories; using StardewModdingAPI.Web.Framework.ModRepositories;
using StardewModdingAPI.Models;
namespace StardewModdingAPI.Web.Controllers namespace StardewModdingAPI.Web.Controllers
{ {
@ -41,14 +41,21 @@ namespace StardewModdingAPI.Web.Controllers
this.Cache = cache; this.Cache = cache;
this.CacheMinutes = config.CacheMinutes; this.CacheMinutes = config.CacheMinutes;
string version = this.GetType().Assembly.GetName().Version.ToString(3);
this.Repositories = this.Repositories =
new IModRepository[] new IModRepository[]
{ {
new ChucklefishRepository(
vendorKey: config.ChucklefishKey,
userAgent: string.Format(config.ChucklefishUserAgent, version),
baseUrl: config.ChucklefishBaseUrl,
modPageUrlFormat: config.ChucklefishModPageUrlFormat
),
new GitHubRepository( new GitHubRepository(
vendorKey: config.GitHubKey, vendorKey: config.GitHubKey,
baseUrl: config.GitHubBaseUrl, baseUrl: config.GitHubBaseUrl,
releaseUrlFormat: config.GitHubReleaseUrlFormat, releaseUrlFormat: config.GitHubReleaseUrlFormat,
userAgent: config.GitHubUserAgent, userAgent: string.Format(config.GitHubUserAgent, version),
acceptHeader: config.GitHubAcceptHeader, acceptHeader: config.GitHubAcceptHeader,
username: config.GitHubUsername, username: config.GitHubUsername,
password: config.GitHubPassword password: config.GitHubPassword

View File

@ -12,13 +12,29 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels
/// <summary>The number of minutes update checks should be cached before refetching them.</summary> /// <summary>The number of minutes update checks should be cached before refetching them.</summary>
public int CacheMinutes { get; set; } public int CacheMinutes { 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 ** GitHub
****/ ****/
/// <summary>The repository key for Nexus Mods.</summary> /// <summary>The repository key for Nexus Mods.</summary>
public string GitHubKey { get; set; } public string GitHubKey { get; set; }
/// <summary>The user agent for the GitHub API client.</summary> /// <summary>The user agent for the GitHub API client, where {0} is the SMAPI version.</summary>
public string GitHubUserAgent { get; set; } public string GitHubUserAgent { get; set; }
/// <summary>The base URL for the GitHub API.</summary> /// <summary>The base URL for the GitHub API.</summary>

View File

@ -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
{
/// <summary>An HTTP client for fetching mod metadata from the Chucklefish mod site.</summary>
internal class ChucklefishRepository : IModRepository
{
/*********
** Properties
*********/
/// <summary>The underlying HTTP client.</summary>
private readonly IClient Client;
/*********
** Accessors
*********/
/// <summary>The unique key for this vendor.</summary>
public string VendorKey { get; }
/// <summary>The base URL for the Chucklefish mod site.</summary>
public string BaseUrl { get; }
/// <summary>The URL for a mod page excluding the base URL, where {0} is the mod ID.</summary>
public string ModPageUrlFormat { get; }
/*********
** Public methods
*********/
/// <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)
{
this.VendorKey = vendorKey;
this.BaseUrl = baseUrl;
this.ModPageUrlFormat = modPageUrlFormat;
this.Client = new FluentClient(baseUrl).SetUserAgent(userAgent);
}
/// <summary>Get metadata about a mod in the repository.</summary>
/// <param name="id">The mod ID in this repository.</param>
public async Task<ModInfoModel> 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());
}
}
/// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
public void Dispose()
{
this.Client.Dispose();
}
}
}

View File

@ -33,7 +33,7 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
/// <param name="vendorKey">The unique key for this vendor.</param> /// <param name="vendorKey">The unique key for this vendor.</param>
/// <param name="baseUrl">The base URL for the Nexus Mods API.</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="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 GitHub API client.</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="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="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> /// <param name="password">The password with which to authenticate to the GitHub API.</param>
@ -43,7 +43,7 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
this.ReleaseUrlFormat = releaseUrlFormat; this.ReleaseUrlFormat = releaseUrlFormat;
this.Client = new FluentClient(baseUrl) this.Client = new FluentClient(baseUrl)
.SetUserAgent(string.Format(userAgent, this.GetType().Assembly.GetName().Version)) .SetUserAgent(userAgent)
.AddDefault(req => req.WithHeader("Accept", acceptHeader)); .AddDefault(req => req.WithHeader("Accept", acceptHeader));
if (!string.IsNullOrWhiteSpace(username)) if (!string.IsNullOrWhiteSpace(username))
this.Client = this.Client.SetBasicAuthentication(username, password); this.Client = this.Client.SetBasicAuthentication(username, password);

View File

@ -5,6 +5,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="HtmlAgilityPack" Version="1.5.5" />
<PackageReference Include="Microsoft.AspNetCore" Version="2.0.0" /> <PackageReference Include="Microsoft.AspNetCore" Version="2.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.0.0" /> <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Rewrite" Version="2.0.0" /> <PackageReference Include="Microsoft.AspNetCore.Rewrite" Version="2.0.0" />

View File

@ -8,6 +8,11 @@
"ModUpdateCheck": { "ModUpdateCheck": {
"CacheMinutes": 60, "CacheMinutes": 60,
"ChucklefishKey": "Chucklefish",
"ChucklefishUserAgent": "SMAPI/{0} (+https://github.com/Pathoschild/SMAPI)",
"ChucklefishBaseUrl": "https://community.playstarbound.com",
"ChucklefishModPageUrlFormat": "resources/{0}",
"GitHubKey": "GitHub", "GitHubKey": "GitHub",
"GitHubUserAgent": "SMAPI/{0} (+https://github.com/Pathoschild/SMAPI)", "GitHubUserAgent": "SMAPI/{0} (+https://github.com/Pathoschild/SMAPI)",
"GitHubBaseUrl": "https://api.github.com", "GitHubBaseUrl": "https://api.github.com",
@ -20,5 +25,5 @@
"NexusUserAgent": "Nexus Client v0.63.15", "NexusUserAgent": "Nexus Client v0.63.15",
"NexusBaseUrl": "http://www.nexusmods.com/stardewvalley", "NexusBaseUrl": "http://www.nexusmods.com/stardewvalley",
"NexusModUrlFormat": "mods/{0}" "NexusModUrlFormat": "mods/{0}"
} }
} }

View File

@ -34,6 +34,9 @@ namespace StardewModdingAPI.Framework.Models
[JsonConverter(typeof(SFieldConverter))] [JsonConverter(typeof(SFieldConverter))]
public IManifestDependency[] Dependencies { get; set; } public IManifestDependency[] Dependencies { get; set; }
/// <summary>The mod's unique ID in the Chucklefish mod site (if any), used for update checks.</summary>
public string ChucklefishID { get; set; }
/// <summary>The mod's unique ID in Nexus Mods (if any), used for update checks.</summary> /// <summary>The mod's unique ID in Nexus Mods (if any), used for update checks.</summary>
public string NexusID { get; set; } public string NexusID { get; set; }

View File

@ -32,11 +32,14 @@ namespace StardewModdingAPI
/// <summary>The other mods that must be loaded before this mod.</summary> /// <summary>The other mods that must be loaded before this mod.</summary>
IManifestDependency[] Dependencies { get; } IManifestDependency[] Dependencies { get; }
/// <summary>The mod's unique ID in the Chucklefish mod site (if any), used for update checks.</summary>
string ChucklefishID { get; }
/// <summary>The mod's unique ID in Nexus Mods (if any), used for update checks.</summary> /// <summary>The mod's unique ID in Nexus Mods (if any), used for update checks.</summary>
string NexusID { get; set; } string NexusID { get; }
/// <summary>The mod's organisation and project name on GitHub (if any), used for update checks.</summary> /// <summary>The mod's organisation and project name on GitHub (if any), used for update checks.</summary>
string GitHubProject { get; set; } string GitHubProject { get; }
/// <summary>Any manifest fields which didn't match a valid field.</summary> /// <summary>Any manifest fields which didn't match a valid field.</summary>
IDictionary<string, object> ExtraFields { get; } IDictionary<string, object> ExtraFields { get; }

View File

@ -522,6 +522,8 @@ namespace StardewModdingAPI
IDictionary<string, IModMetadata> modsByKey = new Dictionary<string, IModMetadata>(StringComparer.InvariantCultureIgnoreCase); IDictionary<string, IModMetadata> modsByKey = new Dictionary<string, IModMetadata>(StringComparer.InvariantCultureIgnoreCase);
foreach (IModMetadata mod in mods) foreach (IModMetadata mod in mods)
{ {
if (!string.IsNullOrWhiteSpace(mod.Manifest.ChucklefishID))
modsByKey[$"Chucklefish:{mod.Manifest.ChucklefishID}"] = mod;
if (!string.IsNullOrWhiteSpace(mod.Manifest.NexusID)) if (!string.IsNullOrWhiteSpace(mod.Manifest.NexusID))
modsByKey[$"Nexus:{mod.Manifest.NexusID}"] = mod; modsByKey[$"Nexus:{mod.Manifest.NexusID}"] = mod;
if (!string.IsNullOrWhiteSpace(mod.Manifest.GitHubProject)) if (!string.IsNullOrWhiteSpace(mod.Manifest.GitHubProject))