using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; using StardewModdingAPI.Toolkit; using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; 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.) /// The changes to apply to remote versions for update checks. /// Whether to allow non-standard versions. public ModInfoModel GetPageVersions(IModPage page, string subkey, bool allowNonStandardVersions, ChangeDescriptor 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. /// Changes to apply to the raw version, if any. /// Whether to allow non-standard versions. public ISemanticVersion GetMappedVersion(string version, ChangeDescriptor 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. /// The changes to apply to remote versions for update checks. /// The main mod version. /// The latest prerelease version, if newer than . private bool TryGetLatestVersions(IModPage mod, string subkey, bool allowNonStandardVersions, ChangeDescriptor mapRemoteVersions, out ISemanticVersion main, out ISemanticVersion preview) { main = null; preview = null; // parse all versions from the mod page IEnumerable<(string name, string description, ISemanticVersion version)> GetAllVersions() { if (mod != null) { ISemanticVersion ParseAndMapVersion(string raw) { raw = this.NormalizeVersion(raw); return this.GetMappedVersion(raw, mapRemoteVersions, allowNonStandardVersions); } // get mod version ISemanticVersion modVersion = ParseAndMapVersion(mod.Version); if (modVersion != null) yield return (name: null, description: null, version: ParseAndMapVersion(mod.Version)); // get file versions foreach (IModDownload download in mod.Downloads) { ISemanticVersion cur = ParseAndMapVersion(download.Version); if (cur != null) yield return (download.Name, download.Description, cur); } } } var versions = GetAllVersions() .OrderByDescending(p => p.version, SemanticVersionComparer.Instance) .ToArray(); // get main + preview versions void TryGetVersions(out ISemanticVersion mainVersion, out ISemanticVersion previewVersion, Func<(string name, string description, ISemanticVersion version), bool> filter = null) { mainVersion = null; previewVersion = null; // get latest main + preview version foreach (var entry in versions) { if (filter?.Invoke(entry) == false) continue; if (entry.version.IsPrerelease()) previewVersion ??= entry.version; else mainVersion ??= entry.version; if (mainVersion != null) break; // any other values will be older } // normalize values if (previewVersion is not null) { mainVersion ??= previewVersion; // if every version is prerelease, latest one is the main version if (!previewVersion.IsNewerThan(mainVersion)) previewVersion = null; } } if (subkey is not null) TryGetVersions(out main, out preview, entry => entry.name?.Contains(subkey, StringComparison.OrdinalIgnoreCase) == true || entry.description?.Contains(subkey, StringComparison.OrdinalIgnoreCase) == true); if (main is null) TryGetVersions(out main, out preview); return main != null; } /// Get a semantic local version for update checks. /// The version to map. /// Changes to apply to the raw version, if any. /// Whether to allow non-standard versions. private string GetRawMappedVersion(string version, ChangeDescriptor map, bool allowNonStandard) { if (version == null || map?.HasChanges != true) return version; var mapped = new List { version }; map.Apply(mapped); return mapped.FirstOrDefault(); } /// 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; } } }