using System; using System.Diagnostics.CodeAnalysis; using System.Net; using System.Net.Http.Headers; using System.Threading.Tasks; using Pathoschild.Http.Client; using StardewModdingAPI.Toolkit; using StardewModdingAPI.Toolkit.Framework.UpdateData; using StardewModdingAPI.Web.Framework.Clients.UpdateManifest.ResponseModels; namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest { /// An API client for fetching update metadata from an arbitrary JSON URL. internal class UpdateManifestClient : IUpdateManifestClient { /********* ** Fields *********/ /// The underlying HTTP client. private readonly IClient Client; /********* ** Accessors *********/ /// The unique key for the mod site. public ModSiteKey SiteKey => ModSiteKey.UpdateManifest; /********* ** Public methods *********/ /// Construct an instance. /// The user agent for the API client. public UpdateManifestClient(string userAgent) { this.Client = new FluentClient() .SetUserAgent(userAgent); this.Client.Formatters.JsonFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/plain")); } /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. public void Dispose() { this.Client.Dispose(); } /// [SuppressMessage("ReSharper", "ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract", Justification = "This is the method which ensures the annotations are correct.")] public async Task GetModData(string id) { // get raw update manifest UpdateManifestModel? manifest; try { manifest = await this.Client.GetAsync(id).As(); if (manifest is null) return this.GetFormatError(id, "manifest can't be empty"); } catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound) { return new GenericModPage(this.SiteKey, id).SetError(RemoteModStatus.DoesNotExist, $"No update manifest found at {id}"); } catch (Exception ex) { return this.GetFormatError(id, ex.Message); } // validate if (!SemanticVersion.TryParse(manifest.Format, out _)) return this.GetFormatError(id, $"invalid format version '{manifest.Format}'"); foreach (UpdateManifestModModel mod in manifest.Mods.Values) { if (mod is null) return this.GetFormatError(id, "a mod record can't be null"); if (string.IsNullOrWhiteSpace(mod.ModPageUrl)) return this.GetFormatError(id, $"all mods must have a {nameof(mod.ModPageUrl)} value"); foreach (UpdateManifestVersionModel? version in mod.Versions) { if (version is null) return this.GetFormatError(id, "a version record can't be null"); if (string.IsNullOrWhiteSpace(version.Version)) return this.GetFormatError(id, $"all version records must have a {nameof(version.Version)} field"); if (!SemanticVersion.TryParse(version.Version, out _)) return this.GetFormatError(id, $"invalid mod version '{version.Version}'"); } } // build model return new UpdateManifestModPage(id, manifest); } /********* ** Private methods *********/ /// Get a mod page instance with an error indicating the update manifest is invalid. /// The full URL to the update manifest. /// A human-readable reason phrase indicating why it's invalid. private IModPage GetFormatError(string url, string reason) { return new GenericModPage(this.SiteKey, url).SetError(RemoteModStatus.InvalidData, $"The update manifest at {url} is invalid ({reason})"); } } }