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 { /// Handles fetching data from mod sites. internal class ModSiteManager { /********* ** Fields *********/ /// The mod sites which provide mod metadata. private readonly IDictionary ModSites; /********* ** Public methods *********/ /// Construct an instance. /// The mod sites which provide mod metadata. public ModSiteManager(IModSiteClient[] modSites) { this.ModSites = modSites.ToDictionary(p => p.SiteKey); } /// Get the mod info for an update key. /// The namespaced update key. public async Task 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}'."); } /// Parse version info for the given mod page info. /// The mod page info. /// The optional update subkey to match in available files. (If no file names or descriptions contain the subkey, it'll be ignored.) /// Maps remote versions to a semantic version for update checks. /// Whether to allow non-standard versions. public ModInfoModel GetPageVersions(IModPage page, string subkey, bool allowNonStandardVersions, IDictionary 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); } /// Get a semantic local version for update checks. /// The version to parse. /// A map of version replacements. /// Whether to allow non-standard versions. public ISemanticVersion GetMappedVersion(string version, IDictionary 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 *********/ /// Get the mod version numbers for the given mod. /// The mod to check. /// The optional update subkey to match in available files. (If no file names or descriptions contain the subkey, it'll be ignored.) /// Whether to allow non-standard versions. /// Maps remote versions to a semantic version for update checks. /// The main mod version. /// The latest prerelease version, if newer than . private bool TryGetLatestVersions(IModPage mod, string subkey, bool allowNonStandardVersions, IDictionary 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; } /// Get a semantic local version for update checks. /// The version to map. /// A map of version replacements. /// Whether to allow non-standard versions. private string GetRawMappedVersion(string version, IDictionary 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; } /// Normalize a version string. /// The version to normalize. 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; } } }