diff options
Diffstat (limited to 'src/SMAPI.Web/Framework')
33 files changed, 611 insertions, 690 deletions
diff --git a/src/SMAPI.Web/Framework/Caching/Mods/IModCacheRepository.cs b/src/SMAPI.Web/Framework/Caching/Mods/IModCacheRepository.cs index 004202f9..0d912c7b 100644 --- a/src/SMAPI.Web/Framework/Caching/Mods/IModCacheRepository.cs +++ b/src/SMAPI.Web/Framework/Caching/Mods/IModCacheRepository.cs @@ -1,6 +1,6 @@ using System; using StardewModdingAPI.Toolkit.Framework.UpdateData; -using StardewModdingAPI.Web.Framework.ModRepositories; +using StardewModdingAPI.Web.Framework.Clients; namespace StardewModdingAPI.Web.Framework.Caching.Mods { @@ -15,13 +15,13 @@ namespace StardewModdingAPI.Web.Framework.Caching.Mods /// <param name="id">The mod's unique ID within the <paramref name="site"/>.</param> /// <param name="mod">The fetched mod.</param> /// <param name="markRequested">Whether to update the mod's 'last requested' date.</param> - bool TryGetMod(ModRepositoryKey site, string id, out Cached<ModInfoModel> mod, bool markRequested = true); + bool TryGetMod(ModSiteKey site, string id, out Cached<IModPage> mod, bool markRequested = true); /// <summary>Save data fetched for a mod.</summary> /// <param name="site">The mod site on which the mod is found.</param> /// <param name="id">The mod's unique ID within the <paramref name="site"/>.</param> /// <param name="mod">The mod data.</param> - void SaveMod(ModRepositoryKey site, string id, ModInfoModel mod); + void SaveMod(ModSiteKey site, string id, IModPage mod); /// <summary>Delete data for mods which haven't been requested within a given time limit.</summary> /// <param name="age">The minimum age for which to remove mods.</param> diff --git a/src/SMAPI.Web/Framework/Caching/Mods/ModCacheMemoryRepository.cs b/src/SMAPI.Web/Framework/Caching/Mods/ModCacheMemoryRepository.cs index 62461116..6b0ec1ec 100644 --- a/src/SMAPI.Web/Framework/Caching/Mods/ModCacheMemoryRepository.cs +++ b/src/SMAPI.Web/Framework/Caching/Mods/ModCacheMemoryRepository.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using StardewModdingAPI.Toolkit.Framework.UpdateData; -using StardewModdingAPI.Web.Framework.ModRepositories; +using StardewModdingAPI.Web.Framework.Clients; namespace StardewModdingAPI.Web.Framework.Caching.Mods { @@ -13,7 +13,7 @@ namespace StardewModdingAPI.Web.Framework.Caching.Mods ** Fields *********/ /// <summary>The cached mod data indexed by <c>{site key}:{ID}</c>.</summary> - private readonly IDictionary<string, Cached<ModInfoModel>> Mods = new Dictionary<string, Cached<ModInfoModel>>(StringComparer.InvariantCultureIgnoreCase); + private readonly IDictionary<string, Cached<IModPage>> Mods = new Dictionary<string, Cached<IModPage>>(StringComparer.InvariantCultureIgnoreCase); /********* @@ -24,7 +24,7 @@ namespace StardewModdingAPI.Web.Framework.Caching.Mods /// <param name="id">The mod's unique ID within the <paramref name="site"/>.</param> /// <param name="mod">The fetched mod.</param> /// <param name="markRequested">Whether to update the mod's 'last requested' date.</param> - public bool TryGetMod(ModRepositoryKey site, string id, out Cached<ModInfoModel> mod, bool markRequested = true) + public bool TryGetMod(ModSiteKey site, string id, out Cached<IModPage> mod, bool markRequested = true) { // get mod if (!this.Mods.TryGetValue(this.GetKey(site, id), out var cachedMod)) @@ -45,10 +45,10 @@ namespace StardewModdingAPI.Web.Framework.Caching.Mods /// <param name="site">The mod site on which the mod is found.</param> /// <param name="id">The mod's unique ID within the <paramref name="site"/>.</param> /// <param name="mod">The mod data.</param> - public void SaveMod(ModRepositoryKey site, string id, ModInfoModel mod) + public void SaveMod(ModSiteKey site, string id, IModPage mod) { string key = this.GetKey(site, id); - this.Mods[key] = new Cached<ModInfoModel>(mod); + this.Mods[key] = new Cached<IModPage>(mod); } /// <summary>Delete data for mods which haven't been requested within a given time limit.</summary> @@ -73,7 +73,7 @@ namespace StardewModdingAPI.Web.Framework.Caching.Mods /// <summary>Get a cache key.</summary> /// <param name="site">The mod site.</param> /// <param name="id">The mod ID.</param> - private string GetKey(ModRepositoryKey site, string id) + private string GetKey(ModSiteKey site, string id) { return $"{site}:{id.Trim()}".ToLower(); } diff --git a/src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishClient.cs b/src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishClient.cs index cdb281e2..ca156da4 100644 --- a/src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishClient.cs +++ b/src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishClient.cs @@ -3,6 +3,7 @@ using System.Net; using System.Threading.Tasks; using HtmlAgilityPack; using Pathoschild.Http.Client; +using StardewModdingAPI.Toolkit.Framework.UpdateData; namespace StardewModdingAPI.Web.Framework.Clients.Chucklefish { @@ -20,6 +21,13 @@ namespace StardewModdingAPI.Web.Framework.Clients.Chucklefish /********* + ** Accessors + *********/ + /// <summary>The unique key for the mod site.</summary> + public ModSiteKey SiteKey => ModSiteKey.Chucklefish; + + + /********* ** Public methods *********/ /// <summary>Construct an instance.</summary> @@ -32,42 +40,40 @@ namespace StardewModdingAPI.Web.Framework.Clients.Chucklefish 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) + /// <summary>Get update check info about a mod.</summary> + /// <param name="id">The mod ID.</param> + public async Task<IModPage> GetModData(string id) { + IModPage page = new GenericModPage(this.SiteKey, id); + + // get mod ID + if (!uint.TryParse(id, out uint parsedId)) + return page.SetError(RemoteModStatus.DoesNotExist, $"The value '{id}' isn't a valid Chucklefish mod ID, must be an integer ID."); + // fetch HTML string html; try { html = await this.Client - .GetAsync(string.Format(this.ModPageUrlFormat, id)) + .GetAsync(string.Format(this.ModPageUrlFormat, parsedId)) .AsString(); } catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound || ex.Status == HttpStatusCode.Forbidden) { - return null; + return page.SetError(RemoteModStatus.DoesNotExist, "Found no Chucklefish mod with this ID."); } - - // parse HTML var doc = new HtmlDocument(); doc.LoadHtml(html); // extract mod info - string url = this.GetModUrl(id); + string url = this.GetModUrl(parsedId); 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 - }; + // return info + return page.SetInfo(name: name, version: version, url: url, downloads: Array.Empty<IModDownload>()); } /// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary> diff --git a/src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishMod.cs b/src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishMod.cs deleted file mode 100644 index fd0101d4..00000000 --- a/src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishMod.cs +++ /dev/null @@ -1,18 +0,0 @@ -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 index 1d8b256e..836d43f7 100644 --- a/src/SMAPI.Web/Framework/Clients/Chucklefish/IChucklefishClient.cs +++ b/src/SMAPI.Web/Framework/Clients/Chucklefish/IChucklefishClient.cs @@ -1,17 +1,7 @@ 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); - } + internal interface IChucklefishClient : IModSiteClient, IDisposable { } } diff --git a/src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeClient.cs b/src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeClient.cs index a6fd21fd..d8008721 100644 --- a/src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeClient.cs +++ b/src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeClient.cs @@ -1,8 +1,8 @@ -using System.Linq; +using System.Collections.Generic; using System.Text.RegularExpressions; using System.Threading.Tasks; using Pathoschild.Http.Client; -using StardewModdingAPI.Toolkit; +using StardewModdingAPI.Toolkit.Framework.UpdateData; using StardewModdingAPI.Web.Framework.Clients.CurseForge.ResponseModels; namespace StardewModdingAPI.Web.Framework.Clients.CurseForge @@ -21,6 +21,13 @@ namespace StardewModdingAPI.Web.Framework.Clients.CurseForge /********* + ** Accessors + *********/ + /// <summary>The unique key for the mod site.</summary> + public ModSiteKey SiteKey => ModSiteKey.CurseForge; + + + /********* ** Public methods *********/ /// <summary>Construct an instance.</summary> @@ -31,59 +38,34 @@ namespace StardewModdingAPI.Web.Framework.Clients.CurseForge this.Client = new FluentClient(apiUrl).SetUserAgent(userAgent); } - /// <summary>Get metadata about a mod.</summary> - /// <param name="id">The CurseForge mod ID.</param> - /// <returns>Returns the mod info if found, else <c>null</c>.</returns> - public async Task<CurseForgeMod> GetModAsync(long id) + /// <summary>Get update check info about a mod.</summary> + /// <param name="id">The mod ID.</param> + public async Task<IModPage> GetModData(string id) { + IModPage page = new GenericModPage(this.SiteKey, id); + + // get ID + if (!uint.TryParse(id, out uint parsedId)) + return page.SetError(RemoteModStatus.DoesNotExist, $"The value '{id}' isn't a valid CurseForge mod ID, must be an integer ID."); + // get raw data ModModel mod = await this.Client - .GetAsync($"addon/{id}") + .GetAsync($"addon/{parsedId}") .As<ModModel>(); if (mod == null) - return null; + return page.SetError(RemoteModStatus.DoesNotExist, "Found no CurseForge mod with this ID."); - // get latest versions - string invalidVersion = null; - ISemanticVersion latest = null; + // get downloads + List<IModDownload> downloads = new List<IModDownload>(); foreach (ModFileModel file in mod.LatestFiles) { - // extract version - ISemanticVersion version; - { - string raw = this.GetRawVersion(file); - if (raw == null) - continue; - - if (!SemanticVersion.TryParse(raw, out version)) - { - invalidVersion ??= raw; - continue; - } - } - - // track latest version - if (latest == null || version.IsNewerThan(latest)) - latest = version; - } - - // get error - string error = null; - if (latest == null && invalidVersion == null) - { - error = mod.LatestFiles.Any() - ? $"CurseForge mod {id} has no downloads which specify the version in a recognised format." - : $"CurseForge mod {id} has no downloads."; + downloads.Add( + new GenericModDownload(name: file.DisplayName ?? file.FileName, description: null, version: this.GetRawVersion(file)) + ); } - // generate result - return new CurseForgeMod - { - Name = mod.Name, - LatestVersion = latest?.ToString() ?? invalidVersion, - Url = mod.WebsiteUrl, - Error = error - }; + // return info + return page.SetInfo(name: mod.Name, version: null, url: mod.WebsiteUrl, downloads: downloads); } /// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary> diff --git a/src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeMod.cs b/src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeMod.cs deleted file mode 100644 index e5bb8cf1..00000000 --- a/src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeMod.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Newtonsoft.Json; - -namespace StardewModdingAPI.Web.Framework.Clients.CurseForge -{ - /// <summary>Mod metadata from the CurseForge API.</summary> - internal class CurseForgeMod - { - /********* - ** Accessors - *********/ - /// <summary>The mod name.</summary> - public string Name { get; set; } - - /// <summary>The latest file version.</summary> - public string LatestVersion { get; set; } - - /// <summary>The mod's web URL.</summary> - public string Url { get; set; } - - /// <summary>A user-friendly error which indicates why fetching the mod info failed (if applicable).</summary> - public string Error { get; set; } - } -} diff --git a/src/SMAPI.Web/Framework/Clients/CurseForge/ICurseForgeClient.cs b/src/SMAPI.Web/Framework/Clients/CurseForge/ICurseForgeClient.cs index 907b4087..2018c230 100644 --- a/src/SMAPI.Web/Framework/Clients/CurseForge/ICurseForgeClient.cs +++ b/src/SMAPI.Web/Framework/Clients/CurseForge/ICurseForgeClient.cs @@ -1,17 +1,7 @@ using System; -using System.Threading.Tasks; namespace StardewModdingAPI.Web.Framework.Clients.CurseForge { /// <summary>An HTTP client for fetching mod metadata from the CurseForge API.</summary> - internal interface ICurseForgeClient : IDisposable - { - /********* - ** Methods - *********/ - /// <summary>Get metadata about a mod.</summary> - /// <param name="id">The CurseForge mod ID.</param> - /// <returns>Returns the mod info if found, else <c>null</c>.</returns> - Task<CurseForgeMod> GetModAsync(long id); - } + internal interface ICurseForgeClient : IModSiteClient, IDisposable { } } diff --git a/src/SMAPI.Web/Framework/Clients/GenericModDownload.cs b/src/SMAPI.Web/Framework/Clients/GenericModDownload.cs new file mode 100644 index 00000000..f08b471c --- /dev/null +++ b/src/SMAPI.Web/Framework/Clients/GenericModDownload.cs @@ -0,0 +1,36 @@ +namespace StardewModdingAPI.Web.Framework.Clients +{ + /// <summary>Generic metadata about a file download on a mod page.</summary> + internal class GenericModDownload : IModDownload + { + /********* + ** Accessors + *********/ + /// <summary>The download's display name.</summary> + public string Name { get; set; } + + /// <summary>The download's description.</summary> + public string Description { get; set; } + + /// <summary>The download's file version.</summary> + public string Version { get; set; } + + + /********* + ** Public methods + *********/ + /// <summary>Construct an empty instance.</summary> + public GenericModDownload() { } + + /// <summary>Construct an instance.</summary> + /// <param name="name">The download's display name.</param> + /// <param name="description">The download's description.</param> + /// <param name="version">The download's file version.</param> + public GenericModDownload(string name, string description, string version) + { + this.Name = name; + this.Description = description; + this.Version = version; + } + } +} diff --git a/src/SMAPI.Web/Framework/Clients/GenericModPage.cs b/src/SMAPI.Web/Framework/Clients/GenericModPage.cs new file mode 100644 index 00000000..622e6c56 --- /dev/null +++ b/src/SMAPI.Web/Framework/Clients/GenericModPage.cs @@ -0,0 +1,79 @@ +using System.Collections.Generic; +using System.Linq; +using StardewModdingAPI.Toolkit.Framework.UpdateData; + +namespace StardewModdingAPI.Web.Framework.Clients +{ + /// <summary>Generic metadata about a mod page.</summary> + internal class GenericModPage : IModPage + { + /********* + ** Accessors + *********/ + /// <summary>The mod site containing the mod.</summary> + public ModSiteKey Site { get; set; } + + /// <summary>The mod's unique ID within the site.</summary> + public string Id { get; set; } + + /// <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; } + + /// <summary>The mod downloads.</summary> + public IModDownload[] Downloads { get; set; } = new IModDownload[0]; + + /// <summary>The mod availability status on the remote site.</summary> + public RemoteModStatus Status { get; set; } = RemoteModStatus.Ok; + + /// <summary>A user-friendly error which indicates why fetching the mod info failed (if applicable).</summary> + public string Error { get; set; } + + + /********* + ** Public methods + *********/ + /// <summary>Construct an empty instance.</summary> + public GenericModPage() { } + + /// <summary>Construct an instance.</summary> + /// <param name="site">The mod site containing the mod.</param> + /// <param name="id">The mod's unique ID within the site.</param> + public GenericModPage(ModSiteKey site, string id) + { + this.Site = site; + this.Id = id; + } + + /// <summary>Set the fetched mod info.</summary> + /// <param name="name">The mod name.</param> + /// <param name="version">The mod's semantic version number.</param> + /// <param name="url">The mod's web URL.</param> + /// <param name="downloads">The mod downloads.</param> + public IModPage SetInfo(string name, string version, string url, IEnumerable<IModDownload> downloads) + { + this.Name = name; + this.Version = version; + this.Url = url; + this.Downloads = downloads.ToArray(); + + return this; + } + + /// <summary>Set a mod fetch error.</summary> + /// <param name="status">The mod availability status on the remote site.</param> + /// <param name="error">A user-friendly error which indicates why fetching the mod info failed (if applicable).</param> + public IModPage SetError(RemoteModStatus status, string error) + { + this.Status = status; + this.Error = error; + + return this; + } + } +} diff --git a/src/SMAPI.Web/Framework/Clients/GitHub/GitHubClient.cs b/src/SMAPI.Web/Framework/Clients/GitHub/GitHubClient.cs index 84c20957..2f1eb854 100644 --- a/src/SMAPI.Web/Framework/Clients/GitHub/GitHubClient.cs +++ b/src/SMAPI.Web/Framework/Clients/GitHub/GitHubClient.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Net; using System.Threading.Tasks; using Pathoschild.Http.Client; +using StardewModdingAPI.Toolkit.Framework.UpdateData; namespace StardewModdingAPI.Web.Framework.Clients.GitHub { @@ -17,6 +18,13 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub /********* + ** Accessors + *********/ + /// <summary>The unique key for the mod site.</summary> + public ModSiteKey SiteKey => ModSiteKey.GitHub; + + + /********* ** Public methods *********/ /// <summary>Construct an instance.</summary> @@ -79,6 +87,54 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub } } + /// <summary>Get update check info about a mod.</summary> + /// <param name="id">The mod ID.</param> + public async Task<IModPage> GetModData(string id) + { + IModPage page = new GenericModPage(this.SiteKey, id); + + if (!id.Contains("/") || id.IndexOf("/", StringComparison.OrdinalIgnoreCase) != id.LastIndexOf("/", StringComparison.OrdinalIgnoreCase)) + return page.SetError(RemoteModStatus.DoesNotExist, $"The value '{id}' isn't a valid GitHub mod ID, must be a username and project name like 'Pathoschild/SMAPI'."); + + // fetch repo info + GitRepo repository = await this.GetRepositoryAsync(id); + if (repository == null) + return page.SetError(RemoteModStatus.DoesNotExist, "Found no GitHub repository for this ID."); + string name = repository.FullName; + string url = $"{repository.WebUrl}/releases"; + + // get releases + GitRelease latest; + GitRelease preview; + { + // get latest release (whether preview or stable) + latest = await this.GetLatestReleaseAsync(id, includePrerelease: true); + if (latest == null) + return page.SetError(RemoteModStatus.DoesNotExist, "Found no GitHub release for this ID."); + + // get stable version if different + preview = null; + if (latest.IsPrerelease) + { + GitRelease release = await this.GetLatestReleaseAsync(id, includePrerelease: false); + if (release != null) + { + preview = latest; + latest = release; + } + } + } + + // get downloads + IModDownload[] downloads = new[] { latest, preview } + .Where(release => release != null) + .Select(release => (IModDownload)new GenericModDownload(release.Name, release.Body, release.Tag)) + .ToArray(); + + // return info + return page.SetInfo(name: name, url: url, version: null, downloads: downloads); + } + /// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary> public void Dispose() { diff --git a/src/SMAPI.Web/Framework/Clients/GitHub/IGitHubClient.cs b/src/SMAPI.Web/Framework/Clients/GitHub/IGitHubClient.cs index a34f03bd..0d6f4643 100644 --- a/src/SMAPI.Web/Framework/Clients/GitHub/IGitHubClient.cs +++ b/src/SMAPI.Web/Framework/Clients/GitHub/IGitHubClient.cs @@ -4,7 +4,7 @@ using System.Threading.Tasks; namespace StardewModdingAPI.Web.Framework.Clients.GitHub { /// <summary>An HTTP client for fetching metadata from GitHub.</summary> - internal interface IGitHubClient : IDisposable + internal interface IGitHubClient : IModSiteClient, IDisposable { /********* ** Methods diff --git a/src/SMAPI.Web/Framework/Clients/IModSiteClient.cs b/src/SMAPI.Web/Framework/Clients/IModSiteClient.cs new file mode 100644 index 00000000..33277711 --- /dev/null +++ b/src/SMAPI.Web/Framework/Clients/IModSiteClient.cs @@ -0,0 +1,23 @@ +using System.Threading.Tasks; +using StardewModdingAPI.Toolkit.Framework.UpdateData; + +namespace StardewModdingAPI.Web.Framework.Clients +{ + /// <summary>A client for fetching update check info from a mo |
