summaryrefslogtreecommitdiff
path: root/src/SMAPI.Web/Framework/ModSiteManager.cs
diff options
context:
space:
mode:
authorJesse Plamondon-Willard <Pathoschild@users.noreply.github.com>2020-06-20 12:43:08 -0400
committerJesse Plamondon-Willard <Pathoschild@users.noreply.github.com>2020-06-20 12:43:08 -0400
commite64ecc89f94641d4054162eff4943f660f43030f (patch)
treec43ca79f9947b3e16f946e1dc5fd1d02f70ce571 /src/SMAPI.Web/Framework/ModSiteManager.cs
parentdf6e745c6b842290338317ed1d3e969ee222998c (diff)
parentcb9ff7019995eff92104703f097856d2523e02ce (diff)
downloadSMAPI-e64ecc89f94641d4054162eff4943f660f43030f.tar.gz
SMAPI-e64ecc89f94641d4054162eff4943f660f43030f.tar.bz2
SMAPI-e64ecc89f94641d4054162eff4943f660f43030f.zip
Merge branch 'develop' into stable
Diffstat (limited to 'src/SMAPI.Web/Framework/ModSiteManager.cs')
-rw-r--r--src/SMAPI.Web/Framework/ModSiteManager.cs194
1 files changed, 194 insertions, 0 deletions
diff --git a/src/SMAPI.Web/Framework/ModSiteManager.cs b/src/SMAPI.Web/Framework/ModSiteManager.cs
new file mode 100644
index 00000000..68b4c6ac
--- /dev/null
+++ b/src/SMAPI.Web/Framework/ModSiteManager.cs
@@ -0,0 +1,194 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using StardewModdingAPI.Toolkit;
+using StardewModdingAPI.Toolkit.Framework.UpdateData;
+using StardewModdingAPI.Web.Framework.Clients;
+
+namespace StardewModdingAPI.Web.Framework
+{
+ /// <summary>Handles fetching data from mod sites.</summary>
+ internal class ModSiteManager
+ {
+ /*********
+ ** Fields
+ *********/
+ /// <summary>The mod sites which provide mod metadata.</summary>
+ private readonly IDictionary<ModSiteKey, IModSiteClient> ModSites;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="modSites">The mod sites which provide mod metadata.</param>
+ public ModSiteManager(IModSiteClient[] modSites)
+ {
+ this.ModSites = modSites.ToDictionary(p => p.SiteKey);
+ }
+
+ /// <summary>Get the mod info for an update key.</summary>
+ /// <param name="updateKey">The namespaced update key.</param>
+ public async Task<IModPage> GetModPageAsync(UpdateKey updateKey)
+ {
+ // get site
+ if (!this.ModSites.TryGetValue(updateKey.Site, out IModSiteClient client))
+ return new GenericModPage(updateKey.Site, updateKey.ID).SetError(RemoteModStatus.DoesNotExist, $"There's no mod site with key '{updateKey.Site}'. Expected one of [{string.Join(", ", this.ModSites.Keys)}].");
+
+ // fetch mod
+ IModPage mod;
+ try
+ {
+ mod = await client.GetModData(updateKey.ID);
+ }
+ catch (Exception ex)
+ {
+ mod = new GenericModPage(updateKey.Site, updateKey.ID).SetError(RemoteModStatus.TemporaryError, ex.ToString());
+ }
+
+ // handle errors
+ return mod ?? new GenericModPage(updateKey.Site, updateKey.ID).SetError(RemoteModStatus.DoesNotExist, $"Found no {updateKey.Site} mod with ID '{updateKey.ID}'.");
+ }
+
+ /// <summary>Parse version info for the given mod page info.</summary>
+ /// <param name="page">The mod page info.</param>
+ /// <param name="subkey">The optional update subkey to match in available files. (If no file names or descriptions contain the subkey, it'll be ignored.)</param>
+ /// <param name="mapRemoteVersions">Maps remote versions to a semantic version for update checks.</param>
+ /// <param name="allowNonStandardVersions">Whether to allow non-standard versions.</param>
+ public ModInfoModel GetPageVersions(IModPage page, string subkey, bool allowNonStandardVersions, IDictionary<string, string> mapRemoteVersions)
+ {
+ // get base model
+ ModInfoModel model = new ModInfoModel()
+ .SetBasicInfo(page.Name, page.Url)
+ .SetError(page.Status, page.Error);
+ if (page.Status != RemoteModStatus.Ok)
+ return model;
+
+ // fetch versions
+ bool hasVersions = this.TryGetLatestVersions(page, subkey, allowNonStandardVersions, mapRemoteVersions, out ISemanticVersion mainVersion, out ISemanticVersion previewVersion);
+ if (!hasVersions && subkey != null)
+ hasVersions = this.TryGetLatestVersions(page, null, allowNonStandardVersions, mapRemoteVersions, out mainVersion, out previewVersion);
+ if (!hasVersions)
+ return model.SetError(RemoteModStatus.InvalidData, $"The {page.Site} mod with ID '{page.Id}' has no valid versions.");
+
+ // return info
+ return model.SetVersions(mainVersion, previewVersion);
+ }
+
+ /// <summary>Get a semantic local version for update checks.</summary>
+ /// <param name="version">The version to parse.</param>
+ /// <param name="map">A map of version replacements.</param>
+ /// <param name="allowNonStandard">Whether to allow non-standard versions.</param>
+ public ISemanticVersion GetMappedVersion(string version, IDictionary<string, string> map, bool allowNonStandard)
+ {
+ // try mapped version
+ string rawNewVersion = this.GetRawMappedVersion(version, map, allowNonStandard);
+ if (SemanticVersion.TryParse(rawNewVersion, allowNonStandard, out ISemanticVersion parsedNew))
+ return parsedNew;
+
+ // return original version
+ return SemanticVersion.TryParse(version, allowNonStandard, out ISemanticVersion parsedOld)
+ ? parsedOld
+ : null;
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// <summary>Get the mod version numbers for the given mod.</summary>
+ /// <param name="mod">The mod to check.</param>
+ /// <param name="subkey">The optional update subkey to match in available files. (If no file names or descriptions contain the subkey, it'll be ignored.)</param>
+ /// <param name="allowNonStandardVersions">Whether to allow non-standard versions.</param>
+ /// <param name="mapRemoteVersions">Maps remote versions to a semantic version for update checks.</param>
+ /// <param name="main">The main mod version.</param>
+ /// <param name="preview">The latest prerelease version, if newer than <paramref name="main"/>.</param>
+ private bool TryGetLatestVersions(IModPage mod, string subkey, bool allowNonStandardVersions, IDictionary<string, string> mapRemoteVersions, out ISemanticVersion main, out ISemanticVersion preview)
+ {
+ main = null;
+ preview = null;
+
+ ISemanticVersion ParseVersion(string raw)
+ {
+ raw = this.NormalizeVersion(raw);
+ return this.GetMappedVersion(raw, mapRemoteVersions, allowNonStandardVersions);
+ }
+
+ if (mod != null)
+ {
+ // get mod version
+ if (subkey == null)
+ main = ParseVersion(mod.Version);
+
+ // get file versions
+ foreach (IModDownload download in mod.Downloads)
+ {
+ // check for subkey if specified
+ if (subkey != null && download.Name?.Contains(subkey, StringComparison.OrdinalIgnoreCase) != true && download.Description?.Contains(subkey, StringComparison.OrdinalIgnoreCase) != true)
+ continue;
+
+ // parse version
+ ISemanticVersion cur = ParseVersion(download.Version);
+ if (cur == null)
+ continue;
+
+ // track highest versions
+ if (main == null || cur.IsNewerThan(main))
+ main = cur;
+ if (cur.IsPrerelease() && (preview == null || cur.IsNewerThan(preview)))
+ preview = cur;
+ }
+
+ if (preview != null && !preview.IsNewerThan(main))
+ preview = null;
+ }
+
+ return main != null;
+ }
+
+ /// <summary>Get a semantic local version for update checks.</summary>
+ /// <param name="version">The version to map.</param>
+ /// <param name="map">A map of version replacements.</param>
+ /// <param name="allowNonStandard">Whether to allow non-standard versions.</param>
+ private string GetRawMappedVersion(string version, IDictionary<string, string> map, bool allowNonStandard)
+ {
+ if (version == null || map == null || !map.Any())
+ return version;
+
+ // match exact raw version
+ if (map.ContainsKey(version))
+ return map[version];
+
+ // match parsed version
+ if (SemanticVersion.TryParse(version, allowNonStandard, out ISemanticVersion parsed))
+ {
+ if (map.ContainsKey(parsed.ToString()))
+ return map[parsed.ToString()];
+
+ foreach ((string fromRaw, string toRaw) in map)
+ {
+ if (SemanticVersion.TryParse(fromRaw, allowNonStandard, out ISemanticVersion target) && parsed.Equals(target) && SemanticVersion.TryParse(toRaw, allowNonStandard, out ISemanticVersion newVersion))
+ return newVersion.ToString();
+ }
+ }
+
+ return version;
+ }
+
+ /// <summary>Normalize a version string.</summary>
+ /// <param name="version">The version to normalize.</param>
+ private string NormalizeVersion(string version)
+ {
+ if (string.IsNullOrWhiteSpace(version))
+ return null;
+
+ version = version.Trim();
+ if (Regex.IsMatch(version, @"^v\d", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase)) // common version prefix
+ version = version.Substring(1);
+
+ return version;
+ }
+ }
+}