Merge branch 'develop' into stable
This commit is contained in:
commit
e64ecc89f9
|
@ -30,8 +30,5 @@ _ReSharper*/
|
|||
# sensitive files
|
||||
appsettings.Development.json
|
||||
|
||||
# AWS generated files
|
||||
src/SMAPI.Web.LegacyRedirects/aws-beanstalk-tools-defaults.json
|
||||
|
||||
# Azure generated files
|
||||
src/SMAPI.Web/Properties/PublishProfiles/*.pubxml
|
||||
|
|
Binary file not shown.
|
@ -4,9 +4,10 @@
|
|||
|
||||
<!--set properties -->
|
||||
<PropertyGroup>
|
||||
<Version>3.5.0</Version>
|
||||
<Version>3.6.0</Version>
|
||||
<Product>SMAPI</Product>
|
||||
|
||||
<LangVersion>latest</LangVersion>
|
||||
<AssemblySearchPaths>$(AssemblySearchPaths);{GAC}</AssemblySearchPaths>
|
||||
<DefineConstants Condition="$(OS) == 'Windows_NT'">$(DefineConstants);SMAPI_FOR_WINDOWS</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
|
|
@ -1,6 +1,54 @@
|
|||
← [README](README.md)
|
||||
|
||||
# Release notes
|
||||
<!--
|
||||
## Future release
|
||||
* For modders:
|
||||
* Migrated to Harmony 2.0 (see [_migrate to Harmony 2.0_](https://stardewvalleywiki.com/Modding:Migrate_to_Harmony_2.0) for more info).
|
||||
-->
|
||||
|
||||
## 3.6
|
||||
Released 20 June 2020 for Stardew Valley 1.4.1 or later.
|
||||
|
||||
* For players:
|
||||
* Added crossplatform compatibility for mods which use the `[HarmonyPatch(type)]` attribute.
|
||||
* Added experimental option to reduce startup time when loading mod DLLs (thanks to ZaneYork!). Enable `RewriteInParallel` in the `smapi-internal/config.json` to try it.
|
||||
* Reduced processing time when a mod loads many unpacked images (thanks to Entoarox!).
|
||||
* Mod load warnings are now listed alphabetically.
|
||||
* MacOS files starting with `._` are now ignored and can no longer cause skipped mods.
|
||||
* Simplified paranoid warning logs and reduced their log level.
|
||||
* Fixed black maps on Android for mods which use `.tmx` files.
|
||||
* Fixed `BadImageFormatException` error detection.
|
||||
* Fixed `reload_i18n` command not reloading content pack translations.
|
||||
|
||||
* For the web UI:
|
||||
* Added GitHub licenses to mod compatibility list.
|
||||
* Improved JSON validator:
|
||||
* added SMAPI `i18n` schema;
|
||||
* editing an uploaded file now remembers the selected schema;
|
||||
* changed default schema to plain JSON.
|
||||
* Updated ModDrop URLs.
|
||||
* Internal changes to improve performance and reliability.
|
||||
|
||||
* For modders:
|
||||
* Added [event priorities](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Events#Custom_priority) (thanks to spacechase0!).
|
||||
* Added [update subkeys](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Update_checks#Update_subkeys).
|
||||
* Added [a custom build of Harmony](https://github.com/Pathoschild/Harmony#readme) to provide more useful stack traces in error logs.
|
||||
* Added `harmony_summary` console command to list or search current Harmony patches.
|
||||
* Added `Multiplayer.PeerConnected` event.
|
||||
* Added support for overriding update keys from the wiki compatibility list.
|
||||
* Improved mod rewriting for compatibility to support more cases (e.g. custom attributes and generic types).
|
||||
* Fixed `helper.Reflection` blocking access to game methods/properties intercepted by SMAPI.
|
||||
* Fixed asset propagation for Gil's portraits.
|
||||
* Fixed `.pdb` files ignored for error stack traces when mods are rewritten by SMAPI.
|
||||
* Fixed `ModMessageReceived` event handlers not tracked for performance monitoring.
|
||||
|
||||
* For SMAPI developers:
|
||||
* Eliminated MongoDB storage in the web services, which complicated the code unnecessarily. The app still uses an abstract interface for storage, so we can wrap a distributed cache in the future if needed.
|
||||
* Overhauled update checks to simplify mod site integrations, centralize common logic, and enable upcoming features.
|
||||
* Merged the separate legacy redirects app on AWS into the main app on Azure.
|
||||
* Changed SMAPI's Harmony ID from `io.smapi` to `SMAPI` for readability in Harmony summaries.
|
||||
|
||||
## 3.5
|
||||
Released 27 April 2020 for Stardew Valley 1.4.1 or later.
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ This document is about SMAPI itself; see also [mod build package](mod-package.md
|
|||
* [Compiling from source](#compiling-from-source)
|
||||
* [Debugging a local build](#debugging-a-local-build)
|
||||
* [Preparing a release](#preparing-a-release)
|
||||
* [Using a custom Harmony build](#using-a-custom-harmony-build)
|
||||
* [Release notes](#release-notes)
|
||||
|
||||
## Customisation
|
||||
|
@ -57,24 +58,22 @@ SMAPI uses a small number of conditional compilation constants, which you can se
|
|||
flag | purpose
|
||||
---- | -------
|
||||
`SMAPI_FOR_WINDOWS` | Whether SMAPI is being compiled on Windows for players on Windows. Set automatically in `crossplatform.targets`.
|
||||
`HARMONY_2` | Whether to enable experimental Harmony 2.0 support and rewrite existing Harmony 1._x_ mods for compatibility. Note that you need to replace `build/0Harmony.dll` with a Harmony 2.0 build (or switch to a package reference) to use this flag.
|
||||
|
||||
## For SMAPI developers
|
||||
### Compiling from source
|
||||
Using an official SMAPI release is recommended for most users.
|
||||
Using an official SMAPI release is recommended for most users, but you can compile from source
|
||||
directly if needed. There are no special steps (just open the project and compile), but SMAPI often
|
||||
uses the latest C# syntax. You may need the latest version of your IDE to compile it.
|
||||
|
||||
SMAPI often uses the latest C# syntax. You may need the latest version of
|
||||
[Visual Studio](https://www.visualstudio.com/vs/community/) on Windows,
|
||||
[MonoDevelop](https://www.monodevelop.com/) on Linux,
|
||||
[Visual Studio for Mac](https://www.visualstudio.com/vs/visual-studio-mac/), or an equivalent IDE
|
||||
to compile it. It uses build configuration derived from the
|
||||
[crossplatform mod config](https://smapi.io/package/readme) to detect your current OS automatically
|
||||
and load the correct references. Compile output will be placed in a `bin` folder at the root of the
|
||||
git repository.
|
||||
SMAPI uses build configuration derived from the [crossplatform mod config](https://smapi.io/package/readme)
|
||||
to detect your current OS automatically and load the correct references. Compile output will be
|
||||
placed in a `bin` folder at the root of the Git repository.
|
||||
|
||||
### Debugging a local build
|
||||
Rebuilding the solution in debug mode will copy the SMAPI files into your game folder. Starting
|
||||
the `SMAPI` project with debugging from Visual Studio (on Mac or Windows) will launch SMAPI with
|
||||
the debugger attached, so you can intercept errors and step through the code being executed. This
|
||||
the debugger attached, so you can intercept errors and step through the code being executed. That
|
||||
doesn't work in MonoDevelop on Linux, unfortunately.
|
||||
|
||||
### Preparing a release
|
||||
|
@ -82,14 +81,14 @@ To prepare a crossplatform SMAPI release, you'll need to compile it on two platf
|
|||
[crossplatforming info](https://stardewvalleywiki.com/Modding:Modder_Guide/Test_and_Troubleshoot#Testing_on_all_platforms)
|
||||
on the wiki for the first-time setup.
|
||||
|
||||
1. Update the version number in `.root/build/common.targets` and `Constants::Version`. Make sure
|
||||
you use a [semantic version](https://semver.org). Recommended format:
|
||||
1. Update the version numbers in `build/common.targets`, `Constants`, and the `manifest.json` for
|
||||
bundled mods. Make sure you use a [semantic version](https://semver.org). Recommended format:
|
||||
|
||||
build type | format | example
|
||||
:--------- | :----------------------- | :------
|
||||
dev build | `<version>-alpha.<date>` | `3.0-alpha.20171230`
|
||||
prerelease | `<version>-beta.<count>` | `3.0-beta.2`
|
||||
release | `<version>` | `3.0`
|
||||
dev build | `<version>-alpha.<date>` | `3.0.0-alpha.20171230`
|
||||
prerelease | `<version>-beta.<count>` | `3.0.0-beta.2`
|
||||
release | `<version>` | `3.0.0`
|
||||
|
||||
2. In Windows:
|
||||
1. Rebuild the solution in Release mode.
|
||||
|
@ -103,5 +102,10 @@ on the wiki for the first-time setup.
|
|||
3. Rename the folders to `SMAPI <version> installer` and `SMAPI <version> installer for developers`.
|
||||
4. Zip the two folders.
|
||||
|
||||
### Custom Harmony build
|
||||
SMAPI uses [a custom build of Harmony](https://github.com/Pathoschild/Harmony#readme), which is
|
||||
included in the `build` folder. To use a different build, just replace `0Harmony.dll` in that
|
||||
folder.
|
||||
|
||||
## Release notes
|
||||
See [release notes](../release-notes.md).
|
||||
|
|
|
@ -110,8 +110,9 @@ Available schemas:
|
|||
|
||||
format | schema URL
|
||||
------ | ----------
|
||||
[SMAPI `manifest.json`](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Manifest) | https://smapi.io/schemas/manifest.json
|
||||
[Content Patcher `content.json`](https://github.com/Pathoschild/StardewMods/tree/develop/ContentPatcher#readme) | https://smapi.io/schemas/content-patcher.json
|
||||
[SMAPI: `manifest.json`](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Manifest) | https://smapi.io/schemas/manifest.json
|
||||
[SMAPI: translations (`i18n` folder)](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Translation) | https://smapi.io/schemas/i18n.json
|
||||
[Content Patcher: `content.json`](https://github.com/Pathoschild/StardewMods/tree/develop/ContentPatcher#readme) | https://smapi.io/schemas/content-patcher.json
|
||||
|
||||
## Web API
|
||||
### Overview
|
||||
|
@ -340,9 +341,19 @@ short url | → | target page
|
|||
A local environment lets you run a complete copy of the web project (including cache database) on
|
||||
your machine, with no external dependencies aside from the actual mod sites.
|
||||
|
||||
1. Enter the Nexus credentials in `appsettings.Development.json` . You can leave the other
|
||||
credentials empty to default to fetching data anonymously, and storing data in-memory and
|
||||
on disk.
|
||||
1. Edit `appsettings.Development.json` and set these options:
|
||||
|
||||
property name | description
|
||||
------------- | -----------
|
||||
`NexusApiKey` | [Your Nexus API key](https://www.nexusmods.com/users/myaccount?tab=api#personal_key).
|
||||
|
||||
Optional settings:
|
||||
|
||||
property name | description
|
||||
--------------------------- | -----------
|
||||
`AzureBlobConnectionString` | The connection string for the Azure Blob storage account. Defaults to using the system's temporary file folder if not specified.
|
||||
`GitHubUsername`<br />`GitHubPassword` | The GitHub credentials with which to query GitHub release info. Defaults to anonymous requests if not specified.
|
||||
|
||||
2. Launch `SMAPI.Web` from Visual Studio to run a local version of the site.
|
||||
|
||||
### Production environment
|
||||
|
@ -355,19 +366,15 @@ accordingly.
|
|||
|
||||
Initial setup:
|
||||
|
||||
1. Launch an empty MongoDB server (e.g. using [MongoDB Atlas](https://www.mongodb.com/cloud/atlas))
|
||||
for mod data.
|
||||
2. Create an Azure Blob storage account for uploaded files.
|
||||
3. Create an Azure App Services environment running the latest .NET Core on Linux or Windows.
|
||||
4. Add these application settings in the new App Services environment:
|
||||
1. Create an Azure Blob storage account for uploaded files.
|
||||
2. Create an Azure App Services environment running the latest .NET Core on Linux or Windows.
|
||||
3. Add these application settings in the new App Services environment:
|
||||
|
||||
property name | description
|
||||
------------------------------- | -----------------
|
||||
`ApiClients.AzureBlobConnectionString` | The connection string for the Azure Blob storage account created in step 2.
|
||||
`ApiClients.GitHubUsername`<br />`ApiClients.GitHubPassword` | The login credentials for the GitHub account with which to fetch release info. If these are omitted, GitHub will impose much stricter rate limits.
|
||||
`ApiClients:NexusApiKey` | The [Nexus API authentication key](https://github.com/Pathoschild/FluentNexus#init-a-client).
|
||||
`MongoDB:ConnectionString` | The connection string for the MongoDB instance.
|
||||
`MongoDB:Database` | The MongoDB database name (e.g. `smapi` in production or `smapi-edge` in testing environments).
|
||||
|
||||
Optional settings:
|
||||
|
||||
|
@ -378,6 +385,4 @@ Initial setup:
|
|||
`Site:BetaBlurb` | If `Site:BetaEnabled` is true and there's a beta version of SMAPI in its GitHub releases, this is shown on the beta download button as explanatory subtext.
|
||||
`Site:SupporterList` | A list of Patreon supports to credit on the download page.
|
||||
|
||||
To deploy updates:
|
||||
1. [Deploy the web project from Visual Studio](https://docs.microsoft.com/en-us/visualstudio/deployment/quickstart-deploy-to-azure).
|
||||
2. If the MongoDB schema changed, delete the MongoDB database. (It'll be recreated automatically.)
|
||||
To deploy updates, just [redeploy the web project from Visual Studio](https://docs.microsoft.com/en-us/visualstudio/deployment/quickstart-deploy-to-azure).
|
||||
|
|
|
@ -3,7 +3,6 @@ using System.Collections.Generic;
|
|||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Microsoft.Win32;
|
||||
using StardewModdingApi.Installer.Enums;
|
||||
using StardewModdingAPI.Installer.Framework;
|
||||
|
@ -624,7 +623,7 @@ namespace StardewModdingApi.Installer
|
|||
{
|
||||
try
|
||||
{
|
||||
this.ForceDelete(Directory.Exists(path) ? new DirectoryInfo(path) : (FileSystemInfo)new FileInfo(path));
|
||||
FileUtilities.ForceDelete(Directory.Exists(path) ? new DirectoryInfo(path) : (FileSystemInfo)new FileInfo(path));
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
@ -665,41 +664,6 @@ namespace StardewModdingApi.Installer
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>Delete a file or folder regardless of file permissions, and block until deletion completes.</summary>
|
||||
/// <param name="entry">The file or folder to reset.</param>
|
||||
/// <remarks>This method is mirrored from <c>FileUtilities.ForceDelete</c> in the toolkit.</remarks>
|
||||
private void ForceDelete(FileSystemInfo entry)
|
||||
{
|
||||
// ignore if already deleted
|
||||
entry.Refresh();
|
||||
if (!entry.Exists)
|
||||
return;
|
||||
|
||||
// delete children
|
||||
if (entry is DirectoryInfo folder)
|
||||
{
|
||||
foreach (FileSystemInfo child in folder.GetFileSystemInfos())
|
||||
this.ForceDelete(child);
|
||||
}
|
||||
|
||||
// reset permissions & delete
|
||||
entry.Attributes = FileAttributes.Normal;
|
||||
entry.Delete();
|
||||
|
||||
// wait for deletion to finish
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
entry.Refresh();
|
||||
if (entry.Exists)
|
||||
Thread.Sleep(500);
|
||||
}
|
||||
|
||||
// throw exception if deletion didn't happen before timeout
|
||||
entry.Refresh();
|
||||
if (entry.Exists)
|
||||
throw new IOException($"Timed out trying to delete {entry.FullName}");
|
||||
}
|
||||
|
||||
/// <summary>Interactively ask the user to choose a value.</summary>
|
||||
/// <param name="print">A callback which prints a message to the console.</param>
|
||||
/// <param name="message">The message to print.</param>
|
||||
|
@ -707,7 +671,7 @@ namespace StardewModdingApi.Installer
|
|||
/// <param name="indent">The indentation to prefix to output.</param>
|
||||
private string InteractivelyChoose(string message, string[] options, string indent = "", Action<string> print = null)
|
||||
{
|
||||
print = print ?? this.PrintInfo;
|
||||
print ??= this.PrintInfo;
|
||||
|
||||
while (true)
|
||||
{
|
||||
|
|
|
@ -1,11 +1,8 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<AssemblyName>SMAPI.Installer</AssemblyName>
|
||||
<RootNamespace>StardewModdingAPI.Installer</RootNamespace>
|
||||
<Description>The SMAPI installer for players.</Description>
|
||||
<TargetFramework>net45</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<OutputType>Exe</OutputType>
|
||||
<PlatformTarget>x86</PlatformTarget>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
|
@ -16,13 +13,10 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="assets\*">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="assets\*" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
<Import Project="..\SMAPI.Internal\SMAPI.Internal.projitems" Label="Shared" />
|
||||
<Import Project="..\..\build\common.targets" />
|
||||
<Import Project="..\..\build\prepare-install-package.targets" />
|
||||
|
||||
</Project>
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="2.10.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
|
||||
<PackageReference Include="NUnit" Version="3.12.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="3.16.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
|
|
|
@ -1,11 +1,8 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<AssemblyName>SMAPI.ModBuildConfig.Analyzer</AssemblyName>
|
||||
<RootNamespace>StardewModdingAPI.ModBuildConfig.Analyzer</RootNamespace>
|
||||
<Version>3.0.0</Version>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<IncludeBuildOutput>false</IncludeBuildOutput>
|
||||
<OutputPath>bin</OutputPath>
|
||||
<LangVersion>latest</LangVersion>
|
||||
|
@ -19,5 +16,4 @@
|
|||
<ItemGroup>
|
||||
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
@ -153,23 +153,22 @@ namespace StardewModdingAPI.ModBuildConfig
|
|||
|
||||
// create zip file
|
||||
Directory.CreateDirectory(outputFolderPath);
|
||||
using (Stream zipStream = new FileStream(zipPath, FileMode.Create, FileAccess.Write))
|
||||
using (ZipArchive archive = new ZipArchive(zipStream, ZipArchiveMode.Create))
|
||||
using Stream zipStream = new FileStream(zipPath, FileMode.Create, FileAccess.Write);
|
||||
using ZipArchive archive = new ZipArchive(zipStream, ZipArchiveMode.Create);
|
||||
|
||||
foreach (var fileEntry in files)
|
||||
{
|
||||
foreach (var fileEntry in files)
|
||||
{
|
||||
string relativePath = fileEntry.Key;
|
||||
FileInfo file = fileEntry.Value;
|
||||
string relativePath = fileEntry.Key;
|
||||
FileInfo file = fileEntry.Value;
|
||||
|
||||
// get file info
|
||||
string filePath = file.FullName;
|
||||
string entryName = folderName + '/' + relativePath.Replace(Path.DirectorySeparatorChar, '/');
|
||||
// get file info
|
||||
string filePath = file.FullName;
|
||||
string entryName = folderName + '/' + relativePath.Replace(Path.DirectorySeparatorChar, '/');
|
||||
|
||||
// add to zip
|
||||
using (Stream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read))
|
||||
using (Stream fileStreamInZip = archive.CreateEntry(entryName).Open())
|
||||
fileStream.CopyTo(fileStreamInZip);
|
||||
}
|
||||
// add to zip
|
||||
using Stream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
|
||||
using Stream fileStreamInZip = archive.CreateEntry(entryName).Open();
|
||||
fileStream.CopyTo(fileStreamInZip);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,24 +1,12 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<AssemblyName>SMAPI.ModBuildConfig</AssemblyName>
|
||||
<RootNamespace>StardewModdingAPI.ModBuildConfig</RootNamespace>
|
||||
<Version>3.0.0</Version>
|
||||
<Version>3.1.0</Version>
|
||||
<TargetFramework>net45</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<PlatformTarget>x86</PlatformTarget>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SMAPI.Toolkit\SMAPI.Toolkit.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\..\build\find-game-folder.targets" Link="build\find-game-folder.targets" />
|
||||
<None Include="..\..\docs\technical\mod-package.md" Link="mod-build-config.md" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.Build" />
|
||||
<Reference Include="Microsoft.Build.Framework" />
|
||||
|
@ -28,19 +16,16 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\..\docs\technical\mod-package.md">
|
||||
<Link>mod-package.md</Link>
|
||||
</None>
|
||||
<ProjectReference Include="..\SMAPI.Toolkit\SMAPI.Toolkit.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="assets\nuget-icon.png">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Include="..\..\build\find-game-folder.targets" Link="build\find-game-folder.targets" />
|
||||
<None Include="..\..\docs\technical\mod-package.md" Link="mod-package.md" />
|
||||
<None Update="assets\nuget-icon.png" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
<Import Project="..\SMAPI.Internal\SMAPI.Internal.projitems" Label="Shared" />
|
||||
<Import Project="..\..\build\common.targets" />
|
||||
<Import Project="..\..\build\prepare-nuget-package.targets" />
|
||||
|
||||
</Project>
|
||||
|
|
|
@ -38,58 +38,26 @@
|
|||
**********************************************-->
|
||||
<!-- common -->
|
||||
<ItemGroup>
|
||||
<Reference Include="$(GameExecutableName)">
|
||||
<HintPath>$(GamePath)\$(GameExecutableName).exe</HintPath>
|
||||
<Private>$(CopyModReferencesToBuildOutput)</Private>
|
||||
</Reference>
|
||||
<Reference Include="StardewValley.GameData">
|
||||
<HintPath>$(GamePath)\StardewValley.GameData.dll</HintPath>
|
||||
<Private>$(CopyModReferencesToBuildOutput)</Private>
|
||||
</Reference>
|
||||
<Reference Include="StardewModdingAPI">
|
||||
<HintPath>$(GamePath)\StardewModdingAPI.exe</HintPath>
|
||||
<Private>$(CopyModReferencesToBuildOutput)</Private>
|
||||
</Reference>
|
||||
<Reference Include="SMAPI.Toolkit.CoreInterfaces">
|
||||
<HintPath>$(GamePath)\smapi-internal\SMAPI.Toolkit.CoreInterfaces.dll</HintPath>
|
||||
<Private>$(CopyModReferencesToBuildOutput)</Private>
|
||||
</Reference>
|
||||
<Reference Include="xTile">
|
||||
<HintPath>$(GamePath)\xTile.dll</HintPath>
|
||||
<Private>$(CopyModReferencesToBuildOutput)</Private>
|
||||
</Reference>
|
||||
<Reference Include="0Harmony" Condition="'$(EnableHarmony)' == 'true'">
|
||||
<HintPath>$(GamePath)\smapi-internal\0Harmony.dll</HintPath>
|
||||
<Private>$(CopyModReferencesToBuildOutput)</Private>
|
||||
</Reference>
|
||||
<Reference Include="$(GameExecutableName)" HintPath="$(GamePath)\$(GameExecutableName).exe" Private="$(CopyModReferencesToBuildOutput)" />
|
||||
<Reference Include="StardewValley.GameData" HintPath="$(GamePath)\StardewValley.GameData.dll" Private="$(CopyModReferencesToBuildOutput)" />
|
||||
<Reference Include="StardewModdingAPI" HintPath="$(GamePath)\StardewModdingAPI.exe" Private="$(CopyModReferencesToBuildOutput)" />
|
||||
<Reference Include="SMAPI.Toolkit.CoreInterfaces" HintPath="$(GamePath)\smapi-internal\SMAPI.Toolkit.CoreInterfaces.dll" Private="$(CopyModReferencesToBuildOutput)" />
|
||||
<Reference Include="xTile" HintPath="$(GamePath)\xTile.dll" Private="$(CopyModReferencesToBuildOutput)" />
|
||||
<Reference Include="0Harmony" Condition="'$(EnableHarmony)' == 'true'" HintPath="$(GamePath)\smapi-internal\0Harmony.dll" Private="$(CopyModReferencesToBuildOutput)" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Windows -->
|
||||
<ItemGroup Condition="$(OS) == 'Windows_NT'">
|
||||
<Reference Include="Microsoft.Xna.Framework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86">
|
||||
<Private>$(CopyModReferencesToBuildOutput)</Private>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.Xna.Framework.Game, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86">
|
||||
<Private>$(CopyModReferencesToBuildOutput)</Private>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.Xna.Framework.Graphics, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86">
|
||||
<Private>$(CopyModReferencesToBuildOutput)</Private>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.Xna.Framework.Xact, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86">
|
||||
<Private>$(CopyModReferencesToBuildOutput)</Private>
|
||||
</Reference>
|
||||
<Reference Include="Netcode">
|
||||
<HintPath>$(GamePath)\Netcode.dll</HintPath>
|
||||
<Private>$(CopyModReferencesToBuildOutput)</Private>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.Xna.Framework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="$(CopyModReferencesToBuildOutput)" />
|
||||
<Reference Include="Microsoft.Xna.Framework.Game, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="$(CopyModReferencesToBuildOutput)" />
|
||||
<Reference Include="Microsoft.Xna.Framework.Graphics, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="$(CopyModReferencesToBuildOutput)" />
|
||||
<Reference Include="Microsoft.Xna.Framework.Xact, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="$(CopyModReferencesToBuildOutput)" />
|
||||
<Reference Include="Netcode" HintPath="$(GamePath)\Netcode.dll" Private="$(CopyModReferencesToBuildOutput)" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Linux/Mac -->
|
||||
<ItemGroup Condition="$(OS) != 'Windows_NT'">
|
||||
<Reference Include="MonoGame.Framework">
|
||||
<HintPath>$(GamePath)\MonoGame.Framework.dll</HintPath>
|
||||
<Private>$(CopyModReferencesToBuildOutput)</Private>
|
||||
</Reference>
|
||||
<Reference Include="MonoGame.Framework" HintPath="$(GamePath)\MonoGame.Framework.dll" Private="$(CopyModReferencesToBuildOutput)" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
|
|
|
@ -1,72 +1,45 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<AssemblyName>ConsoleCommands</AssemblyName>
|
||||
<RootNamespace>StardewModdingAPI.Mods.ConsoleCommands</RootNamespace>
|
||||
<TargetFramework>net45</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<PlatformTarget>x86</PlatformTarget>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SMAPI\SMAPI.csproj">
|
||||
<Private>False</Private>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\SMAPI\SMAPI.csproj" Private="False" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="$(GameExecutableName)">
|
||||
<HintPath>$(GamePath)\$(GameExecutableName).exe</HintPath>
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="StardewValley.GameData">
|
||||
<HintPath>$(GamePath)\StardewValley.GameData.dll</HintPath>
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="$(GameExecutableName)" HintPath="$(GamePath)\$(GameExecutableName).exe" Private="False" />
|
||||
<Reference Include="StardewValley.GameData" HintPath="$(GamePath)\StardewValley.GameData.dll" Private="False" />
|
||||
</ItemGroup>
|
||||
|
||||
<Choose>
|
||||
<!-- Windows -->
|
||||
<When Condition="$(OS) == 'Windows_NT'">
|
||||
<ItemGroup>
|
||||
<Reference Include="Netcode">
|
||||
<HintPath>$(GamePath)\Netcode.dll</HintPath>
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.Xna.Framework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86">
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.Xna.Framework.Game, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86">
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.Xna.Framework.Graphics, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86">
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.Xna.Framework.Xact, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86">
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="Netcode" HintPath="$(GamePath)\Netcode.dll" Private="False" />
|
||||
<Reference Include="Microsoft.Xna.Framework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="False" />
|
||||
<Reference Include="Microsoft.Xna.Framework.Game, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="False" />
|
||||
<Reference Include="Microsoft.Xna.Framework.Graphics, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="False" />
|
||||
<Reference Include="Microsoft.Xna.Framework.Xact, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="False" />
|
||||
</ItemGroup>
|
||||
</When>
|
||||
|
||||
<!-- Linux/Mac -->
|
||||
<Otherwise>
|
||||
<ItemGroup>
|
||||
<Reference Include="MonoGame.Framework">
|
||||
<HintPath>$(GamePath)\MonoGame.Framework.dll</HintPath>
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="MonoGame.Framework" HintPath="$(GamePath)\MonoGame.Framework.dll" Private="False" />
|
||||
</ItemGroup>
|
||||
</Otherwise>
|
||||
</Choose>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="manifest.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="manifest.json" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
<Import Project="..\SMAPI.Internal\SMAPI.Internal.projitems" Label="Shared" />
|
||||
<Import Project="..\..\build\common.targets" />
|
||||
|
||||
</Project>
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"Name": "Console Commands",
|
||||
"Author": "SMAPI",
|
||||
"Version": "3.5.0",
|
||||
"Version": "3.6.0",
|
||||
"Description": "Adds SMAPI console commands that let you manipulate the game.",
|
||||
"UniqueID": "SMAPI.ConsoleCommands",
|
||||
"EntryDll": "ConsoleCommands.dll",
|
||||
"MinimumApiVersion": "3.5.0"
|
||||
"MinimumApiVersion": "3.6.0"
|
||||
}
|
||||
|
|
|
@ -1,34 +1,24 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<AssemblyName>SaveBackup</AssemblyName>
|
||||
<RootNamespace>StardewModdingAPI.Mods.SaveBackup</RootNamespace>
|
||||
<TargetFramework>net45</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<PlatformTarget>x86</PlatformTarget>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SMAPI\SMAPI.csproj">
|
||||
<Private>False</Private>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\SMAPI\SMAPI.csproj" Private="False" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="$(GameExecutableName)">
|
||||
<HintPath>$(GamePath)\$(GameExecutableName).exe</HintPath>
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="$(GameExecutableName)" HintPath="$(GamePath)\$(GameExecutableName).exe" Private="False" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="manifest.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="manifest.json" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
<Import Project="..\SMAPI.Internal\SMAPI.Internal.projitems" Label="Shared" />
|
||||
<Import Project="..\..\build\common.targets" />
|
||||
|
||||
</Project>
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"Name": "Save Backup",
|
||||
"Author": "SMAPI",
|
||||
"Version": "3.5.0",
|
||||
"Version": "3.6.0",
|
||||
"Description": "Automatically backs up all your saves once per day into its folder.",
|
||||
"UniqueID": "SMAPI.SaveBackup",
|
||||
"EntryDll": "SaveBackup.dll",
|
||||
"MinimumApiVersion": "3.5.0"
|
||||
"MinimumApiVersion": "3.6.0"
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Moq" Version="4.13.1" />
|
||||
<PackageReference Include="Moq" Version="4.14.1" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||
<PackageReference Include="NUnit" Version="3.12.0" />
|
||||
</ItemGroup>
|
||||
|
|
|
@ -1,15 +1,11 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<AssemblyName>SMAPI.Toolkit.CoreInterfaces</AssemblyName>
|
||||
<RootNamespace>StardewModdingAPI</RootNamespace>
|
||||
<Description>Provides toolkit interfaces which are available to SMAPI mods.</Description>
|
||||
<TargetFrameworks>net4.5;netstandard2.0</TargetFrameworks>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\SMAPI.Toolkit.CoreInterfaces.xml</DocumentationFile>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<PlatformTarget Condition="'$(TargetFramework)' == 'net4.5'">x86</PlatformTarget>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="..\..\build\common.targets" />
|
||||
|
||||
</Project>
|
||||
|
|
|
@ -62,16 +62,15 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
|
|||
private TResult Post<TBody, TResult>(string url, TBody content)
|
||||
{
|
||||
// note: avoid HttpClient for Mac compatibility
|
||||
using (WebClient client = new WebClient())
|
||||
{
|
||||
Uri fullUrl = new Uri(this.BaseUrl, url);
|
||||
string data = JsonConvert.SerializeObject(content);
|
||||
using WebClient client = new WebClient();
|
||||
|
||||
client.Headers["Content-Type"] = "application/json";
|
||||
client.Headers["User-Agent"] = $"SMAPI/{this.Version}";
|
||||
string response = client.UploadString(fullUrl, data);
|
||||
return JsonConvert.DeserializeObject<TResult>(response, this.JsonSettings);
|
||||
}
|
||||
Uri fullUrl = new Uri(this.BaseUrl, url);
|
||||
string data = JsonConvert.SerializeObject(content);
|
||||
|
||||
client.Headers["Content-Type"] = "application/json";
|
||||
client.Headers["User-Agent"] = $"SMAPI/{this.Version}";
|
||||
string response = client.UploadString(fullUrl, data);
|
||||
return JsonConvert.DeserializeObject<TResult>(response, this.JsonSettings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -105,6 +105,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
|
|||
string pullRequestUrl = this.GetAttribute(node, "data-pr");
|
||||
IDictionary<string, string> mapLocalVersions = this.GetAttributeAsVersionMapping(node, "data-map-local-versions");
|
||||
IDictionary<string, string> mapRemoteVersions = this.GetAttributeAsVersionMapping(node, "data-map-remote-versions");
|
||||
string[] changeUpdateKeys = this.GetAttributeAsCsv(node, "data-change-update-keys");
|
||||
|
||||
// parse stable compatibility
|
||||
WikiCompatibilityInfo compatibility = new WikiCompatibilityInfo
|
||||
|
@ -153,6 +154,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
|
|||
Warnings = warnings,
|
||||
PullRequestUrl = pullRequestUrl,
|
||||
DevNote = devNote,
|
||||
ChangeUpdateKeys = changeUpdateKeys,
|
||||
MapLocalVersions = mapLocalVersions,
|
||||
MapRemoteVersions = mapRemoteVersions,
|
||||
Anchor = anchor
|
||||
|
|
|
@ -3,25 +3,28 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
|
|||
/// <summary>The compatibility status for a mod.</summary>
|
||||
public enum WikiCompatibilityStatus
|
||||
{
|
||||
/// <summary>The status is unknown.</summary>
|
||||
Unknown,
|
||||
|
||||
/// <summary>The mod is compatible.</summary>
|
||||
Ok = 0,
|
||||
Ok,
|
||||
|
||||
/// <summary>The mod is compatible if you use an optional official download.</summary>
|
||||
Optional = 1,
|
||||
Optional,
|
||||
|
||||
/// <summary>The mod is compatible if you use an unofficial update.</summary>
|
||||
Unofficial = 2,
|
||||
Unofficial,
|
||||
|
||||
/// <summary>The mod isn't compatible, but the player can fix it or there's a good alternative.</summary>
|
||||
Workaround = 3,
|
||||
Workaround,
|
||||
|
||||
/// <summary>The mod isn't compatible.</summary>
|
||||
Broken = 4,
|
||||
Broken,
|
||||
|
||||
/// <summary>The mod is no longer maintained by the author, and an unofficial update or continuation is unlikely.</summary>
|
||||
Abandoned = 5,
|
||||
Abandoned,
|
||||
|
||||
/// <summary>The mod is no longer needed and should be removed.</summary>
|
||||
Obsolete = 6
|
||||
Obsolete
|
||||
}
|
||||
}
|
||||
|
|
|
@ -63,6 +63,9 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
|
|||
/// <summary>Special notes intended for developers who maintain unofficial updates or submit pull requests. </summary>
|
||||
public string DevNote { get; set; }
|
||||
|
||||
/// <summary>Update keys to add (optionally prefixed by '+') or remove (prefixed by '-').</summary>
|
||||
public string[] ChangeUpdateKeys { get; set; }
|
||||
|
||||
/// <summary>Maps local versions to a semantic version for update checks.</summary>
|
||||
public IDictionary<string, string> MapLocalVersions { get; set; }
|
||||
|
||||
|
|
|
@ -124,8 +124,8 @@ namespace StardewModdingAPI.Toolkit.Framework.GameScanning
|
|||
XElement root;
|
||||
try
|
||||
{
|
||||
using (FileStream stream = file.OpenRead())
|
||||
root = XElement.Load(stream);
|
||||
using FileStream stream = file.OpenRead();
|
||||
root = XElement.Load(stream);
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
|
|
@ -22,7 +22,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning
|
|||
{
|
||||
// OS metadata files
|
||||
new Regex(@"^__folder_managed_by_vortex$", RegexOptions.Compiled | RegexOptions.IgnoreCase), // Vortex mod manager
|
||||
new Regex(@"^(?:__MACOSX|\._\.DS_Store|\.DS_Store|mcs)$", RegexOptions.Compiled | RegexOptions.IgnoreCase), // MacOS
|
||||
new Regex(@"(?:^\._|^\.DS_Store$|^__MACOSX$|^mcs$)", RegexOptions.Compiled | RegexOptions.IgnoreCase), // MacOS
|
||||
new Regex(@"^(?:desktop\.ini|Thumbs\.db)$", RegexOptions.Compiled | RegexOptions.IgnoreCase), // Windows
|
||||
new Regex(@"\.(?:url|lnk)$", RegexOptions.Compiled | RegexOptions.IgnoreCase), // Windows shortcut files
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
namespace StardewModdingAPI.Toolkit.Framework.UpdateData
|
||||
{
|
||||
/// <summary>A mod repository which SMAPI can check for updates.</summary>
|
||||
public enum ModRepositoryKey
|
||||
/// <summary>A mod site which SMAPI can check for updates.</summary>
|
||||
public enum ModSiteKey
|
||||
{
|
||||
/// <summary>An unknown or invalid mod repository.</summary>
|
||||
Unknown,
|
|
@ -11,12 +11,15 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData
|
|||
/// <summary>The raw update key text.</summary>
|
||||
public string RawText { get; }
|
||||
|
||||
/// <summary>The mod repository containing the mod.</summary>
|
||||
public ModRepositoryKey Repository { get; }
|
||||
/// <summary>The mod site containing the mod.</summary>
|
||||
public ModSiteKey Site { get; }
|
||||
|
||||
/// <summary>The mod ID within the repository.</summary>
|
||||
public string ID { get; }
|
||||
|
||||
/// <summary>If specified, a substring in download names/descriptions to match.</summary>
|
||||
public string Subkey { get; }
|
||||
|
||||
/// <summary>Whether the update key seems to be valid.</summary>
|
||||
public bool LooksValid { get; }
|
||||
|
||||
|
@ -26,53 +29,71 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData
|
|||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="rawText">The raw update key text.</param>
|
||||
/// <param name="repository">The mod repository containing the mod.</param>
|
||||
/// <param name="id">The mod ID within the repository.</param>
|
||||
public UpdateKey(string rawText, ModRepositoryKey repository, string id)
|
||||
/// <param name="site">The mod site containing the mod.</param>
|
||||
/// <param name="id">The mod ID within the site.</param>
|
||||
/// <param name="subkey">If specified, a substring in download names/descriptions to match.</param>
|
||||
public UpdateKey(string rawText, ModSiteKey site, string id, string subkey)
|
||||
{
|
||||
this.RawText = rawText;
|
||||
this.Repository = repository;
|
||||
this.ID = id;
|
||||
this.RawText = rawText?.Trim();
|
||||
this.Site = site;
|
||||
this.ID = id?.Trim();
|
||||
this.Subkey = subkey?.Trim();
|
||||
this.LooksValid =
|
||||
repository != ModRepositoryKey.Unknown
|
||||
site != ModSiteKey.Unknown
|
||||
&& !string.IsNullOrWhiteSpace(id);
|
||||
}
|
||||
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="repository">The mod repository containing the mod.</param>
|
||||
/// <param name="id">The mod ID within the repository.</param>
|
||||
public UpdateKey(ModRepositoryKey repository, string id)
|
||||
: this($"{repository}:{id}", repository, id) { }
|
||||
/// <param name="site">The mod site containing the mod.</param>
|
||||
/// <param name="id">The mod ID within the site.</param>
|
||||
/// <param name="subkey">If specified, a substring in download names/descriptions to match.</param>
|
||||
public UpdateKey(ModSiteKey site, string id, string subkey)
|
||||
: this(UpdateKey.GetString(site, id, subkey), site, id, subkey) { }
|
||||
|
||||
/// <summary>Parse a raw update key.</summary>
|
||||
/// <param name="raw">The raw update key to parse.</param>
|
||||
public static UpdateKey Parse(string raw)
|
||||
{
|
||||
// split parts
|
||||
string[] parts = raw?.Split(':');
|
||||
if (parts == null || parts.Length != 2)
|
||||
return new UpdateKey(raw, ModRepositoryKey.Unknown, null);
|
||||
// extract site + ID
|
||||
string rawSite;
|
||||
string id;
|
||||
{
|
||||
string[] parts = raw?.Trim().Split(':');
|
||||
if (parts == null || parts.Length != 2)
|
||||
return new UpdateKey(raw, ModSiteKey.Unknown, null, null);
|
||||
|
||||
// extract parts
|
||||
string repositoryKey = parts[0].Trim();
|
||||
string id = parts[1].Trim();
|
||||
rawSite = parts[0].Trim();
|
||||
id = parts[1].Trim();
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(id))
|
||||
id = null;
|
||||
|
||||
// parse
|
||||
if (!Enum.TryParse(repositoryKey, true, out ModRepositoryKey repository))
|
||||
return new UpdateKey(raw, ModRepositoryKey.Unknown, id);
|
||||
if (id == null)
|
||||
return new UpdateKey(raw, repository, null);
|
||||
// extract subkey
|
||||
string subkey = null;
|
||||
if (id != null)
|
||||
{
|
||||
string[] parts = id.Split('@');
|
||||
if (parts.Length == 2)
|
||||
{
|
||||
id = parts[0].Trim();
|
||||
subkey = $"@{parts[1]}".Trim();
|
||||
}
|
||||
}
|
||||
|
||||
return new UpdateKey(raw, repository, id);
|
||||
// parse
|
||||
if (!Enum.TryParse(rawSite, true, out ModSiteKey site))
|
||||
return new UpdateKey(raw, ModSiteKey.Unknown, id, subkey);
|
||||
if (id == null)
|
||||
return new UpdateKey(raw, site, null, subkey);
|
||||
|
||||
return new UpdateKey(raw, site, id, subkey);
|
||||
}
|
||||
|
||||
/// <summary>Get a string that represents the current object.</summary>
|
||||
public override string ToString()
|
||||
{
|
||||
return this.LooksValid
|
||||
? $"{this.Repository}:{this.ID}"
|
||||
? UpdateKey.GetString(this.Site, this.ID, this.Subkey)
|
||||
: this.RawText;
|
||||
}
|
||||
|
||||
|
@ -80,10 +101,18 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData
|
|||
/// <param name="other">An object to compare with this object.</param>
|
||||
public bool Equals(UpdateKey other)
|
||||
{
|
||||
if (!this.LooksValid)
|
||||
{
|
||||
return
|
||||
other?.LooksValid == false
|
||||
&& this.RawText.Equals(other.RawText, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
return
|
||||
other != null
|
||||
&& this.Repository == other.Repository
|
||||
&& string.Equals(this.ID, other.ID, StringComparison.InvariantCultureIgnoreCase);
|
||||
&& this.Site == other.Site
|
||||
&& string.Equals(this.ID, other.ID, StringComparison.OrdinalIgnoreCase)
|
||||
&& string.Equals(this.Subkey, other.Subkey, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>Determines whether the specified object is equal to the current object.</summary>
|
||||
|
@ -97,7 +126,16 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData
|
|||
/// <returns>A hash code for the current object.</returns>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return $"{this.Repository}:{this.ID}".ToLower().GetHashCode();
|
||||
return this.ToString().ToLower().GetHashCode();
|
||||
}
|
||||
|
||||
/// <summary>Get the string representation of an update key.</summary>
|
||||
/// <param name="site">The mod site containing the mod.</param>
|
||||
/// <param name="id">The mod ID within the repository.</param>
|
||||
/// <param name="subkey">If specified, a substring in download names/descriptions to match.</param>
|
||||
public static string GetString(ModSiteKey site, string id, string subkey = null)
|
||||
{
|
||||
return $"{site}:{id}{subkey}".Trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
using StardewModdingAPI.Toolkit.Framework.Clients.Wiki;
|
||||
|
@ -11,8 +10,6 @@ using StardewModdingAPI.Toolkit.Framework.ModData;
|
|||
using StardewModdingAPI.Toolkit.Framework.ModScanning;
|
||||
using StardewModdingAPI.Toolkit.Serialization;
|
||||
|
||||
[assembly: InternalsVisibleTo("StardewModdingAPI")]
|
||||
[assembly: InternalsVisibleTo("SMAPI.Web")]
|
||||
namespace StardewModdingAPI.Toolkit
|
||||
{
|
||||
/// <summary>A convenience wrapper for the various tools.</summary>
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("StardewModdingAPI")]
|
||||
[assembly: InternalsVisibleTo("SMAPI.Web")]
|
|
@ -1,20 +1,16 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<AssemblyName>SMAPI.Toolkit</AssemblyName>
|
||||
<RootNamespace>StardewModdingAPI.Toolkit</RootNamespace>
|
||||
<Description>A library which encapsulates mod-handling logic for mod managers and tools. Not intended for use by mods.</Description>
|
||||
<TargetFrameworks>net4.5;netstandard2.0</TargetFrameworks>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\SMAPI.Toolkit.xml</DocumentationFile>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<PlatformTarget Condition="'$(TargetFramework)' == 'net4.5'">x86</PlatformTarget>
|
||||
<RootNamespace>StardewModdingAPI.Toolkit</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.23" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||
<PackageReference Include="Pathoschild.Http.FluentClient" Version="3.3.1" />
|
||||
<PackageReference Include="Pathoschild.Http.FluentClient" Version="4.0.0" />
|
||||
<PackageReference Include="System.Management" Version="4.5.0" Condition="'$(OS)' == 'Windows_NT'" />
|
||||
<PackageReference Include="Microsoft.Win32.Registry" Version="4.5.0" Condition="'$(OS)' == 'Windows_NT' AND '$(TargetFramework)' == 'netstandard2.0'" />
|
||||
</ItemGroup>
|
||||
|
@ -24,5 +20,4 @@
|
|||
</ItemGroup>
|
||||
|
||||
<Import Project="..\..\build\common.targets" />
|
||||
|
||||
</Project>
|
||||
|
|
|
@ -30,10 +30,7 @@ namespace StardewModdingAPI.Toolkit.Utilities
|
|||
/// <summary>Detect the current OS.</summary>
|
||||
public static Platform DetectPlatform()
|
||||
{
|
||||
if (EnvironmentUtility.CachedPlatform == null)
|
||||
EnvironmentUtility.CachedPlatform = EnvironmentUtility.DetectPlatformImpl();
|
||||
|
||||
return EnvironmentUtility.CachedPlatform.Value;
|
||||
return EnvironmentUtility.CachedPlatform ??= EnvironmentUtility.DetectPlatformImpl();
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Pathoschild.Http.Client;
|
||||
using StardewModdingAPI.Toolkit.Framework.Clients.WebApi;
|
||||
|
||||
namespace SMAPI.Web.LegacyRedirects.Controllers
|
||||
{
|
||||
/// <summary>Provides an API to perform mod update checks.</summary>
|
||||
[ApiController]
|
||||
[Produces("application/json")]
|
||||
[Route("api/v{version}/mods")]
|
||||
public class ModsApiController : Controller
|
||||
{
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Fetch version metadata for the given mods.</summary>
|
||||
/// <param name="model">The mod search criteria.</param>
|
||||
[HttpPost]
|
||||
public async Task<IEnumerable<ModEntryModel>> PostAsync([FromBody] ModSearchModel model)
|
||||
{
|
||||
using IClient client = new FluentClient("https://smapi.io/api");
|
||||
|
||||
Startup.ConfigureJsonNet(client.Formatters.JsonFormatter.SerializerSettings);
|
||||
|
||||
return await client
|
||||
.PostAsync(this.Request.Path)
|
||||
.WithBody(model)
|
||||
.AsArray<ModEntryModel>();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
using System;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Rewrite;
|
||||
|
||||
namespace SMAPI.Web.LegacyRedirects.Framework
|
||||
{
|
||||
/// <summary>Rewrite requests to prepend the subdomain portion (if any) to the path.</summary>
|
||||
/// <remarks>Derived from <a href="https://stackoverflow.com/a/44526747/262123" />.</remarks>
|
||||
internal class LambdaRewriteRule : IRule
|
||||
{
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>Rewrite an HTTP request if needed.</summary>
|
||||
private readonly Action<RewriteContext, HttpRequest, HttpResponse> Rewrite;
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="rewrite">Rewrite an HTTP request if needed.</param>
|
||||
public LambdaRewriteRule(Action<RewriteContext, HttpRequest, HttpResponse> rewrite)
|
||||
{
|
||||
this.Rewrite = rewrite ?? throw new ArgumentNullException(nameof(rewrite));
|
||||
}
|
||||
|
||||
/// <summary>Applies the rule. Implementations of ApplyRule should set the value for <see cref="RewriteContext.Result" /> (defaults to RuleResult.ContinueRules).</summary>
|
||||
/// <param name="context">The rewrite context.</param>
|
||||
public void ApplyRule(RewriteContext context)
|
||||
{
|
||||
HttpRequest request = context.HttpContext.Request;
|
||||
HttpResponse response = context.HttpContext.Response;
|
||||
this.Rewrite(context, request, response);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace SMAPI.Web.LegacyRedirects
|
||||
{
|
||||
/// <summary>The main app entry point.</summary>
|
||||
public class Program
|
||||
{
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>The main app entry point.</summary>
|
||||
/// <param name="args">The command-line arguments.</param>
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
Host
|
||||
.CreateDefaultBuilder(args)
|
||||
.ConfigureWebHostDefaults(builder => builder.UseStartup<Startup>())
|
||||
.Build()
|
||||
.Run();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
{
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:52756",
|
||||
"sslPort": 0
|
||||
}
|
||||
},
|
||||
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"SMAPI.Web.LegacyRedirects": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "/",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"applicationUrl": "https://localhost:5001;http://localhost:5000"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Remove="aws-beanstalk-tools-defaults.json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.0.2" />
|
||||
<PackageReference Include="Pathoschild.Http.FluentClient" Version="3.3.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SMAPI.Toolkit.CoreInterfaces\SMAPI.Toolkit.CoreInterfaces.csproj" />
|
||||
<ProjectReference Include="..\SMAPI.Toolkit\SMAPI.Toolkit.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Hangfire;
|
||||
|
@ -36,7 +37,9 @@ namespace StardewModdingAPI.Web
|
|||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="wikiCache">The cache in which to store wiki metadata.</param>
|
||||
/// <param name="modCache">The cache in which to store mod data.</param>
|
||||
public BackgroundService(IWikiCacheRepository wikiCache, IModCacheRepository modCache)
|
||||
/// <param name="hangfireStorage">The Hangfire storage implementation.</param>
|
||||
[SuppressMessage("ReSharper", "UnusedParameter.Local", Justification = "The Hangfire reference forces it to initialize first, since it's needed by the background service.")]
|
||||
public BackgroundService(IWikiCacheRepository wikiCache, IModCacheRepository modCache, JobStorage hangfireStorage)
|
||||
{
|
||||
BackgroundService.WikiCache = wikiCache;
|
||||
BackgroundService.ModCache = modCache;
|
||||
|
@ -81,7 +84,7 @@ namespace StardewModdingAPI.Web
|
|||
public static async Task UpdateWikiAsync()
|
||||
{
|
||||
WikiModList wikiCompatList = await new ModToolkit().GetWikiCompatibilityListAsync();
|
||||
BackgroundService.WikiCache.SaveWikiData(wikiCompatList.StableVersion, wikiCompatList.BetaVersion, wikiCompatList.Mods, out _, out _);
|
||||
BackgroundService.WikiCache.SaveWikiData(wikiCompatList.StableVersion, wikiCompatList.BetaVersion, wikiCompatList.Mods);
|
||||
}
|
||||
|
||||
/// <summary>Remove mods which haven't been requested in over 48 hours.</summary>
|
||||
|
|
|
@ -27,12 +27,13 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
private readonly IDictionary<string, string> SchemaFormats = new Dictionary<string, string>
|
||||
{
|
||||
["none"] = "None",
|
||||
["manifest"] = "Manifest",
|
||||
["manifest"] = "SMAPI: manifest",
|
||||
["i18n"] = "SMAPI: translations (i18n)",
|
||||
["content-patcher"] = "Content Patcher"
|
||||
};
|
||||
|
||||
/// <summary>The schema ID to use if none was specified.</summary>
|
||||
private string DefaultSchemaID = "manifest";
|
||||
private string DefaultSchemaID = "none";
|
||||
|
||||
/// <summary>A token in an error message which indicates that the child errors should be displayed instead.</summary>
|
||||
private readonly string TransparentToken = "$transparent";
|
||||
|
@ -57,16 +58,22 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
/// <summary>Render the schema validator UI.</summary>
|
||||
/// <param name="schemaName">The schema name with which to validate the JSON, or 'edit' to return to the edit screen.</param>
|
||||
/// <param name="id">The stored file ID.</param>
|
||||
/// <param name="operation">The operation to perform for the selected log ID. This can be 'edit', or any other value to view.</param>
|
||||
[HttpGet]
|
||||
[Route("json")]
|
||||
[Route("json/{schemaName}")]
|
||||
[Route("json/{schemaName}/{id}")]
|
||||
public async Task<ViewResult> Index(string schemaName = null, string id = null)
|
||||
[Route("json/{schemaName}/{id}/{operation}")]
|
||||
public async Task<ViewResult> Index(string schemaName = null, string id = null, string operation = null)
|
||||
{
|
||||
// parse arguments
|
||||
schemaName = this.NormalizeSchemaName(schemaName);
|
||||
bool hasId = !string.IsNullOrWhiteSpace(id);
|
||||
bool isEditView = !hasId || operation?.Trim().ToLower() == "edit";
|
||||
|
||||
var result = new JsonValidatorModel(id, schemaName, this.SchemaFormats);
|
||||
if (string.IsNullOrWhiteSpace(id))
|
||||
// build result model
|
||||
var result = this.GetModel(id, schemaName, isEditView);
|
||||
if (!hasId)
|
||||
return this.View("Index", result);
|
||||
|
||||
// fetch raw JSON
|
||||
|
@ -76,7 +83,7 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
result.SetContent(file.Content, expiry: file.Expiry, uploadWarning: file.Warning);
|
||||
|
||||
// skip parsing if we're going to the edit screen
|
||||
if (schemaName?.ToLower() == "edit")
|
||||
if (isEditView)
|
||||
return this.View("Index", result);
|
||||
|
||||
// parse JSON
|
||||
|
@ -130,7 +137,7 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
public async Task<ActionResult> PostAsync(JsonValidatorRequestModel request)
|
||||
{
|
||||
if (request == null)
|
||||
return this.View("Index", this.GetModel(null, null).SetUploadError("The request seems to be invalid."));
|
||||
return this.View("Index", this.GetModel(null, null, isEditView: true).SetUploadError("The request seems to be invalid."));
|
||||
|
||||
// normalize schema name
|
||||
string schemaName = this.NormalizeSchemaName(request.SchemaName);
|
||||
|
@ -138,12 +145,12 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
// get raw text
|
||||
string input = request.Content;
|
||||
if (string.IsNullOrWhiteSpace(input))
|
||||
return this.View("Index", this.GetModel(null, schemaName).SetUploadError("The JSON file seems to be empty."));
|
||||
return this.View("Index", this.GetModel(null, schemaName, isEditView: true).SetUploadError("The JSON file seems to be empty."));
|
||||
|
||||
// upload file
|
||||
UploadResult result = await this.Storage.SaveAsync(input);
|
||||
if (!result.Succeeded)
|
||||
return this.View("Index", this.GetModel(result.ID, schemaName).SetUploadError(result.UploadError));
|
||||
return this.View("Index", this.GetModel(result.ID, schemaName, isEditView: true).SetContent(input, null).SetUploadError(result.UploadError));
|
||||
|
||||
// redirect to view
|
||||
return this.Redirect(this.Url.PlainAction("Index", "JsonValidator", new { schemaName = schemaName, id = result.ID }));
|
||||
|
@ -156,9 +163,10 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
/// <summary>Build a JSON validator model.</summary>
|
||||
/// <param name="pasteID">The stored file ID.</param>
|
||||
/// <param name="schemaName">The schema name with which the JSON was validated.</param>
|
||||
private JsonValidatorModel GetModel(string pasteID, string schemaName)
|
||||
/// <param name="isEditView">Whether to show the edit view.</param>
|
||||
private JsonValidatorModel GetModel(string pasteID, string schemaName, bool isEditView)
|
||||
{
|
||||
return new JsonValidatorModel(pasteID, schemaName, this.SchemaFormats);
|
||||
return new JsonValidatorModel(pasteID, schemaName, this.SchemaFormats, isEditView);
|
||||
}
|
||||
|
||||
/// <summary>Get a normalized schema name, or the <see cref="DefaultSchemaID"/> if blank.</summary>
|
||||
|
@ -275,21 +283,20 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
errors = new Dictionary<string, string>(errors, StringComparer.InvariantCultureIgnoreCase);
|
||||
|
||||
// match error by type and message
|
||||
foreach (var pair in errors)
|
||||
foreach ((string target, string errorMessage) in errors)
|
||||
{
|
||||
if (!pair.Key.Contains(":"))
|
||||
if (!target.Contains(":"))
|
||||
continue;
|
||||
|
||||
string[] parts = pair.Key.Split(':', 2);
|
||||
string[] parts = target.Split(':', 2);
|
||||
if (parts[0].Equals(error.ErrorType.ToString(), StringComparison.InvariantCultureIgnoreCase) && Regex.IsMatch(error.Message, parts[1]))
|
||||
return pair.Value?.Trim();
|
||||
return errorMessage?.Trim();
|
||||
}
|
||||
|
||||
// match by type
|
||||
if (errors.TryGetValue(error.ErrorType.ToString(), out string message))
|
||||
return message?.Trim();
|
||||
|
||||
return null;
|
||||
return errors.TryGetValue(error.ErrorType.ToString(), out string message)
|
||||
? message?.Trim()
|
||||
: null;
|
||||
}
|
||||
|
||||
return GetRawOverrideError()
|
||||
|
@ -304,10 +311,10 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
{
|
||||
if (schema.ExtensionData != null)
|
||||
{
|
||||
foreach (var pair in schema.ExtensionData)
|
||||
foreach ((string curKey, JToken value) in schema.ExtensionData)
|
||||
{
|
||||
if (pair.Key.Equals(key, StringComparison.InvariantCultureIgnoreCase))
|
||||
return pair.Value.ToObject<T>();
|
||||
if (curKey.Equals(key, StringComparison.InvariantCultureIgnoreCase))
|
||||
return value.ToObject<T>();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -318,14 +325,11 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
/// <param name="value">The value to format.</param>
|
||||
private string FormatValue(object value)
|
||||
{
|
||||
switch (value)
|
||||
return value switch
|
||||
{
|
||||
case List<string> list:
|
||||
return string.Join(", ", list);
|
||||
|
||||
default:
|
||||
return value?.ToString() ?? "null";
|
||||
}
|
||||
List<string> list => string.Join(", ", list),
|
||||
_ => value?.ToString() ?? "null"
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,15 +12,16 @@ using StardewModdingAPI.Toolkit.Framework.Clients.Wiki;
|
|||
using StardewModdingAPI.Toolkit.Framework.ModData;
|
||||
using StardewModdingAPI.Toolkit.Framework.UpdateData;
|
||||
using StardewModdingAPI.Web.Framework;
|
||||
using StardewModdingAPI.Web.Framework.Caching;
|
||||
using StardewModdingAPI.Web.Framework.Caching.Mods;
|
||||
using StardewModdingAPI.Web.Framework.Caching.Wiki;
|
||||
using StardewModdingAPI.Web.Framework.Clients;
|
||||
using StardewModdingAPI.Web.Framework.Clients.Chucklefish;
|
||||
using StardewModdingAPI.Web.Framework.Clients.CurseForge;
|
||||
using StardewModdingAPI.Web.Framework.Clients.GitHub;
|
||||
using StardewModdingAPI.Web.Framework.Clients.ModDrop;
|
||||
using StardewModdingAPI.Web.Framework.Clients.Nexus;
|
||||
using StardewModdingAPI.Web.Framework.ConfigModels;
|
||||
using StardewModdingAPI.Web.Framework.ModRepositories;
|
||||
|
||||
namespace StardewModdingAPI.Web.Controllers
|
||||
{
|
||||
|
@ -32,8 +33,8 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
/*********
|
||||
** Fields
|
||||
*********/
|
||||
/// <summary>The mod repositories which provide mod metadata.</summary>
|
||||
private readonly IDictionary<ModRepositoryKey, IModRepository> Repositories;
|
||||
/// <summary>The mod sites which provide mod metadata.</summary>
|
||||
private readonly ModSiteManager ModSites;
|
||||
|
||||
/// <summary>The cache in which to store wiki data.</summary>
|
||||
private readonly IWikiCacheRepository WikiCache;
|
||||
|
@ -61,23 +62,14 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
/// <param name="github">The GitHub API client.</param>
|
||||
/// <param name="modDrop">The ModDrop API client.</param>
|
||||
/// <param name="nexus">The Nexus API client.</param>
|
||||
public ModsApiController(IHostingEnvironment environment, IWikiCacheRepository wikiCache, IModCacheRepository modCache, IOptions<ModUpdateCheckConfig> config, IChucklefishClient chucklefish, ICurseForgeClient curseForge, IGitHubClient github, IModDropClient modDrop, INexusClient nexus)
|
||||
public ModsApiController(IWebHostEnvironment environment, IWikiCacheRepository wikiCache, IModCacheRepository modCache, IOptions<ModUpdateCheckConfig> config, IChucklefishClient chucklefish, ICurseForgeClient curseForge, IGitHubClient github, IModDropClient modDrop, INexusClient nexus)
|
||||
{
|
||||
this.ModDatabase = new ModToolkit().GetModDatabase(Path.Combine(environment.WebRootPath, "SMAPI.metadata.json"));
|
||||
|
||||
this.WikiCache = wikiCache;
|
||||
this.ModCache = modCache;
|
||||
this.Config = config;
|
||||
this.Repositories =
|
||||
new IModRepository[]
|
||||
{
|
||||
new ChucklefishRepository(chucklefish),
|
||||
new CurseForgeRepository(curseForge),
|
||||
new GitHubRepository(github),
|
||||
new ModDropRepository(modDrop),
|
||||
new NexusRepository(nexus)
|
||||
}
|
||||
.ToDictionary(p => p.VendorKey);
|
||||
this.ModSites = new ModSiteManager(new IModSiteClient[] { chucklefish, curseForge, github, modDrop, nexus });
|
||||
}
|
||||
|
||||
/// <summary>Fetch version metadata for the given mods.</summary>
|
||||
|
@ -90,7 +82,7 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
return new ModEntryModel[0];
|
||||
|
||||
// fetch wiki data
|
||||
WikiModEntry[] wikiData = this.WikiCache.GetWikiMods().Select(p => p.GetModel()).ToArray();
|
||||
WikiModEntry[] wikiData = this.WikiCache.GetWikiMods().Select(p => p.Data).ToArray();
|
||||
IDictionary<string, ModEntryModel> mods = new Dictionary<string, ModEntryModel>(StringComparer.CurrentCultureIgnoreCase);
|
||||
foreach (ModSearchEntryModel mod in model.Mods)
|
||||
{
|
||||
|
@ -143,45 +135,23 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
// validate update key
|
||||
if (!updateKey.LooksValid)
|
||||
{
|
||||
errors.Add($"The update key '{updateKey}' isn't in a valid format. It should contain the site key and mod ID like 'Nexus:541'.");
|
||||
errors.Add($"The update key '{updateKey}' isn't in a valid format. It should contain the site key and mod ID like 'Nexus:541', with an optional subkey like 'Nexus:541@subkey'.");
|
||||
continue;
|
||||
}
|
||||
|
||||
// fetch data
|
||||
ModInfoModel data = await this.GetInfoForUpdateKeyAsync(updateKey, allowNonStandardVersions);
|
||||
if (data.Error != null)
|
||||
ModInfoModel data = await this.GetInfoForUpdateKeyAsync(updateKey, allowNonStandardVersions, wikiEntry?.MapRemoteVersions);
|
||||
if (data.Status != RemoteModStatus.Ok)
|
||||
{
|
||||
errors.Add(data.Error);
|
||||
errors.Add(data.Error ?? data.Status.ToString());
|
||||
continue;
|
||||
}
|
||||
|
||||
// handle main version
|
||||
if (data.Version != null)
|
||||
{
|
||||
ISemanticVersion version = this.GetMappedVersion(data.Version, wikiEntry?.MapRemoteVersions, allowNonStandardVersions);
|
||||
if (version == null)
|
||||
{
|
||||
errors.Add($"The update key '{updateKey}' matches a mod with invalid semantic version '{data.Version}'.");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.IsNewer(version, main?.Version))
|
||||
main = new ModEntryVersionModel(version, data.Url);
|
||||
}
|
||||
|
||||
// handle optional version
|
||||
if (data.PreviewVersion != null)
|
||||
{
|
||||
ISemanticVersion version = this.GetMappedVersion(data.PreviewVersion, wikiEntry?.MapRemoteVersions, allowNonStandardVersions);
|
||||
if (version == null)
|
||||
{
|
||||
errors.Add($"The update key '{updateKey}' matches a mod with invalid optional semantic version '{data.PreviewVersion}'.");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.IsNewer(version, optional?.Version))
|
||||
optional = new ModEntryVersionModel(version, data.Url);
|
||||
}
|
||||
// handle versions
|
||||
if (this.IsNewer(data.Version, main?.Version))
|
||||
main = new ModEntryVersionModel(data.Version, data.Url);
|
||||
if (this.IsNewer(data.PreviewVersion, optional?.Version))
|
||||
optional = new ModEntryVersionModel(data.PreviewVersion, data.Url);
|
||||
}
|
||||
|
||||
// get unofficial version
|
||||
|
@ -221,7 +191,7 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
}
|
||||
|
||||
// get recommended update (if any)
|
||||
ISemanticVersion installedVersion = this.GetMappedVersion(search.InstalledVersion?.ToString(), wikiEntry?.MapLocalVersions, allowNonStandard: allowNonStandardVersions);
|
||||
ISemanticVersion installedVersion = this.ModSites.GetMappedVersion(search.InstalledVersion?.ToString(), wikiEntry?.MapLocalVersions, allowNonStandard: allowNonStandardVersions);
|
||||
if (apiVersion != null && installedVersion != null)
|
||||
{
|
||||
// get newer versions
|
||||
|
@ -281,29 +251,27 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
/// <summary>Get the mod info for an update key.</summary>
|
||||
/// <param name="updateKey">The namespaced update key.</param>
|
||||
/// <param name="allowNonStandardVersions">Whether to allow non-standard versions.</param>
|
||||
private async Task<ModInfoModel> GetInfoForUpdateKeyAsync(UpdateKey updateKey, bool allowNonStandardVersions)
|
||||
/// <param name="mapRemoteVersions">Maps remote versions to a semantic version for update checks.</param>
|
||||
private async Task<ModInfoModel> GetInfoForUpdateKeyAsync(UpdateKey updateKey, bool allowNonStandardVersions, IDictionary<string, string> mapRemoteVersions)
|
||||
{
|
||||
// get mod
|
||||
if (!this.ModCache.TryGetMod(updateKey.Repository, updateKey.ID, out CachedMod mod) || this.ModCache.IsStale(mod.LastUpdated, mod.FetchStatus == RemoteModStatus.TemporaryError ? this.Config.Value.ErrorCacheMinutes : this.Config.Value.SuccessCacheMinutes))
|
||||
// get mod page
|
||||
IModPage page;
|
||||
{
|
||||
// get site
|
||||
if (!this.Repositories.TryGetValue(updateKey.Repository, out IModRepository repository))
|
||||
return new ModInfoModel().SetError(RemoteModStatus.DoesNotExist, $"There's no mod site with key '{updateKey.Repository}'. Expected one of [{string.Join(", ", this.Repositories.Keys)}].");
|
||||
bool isCached =
|
||||
this.ModCache.TryGetMod(updateKey.Site, updateKey.ID, out Cached<IModPage> cachedMod)
|
||||
&& !this.ModCache.IsStale(cachedMod.LastUpdated, cachedMod.Data.Status == RemoteModStatus.TemporaryError ? this.Config.Value.ErrorCacheMinutes : this.Config.Value.SuccessCacheMinutes);
|
||||
|
||||
// fetch mod
|
||||
ModInfoModel result = await repository.GetModInfoAsync(updateKey.ID);
|
||||
if (result.Error == null)
|
||||
if (isCached)
|
||||
page = cachedMod.Data;
|
||||
else
|
||||
{
|
||||
if (result.Version == null)
|
||||
result.SetError(RemoteModStatus.InvalidData, $"The update key '{updateKey}' matches a mod with no version number.");
|
||||
else if (!SemanticVersion.TryParse(result.Version, allowNonStandardVersions, out _))
|
||||
result.SetError(RemoteModStatus.InvalidData, $"The update key '{updateKey}' matches a mod with invalid semantic version '{result.Version}'.");
|
||||
page = await this.ModSites.GetModPageAsync(updateKey);
|
||||
this.ModCache.SaveMod(updateKey.Site, updateKey.ID, page);
|
||||
}
|
||||
|
||||
// cache mod
|
||||
this.ModCache.SaveMod(repository.VendorKey, updateKey.ID, result, out mod);
|
||||
}
|
||||
return mod.GetModel();
|
||||
|
||||
// get version info
|
||||
return this.ModSites.GetPageVersions(page, updateKey.Subkey, allowNonStandardVersions, mapRemoteVersions);
|
||||
}
|
||||
|
||||
/// <summary>Get update keys based on the available mod metadata, while maintaining the precedence order.</summary>
|
||||
|
@ -312,90 +280,79 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
/// <param name="entry">The mod's entry in the wiki list.</param>
|
||||
private IEnumerable<UpdateKey> GetUpdateKeys(string[] specifiedKeys, ModDataRecord record, WikiModEntry entry)
|
||||
{
|
||||
IEnumerable<string> GetRaw()
|
||||
// get unique update keys
|
||||
List<UpdateKey> updateKeys = this.GetUnfilteredUpdateKeys(specifiedKeys, record, entry)
|
||||
.Select(UpdateKey.Parse)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
// apply remove overrides from wiki
|
||||
{
|
||||
// specified update keys
|
||||
if (specifiedKeys != null)
|
||||
{
|
||||
foreach (string key in specifiedKeys)
|
||||
yield return key?.Trim();
|
||||
}
|
||||
|
||||
// default update key
|
||||
string defaultKey = record?.GetDefaultUpdateKey();
|
||||
if (defaultKey != null)
|
||||
yield return defaultKey;
|
||||
|
||||
// wiki metadata
|
||||
if (entry != null)
|
||||
{
|
||||
if (entry.NexusID.HasValue)
|
||||
yield return $"{ModRepositoryKey.Nexus}:{entry.NexusID}";
|
||||
if (entry.ModDropID.HasValue)
|
||||
yield return $"{ModRepositoryKey.ModDrop}:{entry.ModDropID}";
|
||||
if (entry.CurseForgeID.HasValue)
|
||||
yield return $"{ModRepositoryKey.CurseForge}:{entry.CurseForgeID}";
|
||||
if (entry.ChucklefishID.HasValue)
|
||||
yield return $"{ModRepositoryKey.Chucklefish}:{entry.ChucklefishID}";
|
||||
}
|
||||
var removeKeys = new HashSet<UpdateKey>(
|
||||
from key in entry?.ChangeUpdateKeys ?? new string[0]
|
||||
where key.StartsWith('-')
|
||||
select UpdateKey.Parse(key.Substring(1))
|
||||
);
|
||||
if (removeKeys.Any())
|
||||
updateKeys.RemoveAll(removeKeys.Contains);
|
||||
}
|
||||
|
||||
HashSet<UpdateKey> seen = new HashSet<UpdateKey>();
|
||||
foreach (string rawKey in GetRaw())
|
||||
// if the list has both an update key (like "Nexus:2400") and subkey (like "Nexus:2400@subkey") for the same page, the subkey takes priority
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(rawKey))
|
||||
continue;
|
||||
var removeKeys = new HashSet<UpdateKey>();
|
||||
foreach (var key in updateKeys)
|
||||
{
|
||||
if (key.Subkey != null)
|
||||
removeKeys.Add(new UpdateKey(key.Site, key.ID, null));
|
||||
}
|
||||
if (removeKeys.Any())
|
||||
updateKeys.RemoveAll(removeKeys.Contains);
|
||||
}
|
||||
|
||||
UpdateKey key = UpdateKey.Parse(rawKey);
|
||||
if (seen.Add(key))
|
||||
return updateKeys;
|
||||
}
|
||||
|
||||
/// <summary>Get every available update key based on the available mod metadata, including duplicates and keys which should be filtered.</summary>
|
||||
/// <param name="specifiedKeys">The specified update keys.</param>
|
||||
/// <param name="record">The mod's entry in SMAPI's internal database.</param>
|
||||
/// <param name="entry">The mod's entry in the wiki list.</param>
|
||||
private IEnumerable<string> GetUnfilteredUpdateKeys(string[] specifiedKeys, ModDataRecord record, WikiModEntry entry)
|
||||
{
|
||||
// specified update keys
|
||||
foreach (string key in specifiedKeys ?? Array.Empty<string>())
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(key))
|
||||
yield return key.Trim();
|
||||
}
|
||||
|
||||
// default update key
|
||||
{
|
||||
string defaultKey = record?.GetDefaultUpdateKey();
|
||||
if (!string.IsNullOrWhiteSpace(defaultKey))
|
||||
yield return defaultKey;
|
||||
}
|
||||
|
||||
// wiki metadata
|
||||
if (entry != null)
|
||||
{
|
||||
if (entry.NexusID.HasValue)
|
||||
yield return UpdateKey.GetString(ModSiteKey.Nexus, entry.NexusID.ToString());
|
||||
if (entry.ModDropID.HasValue)
|
||||
yield return UpdateKey.GetString(ModSiteKey.ModDrop, entry.ModDropID.ToString());
|
||||
if (entry.CurseForgeID.HasValue)
|
||||
yield return UpdateKey.GetString(ModSiteKey.CurseForge, entry.CurseForgeID.ToString());
|
||||
if (entry.ChucklefishID.HasValue)
|
||||
yield return UpdateKey.GetString(ModSiteKey.Chucklefish, entry.ChucklefishID.ToString());
|
||||
}
|
||||
|
||||
// overrides from wiki
|
||||
foreach (string key in entry?.ChangeUpdateKeys ?? Array.Empty<string>())
|
||||
{
|
||||
if (key.StartsWith('+'))
|
||||
yield return key.Substring(1);
|
||||
else if (!key.StartsWith("-"))
|
||||
yield return key;
|
||||
}
|
||||
}
|
||||
|
||||
/// <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>
|
||||
private 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;
|
||||
}
|
||||
|
||||
/// <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 (var pair in map)
|
||||
{
|
||||
if (SemanticVersion.TryParse(pair.Key, allowNonStandard, out ISemanticVersion target) && parsed.Equals(target) && SemanticVersion.TryParse(pair.Value, allowNonStandard, out ISemanticVersion newVersion))
|
||||
return newVersion.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
return version;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ using System.Linq;
|
|||
using System.Text.RegularExpressions;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StardewModdingAPI.Web.Framework.Caching;
|
||||
using StardewModdingAPI.Web.Framework.Caching.Wiki;
|
||||
using StardewModdingAPI.Web.Framework.ConfigModels;
|
||||
using StardewModdingAPI.Web.ViewModels;
|
||||
|
@ -51,16 +52,16 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
public ModListModel FetchData()
|
||||
{
|
||||
// fetch cached data
|
||||
if (!this.Cache.TryGetWikiMetadata(out CachedWikiMetadata metadata))
|
||||
if (!this.Cache.TryGetWikiMetadata(out Cached<WikiMetadata> metadata))
|
||||
return new ModListModel();
|
||||
|
||||
// build model
|
||||
return new ModListModel(
|
||||
stableVersion: metadata.StableVersion,
|
||||
betaVersion: metadata.BetaVersion,
|
||||
stableVersion: metadata.Data.StableVersion,
|
||||
betaVersion: metadata.Data.BetaVersion,
|
||||
mods: this.Cache
|
||||
.GetWikiMods()
|
||||
.Select(mod => new ModModel(mod.GetModel()))
|
||||
.Select(mod => new ModModel(mod.Data))
|
||||
.OrderBy(p => Regex.Replace(p.Name.ToLower(), "[^a-z0-9]", "")), // ignore case, spaces, and special characters when sorting
|
||||
lastUpdated: metadata.LastUpdated,
|
||||
isStale: this.Cache.IsStale(metadata.LastUpdated, this.StaleMinutes)
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
using System;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework.Caching
|
||||
{
|
||||
/// <summary>A cache entry.</summary>
|
||||
/// <typeparam name="T">The cached value type.</typeparam>
|
||||
internal class Cached<T>
|
||||
{
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>The cached data.</summary>
|
||||
public T Data { get; set; }
|
||||
|
||||
/// <summary>When the data was last updated.</summary>
|
||||
public DateTimeOffset LastUpdated { get; set; }
|
||||
|
||||
/// <summary>When the data was last requested through the mod API.</summary>
|
||||
public DateTimeOffset LastRequested { get; set; }
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Construct an empty instance.</summary>
|
||||
public Cached() { }
|
||||
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="data">The cached data.</param>
|
||||
public Cached(T data)
|
||||
{
|
||||
this.Data = data;
|
||||
this.LastUpdated = DateTimeOffset.UtcNow;
|
||||
this.LastRequested = DateTimeOffset.UtcNow;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,107 +0,0 @@
|
|||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Bson.Serialization.Attributes;
|
||||
using StardewModdingAPI.Toolkit.Framework.UpdateData;
|
||||
using StardewModdingAPI.Web.Framework.ModRepositories;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework.Caching.Mods
|
||||
{
|
||||
/// <summary>The model for cached mod data.</summary>
|
||||
internal class CachedMod
|
||||
{
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/****
|
||||
** Tracking
|
||||
****/
|
||||
/// <summary>The internal MongoDB ID.</summary>
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Named per MongoDB conventions.")]
|
||||
[BsonIgnoreIfDefault]
|
||||
public ObjectId _id { get; set; }
|
||||
|
||||
/// <summary>When the data was last updated.</summary>
|
||||
public DateTimeOffset LastUpdated { get; set; }
|
||||
|
||||
/// <summary>When the data was last requested through the web API.</summary>
|
||||
public DateTimeOffset LastRequested { get; set; }
|
||||
|
||||
/****
|
||||
** Metadata
|
||||
****/
|
||||
/// <summary>The mod site on which the mod is found.</summary>
|
||||
public ModRepositoryKey Site { get; set; }
|
||||
|
||||
/// <summary>The mod's unique ID within the <see cref="Site"/>.</summary>
|
||||
public string ID { get; set; }
|
||||
|
||||
/// <summary>The mod availability status on the remote site.</summary>
|
||||
public RemoteModStatus FetchStatus { get; set; }
|
||||
|
||||
/// <summary>The error message providing more info for the <see cref="FetchStatus"/>, if applicable.</summary>
|
||||
public string FetchError { get; set; }
|
||||
|
||||
|
||||
/****
|
||||
** Mod info
|
||||
****/
|
||||
/// <summary>The mod's display name.</summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>The mod's latest version.</summary>
|
||||
public string MainVersion { get; set; }
|
||||
|
||||
/// <summary>The mod's latest optional or prerelease version, if newer than <see cref="MainVersion"/>.</summary>
|
||||
public string PreviewVersion { get; set; }
|
||||
|
||||
/// <summary>The URL for the mod page.</summary>
|
||||
public string Url { get; set; }
|
||||
|
||||
/// <summary>The license URL, if available.</summary>
|
||||
public string LicenseUrl { get; set; }
|
||||
|
||||
/// <summary>The license name, if available.</summary>
|
||||
public string LicenseName { get; set; }
|
||||
|
||||
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
public CachedMod() { }
|
||||
|
||||
/// <summary>Construct an instance.</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>
|
||||
public CachedMod(ModRepositoryKey site, string id, ModInfoModel mod)
|
||||
{
|
||||
// tracking
|
||||
this.LastUpdated = DateTimeOffset.UtcNow;
|
||||
this.LastRequested = DateTimeOffset.UtcNow;
|
||||
|
||||
// metadata
|
||||
this.Site = site;
|
||||
this.ID = id;
|
||||
this.FetchStatus = mod.Status;
|
||||
this.FetchError = mod.Error;
|
||||
|
||||
// mod info
|
||||
this.Name = mod.Name;
|
||||
this.MainVersion = mod.Version;
|
||||
this.PreviewVersion = mod.PreviewVersion;
|
||||
this.Url = mod.Url;
|
||||
this.LicenseUrl = mod.LicenseUrl;
|
||||
this.LicenseName = mod.LicenseName;
|
||||
}
|
||||
|
||||
/// <summary>Get the API model for the cached data.</summary>
|
||||
public ModInfoModel GetModel()
|
||||
{
|
||||
return new ModInfoModel(name: this.Name, version: this.MainVersion, url: this.Url, previewVersion: this.PreviewVersion)
|
||||
.SetLicense(this.LicenseUrl, this.LicenseName)
|
||||
.SetError(this.FetchStatus, this.FetchError);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,10 +1,10 @@
|
|||
using System;
|
||||
using StardewModdingAPI.Toolkit.Framework.UpdateData;
|
||||
using StardewModdingAPI.Web.Framework.ModRepositories;
|
||||
using StardewModdingAPI.Web.Framework.Clients;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework.Caching.Mods
|
||||
{
|
||||
/// <summary>Encapsulates logic for accessing the mod data cache.</summary>
|
||||
/// <summary>Manages cached mod data.</summary>
|
||||
internal interface IModCacheRepository : ICacheRepository
|
||||
{
|
||||
/*********
|
||||
|
@ -15,14 +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 CachedMod 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>
|
||||
/// <param name="cachedMod">The stored mod record.</param>
|
||||
void SaveMod(ModRepositoryKey site, string id, ModInfoModel mod, out CachedMod cachedMod);
|
||||
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>
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using StardewModdingAPI.Toolkit.Framework.UpdateData;
|
||||
using StardewModdingAPI.Web.Framework.Clients;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework.Caching.Mods
|
||||
{
|
||||
/// <summary>Manages cached mod data in-memory.</summary>
|
||||
internal class ModCacheMemoryRepository : BaseCacheRepository, IModCacheRepository
|
||||
{
|
||||
/*********
|
||||
** Fields
|
||||
*********/
|
||||
/// <summary>The cached mod data indexed by <c>{site key}:{ID}</c>.</summary>
|
||||
private readonly IDictionary<string, Cached<IModPage>> Mods = new Dictionary<string, Cached<IModPage>>(StringComparer.InvariantCultureIgnoreCase);
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Get the cached mod data.</summary>
|
||||
/// <param name="site">The mod site to search.</param>
|
||||
/// <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(ModSiteKey site, string id, out Cached<IModPage> mod, bool markRequested = true)
|
||||
{
|
||||
// get mod
|
||||
if (!this.Mods.TryGetValue(this.GetKey(site, id), out var cachedMod))
|
||||
{
|
||||
mod = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
// bump 'last requested'
|
||||
if (markRequested)
|
||||
cachedMod.LastRequested = DateTimeOffset.UtcNow;
|
||||
|
||||
mod = cachedMod;
|
||||
return 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>
|
||||
public void SaveMod(ModSiteKey site, string id, IModPage mod)
|
||||
{
|
||||
string key = this.GetKey(site, id);
|
||||
this.Mods[key] = new Cached<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>
|
||||
public void RemoveStaleMods(TimeSpan age)
|
||||
{
|
||||
DateTimeOffset minDate = DateTimeOffset.UtcNow.Subtract(age);
|
||||
|
||||
string[] staleKeys = this.Mods
|
||||
.Where(p => p.Value.LastRequested < minDate)
|
||||
.Select(p => p.Key)
|
||||
.ToArray();
|
||||
|
||||
foreach (string key in staleKeys)
|
||||
this.Mods.Remove(key);
|
||||
}
|
||||
|
||||
|
||||
/*********
|
||||
** Private methods
|
||||
*********/
|
||||
/// <summary>Get a cache key.</summary>
|
||||
/// <param name="site">The mod site.</param>
|
||||
/// <param name="id">The mod ID.</param>
|
||||
private string GetKey(ModSiteKey site, string id)
|
||||
{
|
||||
return $"{site}:{id.Trim()}".ToLower();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,104 +0,0 @@
|
|||
using System;
|
||||
using MongoDB.Driver;
|
||||
using StardewModdingAPI.Toolkit.Framework.UpdateData;
|
||||
using StardewModdingAPI.Web.Framework.ModRepositories;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework.Caching.Mods
|
||||
{
|
||||
/// <summary>Encapsulates logic for accessing the mod data cache.</summary>
|
||||
internal class ModCacheRepository : BaseCacheRepository, IModCacheRepository
|
||||
{
|
||||
/*********
|
||||
** Fields
|
||||
*********/
|
||||
/// <summary>The collection for cached mod data.</summary>
|
||||
private readonly IMongoCollection<CachedMod> Mods;
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="database">The authenticated MongoDB database.</param>
|
||||
public ModCacheRepository(IMongoDatabase database)
|
||||
{
|
||||
// get collections
|
||||
this.Mods = database.GetCollection<CachedMod>("mods");
|
||||
|
||||
// add indexes if needed
|
||||
this.Mods.Indexes.CreateOne(new CreateIndexModel<CachedMod>(Builders<CachedMod>.IndexKeys.Ascending(p => p.ID).Ascending(p => p.Site)));
|
||||
}
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Get the cached mod data.</summary>
|
||||
/// <param name="site">The mod site to search.</param>
|
||||
/// <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 CachedMod mod, bool markRequested = true)
|
||||
{
|
||||
// get mod
|
||||
id = this.NormalizeId(id);
|
||||
mod = this.Mods.Find(entry => entry.ID == id && entry.Site == site).FirstOrDefault();
|
||||
if (mod == null)
|
||||
return false;
|
||||
|
||||
// bump 'last requested'
|
||||
if (markRequested)
|
||||
{
|
||||
mod.LastRequested = DateTimeOffset.UtcNow;
|
||||
mod = this.SaveMod(mod);
|
||||
}
|
||||
|
||||
return 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>
|
||||
/// <param name="cachedMod">The stored mod record.</param>
|
||||
public void SaveMod(ModRepositoryKey site, string id, ModInfoModel mod, out CachedMod cachedMod)
|
||||
{
|
||||
id = this.NormalizeId(id);
|
||||
|
||||
cachedMod = this.SaveMod(new CachedMod(site, id, 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>
|
||||
public void RemoveStaleMods(TimeSpan age)
|
||||
{
|
||||
DateTimeOffset minDate = DateTimeOffset.UtcNow.Subtract(age);
|
||||
var result = this.Mods.DeleteMany(p => p.LastRequested < minDate);
|
||||
}
|
||||
|
||||
|
||||
/*********
|
||||
** Private methods
|
||||
*********/
|
||||
/// <summary>Save data fetched for a mod.</summary>
|
||||
/// <param name="mod">The mod data.</param>
|
||||
public CachedMod SaveMod(CachedMod mod)
|
||||
{
|
||||
string id = this.NormalizeId(mod.ID);
|
||||
|
||||
this.Mods.ReplaceOne(
|
||||
entry => entry.ID == id && entry.Site == mod.Site,
|
||||
mod,
|
||||
new UpdateOptions { IsUpsert = true }
|
||||
);
|
||||
|
||||
return mod;
|
||||
}
|
||||
|
||||
/// <summary>Normalize a mod ID for case-insensitive search.</summary>
|
||||
/// <param name="id">The mod ID.</param>
|
||||
public string NormalizeId(string id)
|
||||
{
|
||||
return id.Trim().ToLower();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
using System;
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Bson.Serialization;
|
||||
using MongoDB.Bson.Serialization.Serializers;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework.Caching
|
||||
{
|
||||
/// <summary>Serializes <see cref="DateTimeOffset"/> to a UTC date field instead of the default array.</summary>
|
||||
public class UtcDateTimeOffsetSerializer : StructSerializerBase<DateTimeOffset>
|
||||
{
|
||||
/*********
|
||||
** Fields
|
||||
*********/
|
||||
/// <summary>The underlying date serializer.</summary>
|
||||
private static readonly DateTimeSerializer DateTimeSerializer = new DateTimeSerializer(DateTimeKind.Utc, BsonType.DateTime);
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Deserializes a value.</summary>
|
||||
/// <param name="context">The deserialization context.</param>
|
||||
/// <param name="args">The deserialization args.</param>
|
||||
/// <returns>A deserialized value.</returns>
|
||||
public override DateTimeOffset Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
|
||||
{
|
||||
DateTime date = UtcDateTimeOffsetSerializer.DateTimeSerializer.Deserialize(context, args);
|
||||
return new DateTimeOffset(date, TimeSpan.Zero);
|
||||
}
|
||||
|
||||
/// <summary>Serializes a value.</summary>
|
||||
/// <param name="context">The serialization context.</param>
|
||||
/// <param name="args">The serialization args.</param>
|
||||
/// <param name="value">The object.</param>
|
||||
public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, DateTimeOffset value)
|
||||
{
|
||||
UtcDateTimeOffsetSerializer.DateTimeSerializer.Serialize(context, args, value.UtcDateTime);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,230 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Bson.Serialization.Attributes;
|
||||
using MongoDB.Bson.Serialization.Options;
|
||||
using StardewModdingAPI.Toolkit;
|
||||
using StardewModdingAPI.Toolkit.Framework.Clients.Wiki;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework.Caching.Wiki
|
||||
{
|
||||
/// <summary>The model for cached wiki mods.</summary>
|
||||
internal class CachedWikiMod
|
||||
{
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/****
|
||||
** Tracking
|
||||
****/
|
||||
/// <summary>The internal MongoDB ID.</summary>
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Named per MongoDB conventions.")]
|
||||
public ObjectId _id { get; set; }
|
||||
|
||||
/// <summary>When the data was last updated.</summary>
|
||||
public DateTimeOffset LastUpdated { get; set; }
|
||||
|
||||
/****
|
||||
** Mod info
|
||||
****/
|
||||
/// <summary>The mod's unique ID. If the mod has alternate/old IDs, they're listed in latest to newest order.</summary>
|
||||
public string[] ID { get; set; }
|
||||
|
||||
/// <summary>The mod's display name. If the mod has multiple names, the first one is the most canonical name.</summary>
|
||||
public string[] Name { get; set; }
|
||||
|
||||
/// <summary>The mod's author name. If the author has multiple names, the first one is the most canonical name.</summary>
|
||||
public string[] Author { get; set; }
|
||||
|
||||
/// <summary>The mod ID on Nexus.</summary>
|
||||
public int? NexusID { get; set; }
|
||||
|
||||
/// <summary>The mod ID in the Chucklefish mod repo.</summary>
|
||||
public int? ChucklefishID { get; set; }
|
||||
|
||||
/// <summary>The mod ID in the CurseForge mod repo.</summary>
|
||||
public int? CurseForgeID { get; set; }
|
||||
|
||||
/// <summary>The mod key in the CurseForge mod repo (used in mod page URLs).</summary>
|
||||
public string CurseForgeKey { get; set; }
|
||||
|
||||
/// <summary>The mod ID in the ModDrop mod repo.</summary>
|
||||
public int? ModDropID { get; set; }
|
||||
|
||||
/// <summary>The GitHub repository in the form 'owner/repo'.</summary>
|
||||
public string GitHubRepo { get; set; }
|
||||
|
||||
/// <summary>The URL to a non-GitHub source repo.</summary>
|
||||
public string CustomSourceUrl { get; set; }
|
||||
|
||||
/// <summary>The custom mod page URL (if applicable).</summary>
|
||||
public string CustomUrl { get; set; }
|
||||
|
||||
/// <summary>The name of the mod which loads this content pack, if applicable.</summary>
|
||||
public string ContentPackFor { get; set; }
|
||||
|
||||
/// <summary>The human-readable warnings for players about this mod.</summary>
|
||||
public string[] Warnings { get; set; }
|
||||
|
||||
/// <summary>The URL of the pull request which submits changes for an unofficial update to the author, if any.</summary>
|
||||
public string PullRequestUrl { get; set; }
|
||||
|
||||
/// <summary>Special notes intended for developers who maintain unofficial updates or submit pull requests. </summary>
|
||||
public string DevNote { get; set; }
|
||||
|
||||
/// <summary>The link anchor for the mod entry in the wiki compatibility list.</summary>
|
||||
public string Anchor { get; set; }
|
||||
|
||||
/****
|
||||
** Stable compatibility
|
||||
****/
|
||||
/// <summary>The compatibility status.</summary>
|
||||
public WikiCompatibilityStatus MainStatus { get; set; }
|
||||
|
||||
/// <summary>The human-readable summary of the compatibility status or workaround, without HTML formatting.</summary>
|
||||
public string MainSummary { get; set; }
|
||||
|
||||
/// <summary>The game or SMAPI version which broke this mod (if applicable).</summary>
|
||||
public string MainBrokeIn { get; set; }
|
||||
|
||||
/// <summary>The version of the latest unofficial update, if applicable.</summary>
|
||||
public string MainUnofficialVersion { get; set; }
|
||||
|
||||
/// <summary>The URL to the latest unofficial update, if applicable.</summary>
|
||||
public string MainUnofficialUrl { get; set; }
|
||||
|
||||
/****
|
||||
** Beta compatibility
|
||||
****/
|
||||
/// <summary>The compatibility status.</summary>
|
||||
public WikiCompatibilityStatus? BetaStatus { get; set; }
|
||||
|
||||
/// <summary>The human-readable summary of the compatibility status or workaround, without HTML formatting.</summary>
|
||||
public string BetaSummary { get; set; }
|
||||
|
||||
/// <summary>The game or SMAPI version which broke this mod (if applicable).</summary>
|
||||
public string BetaBrokeIn { get; set; }
|
||||
|
||||
/// <summary>The version of the latest unofficial update, if applicable.</summary>
|
||||
public string BetaUnofficialVersion { get; set; }
|
||||
|
||||
/// <summary>The URL to the latest unofficial update, if applicable.</summary>
|
||||
public string BetaUnofficialUrl { get; set; }
|
||||
|
||||
/****
|
||||
** Version maps
|
||||
****/
|
||||
/// <summary>Maps local versions to a semantic version for update checks.</summary>
|
||||
[BsonDictionaryOptions(Representation = DictionaryRepresentation.ArrayOfArrays)]
|
||||
public IDictionary<string, string> MapLocalVersions { get; set; }
|
||||
|
||||
/// <summary>Maps remote versions to a semantic version for update checks.</summary>
|
||||
[BsonDictionaryOptions(Representation = DictionaryRepresentation.ArrayOfArrays)]
|
||||
public IDictionary<string, string> MapRemoteVersions { get; set; }
|
||||
|
||||
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
public CachedWikiMod() { }
|
||||
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="mod">The mod data.</param>
|
||||
public CachedWikiMod(WikiModEntry mod)
|
||||
{
|
||||
// tracking
|
||||
this.LastUpdated = DateTimeOffset.UtcNow;
|
||||
|
||||
// mod info
|
||||
this.ID = mod.ID;
|
||||
this.Name = mod.Name;
|
||||
this.Author = mod.Author;
|
||||
this.NexusID = mod.NexusID;
|
||||
this.ChucklefishID = mod.ChucklefishID;
|
||||
this.CurseForgeID = mod.CurseForgeID;
|
||||
this.CurseForgeKey = mod.CurseForgeKey;
|
||||
this.ModDropID = mod.ModDropID;
|
||||
this.GitHubRepo = mod.GitHubRepo;
|
||||
this.CustomSourceUrl = mod.CustomSourceUrl;
|
||||
this.CustomUrl = mod.CustomUrl;
|
||||
this.ContentPackFor = mod.ContentPackFor;
|
||||
this.PullRequestUrl = mod.PullRequestUrl;
|
||||
this.Warnings = mod.Warnings;
|
||||
this.DevNote = mod.DevNote;
|
||||
this.Anchor = mod.Anchor;
|
||||
|
||||
// stable compatibility
|
||||
this.MainStatus = mod.Compatibility.Status;
|
||||
this.MainSummary = mod.Compatibility.Summary;
|
||||
this.MainBrokeIn = mod.Compatibility.BrokeIn;
|
||||
this.MainUnofficialVersion = mod.Compatibility.UnofficialVersion?.ToString();
|
||||
this.MainUnofficialUrl = mod.Compatibility.UnofficialUrl;
|
||||
|
||||
// beta compatibility
|
||||
this.BetaStatus = mod.BetaCompatibility?.Status;
|
||||
this.BetaSummary = mod.BetaCompatibility?.Summary;
|
||||
this.BetaBrokeIn = mod.BetaCompatibility?.BrokeIn;
|
||||
this.BetaUnofficialVersion = mod.BetaCompatibility?.UnofficialVersion?.ToString();
|
||||
this.BetaUnofficialUrl = mod.BetaCompatibility?.UnofficialUrl;
|
||||
|
||||
// version maps
|
||||
this.MapLocalVersions = mod.MapLocalVersions;
|
||||
this.MapRemoteVersions = mod.MapRemoteVersions;
|
||||
}
|
||||
|
||||
/// <summary>Reconstruct the original model.</summary>
|
||||
public WikiModEntry GetModel()
|
||||
{
|
||||
var mod = new WikiModEntry
|
||||
{
|
||||
ID = this.ID,
|
||||
Name = this.Name,
|
||||
Author = this.Author,
|
||||
NexusID = this.NexusID,
|
||||
ChucklefishID = this.ChucklefishID,
|
||||
CurseForgeID = this.CurseForgeID,
|
||||
CurseForgeKey = this.CurseForgeKey,
|
||||
ModDropID = this.ModDropID,
|
||||
GitHubRepo = this.GitHubRepo,
|
||||
CustomSourceUrl = this.CustomSourceUrl,
|
||||
CustomUrl = this.CustomUrl,
|
||||
ContentPackFor = this.ContentPackFor,
|
||||
Warnings = this.Warnings,
|
||||
PullRequestUrl = this.PullRequestUrl,
|
||||
DevNote = this.DevNote,
|
||||
Anchor = this.Anchor,
|
||||
|
||||
// stable compatibility
|
||||
Compatibility = new WikiCompatibilityInfo
|
||||
{
|
||||
Status = this.MainStatus,
|
||||
Summary = this.MainSummary,
|
||||
BrokeIn = this.MainBrokeIn,
|
||||
UnofficialVersion = this.MainUnofficialVersion != null ? new SemanticVersion(this.MainUnofficialVersion) : null,
|
||||
UnofficialUrl = this.MainUnofficialUrl
|
||||
},
|
||||
|
||||
// version maps
|
||||
MapLocalVersions = this.MapLocalVersions,
|
||||
MapRemoteVersions = this.MapRemoteVersions
|
||||
};
|
||||
|
||||
// beta compatibility
|
||||
if (this.BetaStatus != null)
|
||||
{
|
||||
mod.BetaCompatibility = new WikiCompatibilityInfo
|
||||
{
|
||||
Status = this.BetaStatus.Value,
|
||||
Summary = this.BetaSummary,
|
||||
BrokeIn = this.BetaBrokeIn,
|
||||
UnofficialVersion = this.BetaUnofficialVersion != null ? new SemanticVersion(this.BetaUnofficialVersion) : null,
|
||||
UnofficialUrl = this.BetaUnofficialUrl
|
||||
};
|
||||
}
|
||||
|
||||
return mod;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,11 +1,10 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq.Expressions;
|
||||
using StardewModdingAPI.Toolkit.Framework.Clients.Wiki;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework.Caching.Wiki
|
||||
{
|
||||
/// <summary>Encapsulates logic for accessing the wiki data cache.</summary>
|
||||
/// <summary>Manages cached wiki data.</summary>
|
||||
internal interface IWikiCacheRepository : ICacheRepository
|
||||
{
|
||||
/*********
|
||||
|
@ -13,18 +12,16 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki
|
|||
*********/
|
||||
/// <summary>Get the cached wiki metadata.</summary>
|
||||
/// <param name="metadata">The fetched metadata.</param>
|
||||
bool TryGetWikiMetadata(out CachedWikiMetadata metadata);
|
||||
bool TryGetWikiMetadata(out Cached<WikiMetadata> metadata);
|
||||
|
||||
/// <summary>Get the cached wiki mods.</summary>
|
||||
/// <param name="filter">A filter to apply, if any.</param>
|
||||
IEnumerable<CachedWikiMod> GetWikiMods(Expression<Func<CachedWikiMod, bool>> filter = null);
|
||||
IEnumerable<Cached<WikiModEntry>> GetWikiMods(Func<WikiModEntry, bool> filter = null);
|
||||
|
||||
/// <summary>Save data fetched from the wiki compatibility list.</summary>
|
||||
/// <param name="stableVersion">The current stable Stardew Valley version.</param>
|
||||
/// <param name="betaVersion">The current beta Stardew Valley version.</param>
|
||||
/// <param name="mods">The mod data.</param>
|
||||
/// <param name="cachedMetadata">The stored metadata record.</param>
|
||||
/// <param name="cachedMods">The stored mod records.</param>
|
||||
void SaveWikiData(string stableVersion, string betaVersion, IEnumerable<WikiModEntry> mods, out CachedWikiMetadata cachedMetadata, out CachedWikiMod[] cachedMods);
|
||||
void SaveWikiData(string stableVersion, string betaVersion, IEnumerable<WikiModEntry> mods);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using StardewModdingAPI.Toolkit.Framework.Clients.Wiki;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework.Caching.Wiki
|
||||
{
|
||||
/// <summary>Manages cached wiki data in-memory.</summary>
|
||||
internal class WikiCacheMemoryRepository : BaseCacheRepository, IWikiCacheRepository
|
||||
{
|
||||
/*********
|
||||
** Fields
|
||||
*********/
|
||||
/// <summary>The saved wiki metadata.</summary>
|
||||
private Cached<WikiMetadata> Metadata;
|
||||
|
||||
/// <summary>The cached wiki data.</summary>
|
||||
private Cached<WikiModEntry>[] Mods = new Cached<WikiModEntry>[0];
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Get the cached wiki metadata.</summary>
|
||||
/// <param name="metadata">The fetched metadata.</param>
|
||||
public bool TryGetWikiMetadata(out Cached<WikiMetadata> metadata)
|
||||
{
|
||||
metadata = this.Metadata;
|
||||
return metadata != null;
|
||||
}
|
||||
|
||||
/// <summary>Get the cached wiki mods.</summary>
|
||||
/// <param name="filter">A filter to apply, if any.</param>
|
||||
public IEnumerable<Cached<WikiModEntry>> GetWikiMods(Func<WikiModEntry, bool> filter = null)
|
||||
{
|
||||
foreach (var mod in this.Mods)
|
||||
{
|
||||
if (filter == null || filter(mod.Data))
|
||||
yield return mod;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Save data fetched from the wiki compatibility list.</summary>
|
||||
/// <param name="stableVersion">The current stable Stardew Valley version.</param>
|
||||
/// <param name="betaVersion">The current beta Stardew Valley version.</param>
|
||||
/// <param name="mods">The mod data.</param>
|
||||
public void SaveWikiData(string stableVersion, string betaVersion, IEnumerable<WikiModEntry> mods)
|
||||
{
|
||||
this.Metadata = new Cached<WikiMetadata>(new WikiMetadata(stableVersion, betaVersion));
|
||||
this.Mods = mods.Select(mod => new Cached<WikiModEntry>(mod)).ToArray();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,73 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using MongoDB.Driver;
|
||||
using StardewModdingAPI.Toolkit.Framework.Clients.Wiki;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework.Caching.Wiki
|
||||
{
|
||||
/// <summary>Encapsulates logic for accessing the wiki data cache.</summary>
|
||||
internal class WikiCacheRepository : BaseCacheRepository, IWikiCacheRepository
|
||||
{
|
||||
/*********
|
||||
** Fields
|
||||
*********/
|
||||
/// <summary>The collection for wiki metadata.</summary>
|
||||
private readonly IMongoCollection<CachedWikiMetadata> WikiMetadata;
|
||||
|
||||
/// <summary>The collection for wiki mod data.</summary>
|
||||
private readonly IMongoCollection<CachedWikiMod> WikiMods;
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="database">The authenticated MongoDB database.</param>
|
||||
public WikiCacheRepository(IMongoDatabase database)
|
||||
{
|
||||
// get collections
|
||||
this.WikiMetadata = database.GetCollection<CachedWikiMetadata>("wiki-metadata");
|
||||
this.WikiMods = database.GetCollection<CachedWikiMod>("wiki-mods");
|
||||
|
||||
// add indexes if needed
|
||||
this.WikiMods.Indexes.CreateOne(new CreateIndexModel<CachedWikiMod>(Builders<CachedWikiMod>.IndexKeys.Ascending(p => p.ID)));
|
||||
}
|
||||
|
||||
/// <summary>Get the cached wiki metadata.</summary>
|
||||
/// <param name="metadata">The fetched metadata.</param>
|
||||
public bool TryGetWikiMetadata(out CachedWikiMetadata metadata)
|
||||
{
|
||||
metadata = this.WikiMetadata.Find("{}").FirstOrDefault();
|
||||
return metadata != null;
|
||||
}
|
||||
|
||||
/// <summary>Get the cached wiki mods.</summary>
|
||||
/// <param name="filter">A filter to apply, if any.</param>
|
||||
public IEnumerable<CachedWikiMod> GetWikiMods(Expression<Func<CachedWikiMod, bool>> filter = null)
|
||||
{
|
||||
return filter != null
|
||||
? this.WikiMods.Find(filter).ToList()
|
||||
: this.WikiMods.Find("{}").ToList();
|
||||
}
|
||||
|
||||
/// <summary>Save data fetched from the wiki compatibility list.</summary>
|
||||
/// <param name="stableVersion">The current stable Stardew Valley version.</param>
|
||||
/// <param name="betaVersion">The current beta Stardew Valley version.</param>
|
||||
/// <param name="mods">The mod data.</param>
|
||||
/// <param name="cachedMetadata">The stored metadata record.</param>
|
||||
/// <param name="cachedMods">The stored mod records.</param>
|
||||
public void SaveWikiData(string stableVersion, string betaVersion, IEnumerable<WikiModEntry> mods, out CachedWikiMetadata cachedMetadata, out CachedWikiMod[] cachedMods)
|
||||
{
|
||||
cachedMetadata = new CachedWikiMetadata(stableVersion, betaVersion);
|
||||
cachedMods = mods.Select(mod => new CachedWikiMod(mod)).ToArray();
|
||||
|
||||
this.WikiMods.DeleteMany("{}");
|
||||
this.WikiMods.InsertMany(cachedMods);
|
||||
|
||||
this.WikiMetadata.DeleteMany("{}");
|
||||
this.WikiMetadata.InsertOne(cachedMetadata);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,22 +1,11 @@
|
|||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using MongoDB.Bson;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework.Caching.Wiki
|
||||
{
|
||||
/// <summary>The model for cached wiki metadata.</summary>
|
||||
internal class CachedWikiMetadata
|
||||
internal class WikiMetadata
|
||||
{
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>The internal MongoDB ID.</summary>
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Named per MongoDB conventions.")]
|
||||
public ObjectId _id { get; set; }
|
||||
|
||||
/// <summary>When the data was last updated.</summary>
|
||||
public DateTimeOffset LastUpdated { get; set; }
|
||||
|
||||
/// <summary>The current stable Stardew Valley version.</summary>
|
||||
public string StableVersion { get; set; }
|
||||
|
||||
|
@ -28,16 +17,15 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki
|
|||
** Public methods
|
||||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
public CachedWikiMetadata() { }
|
||||
public WikiMetadata() { }
|
||||
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="stableVersion">The current stable Stardew Valley version.</param>
|
||||
/// <param name="betaVersion">The current beta Stardew Valley version.</param>
|
||||
public CachedWikiMetadata(string stableVersion, string betaVersion)
|
||||
public WikiMetadata(string stableVersion, string betaVersion)
|
||||
{
|
||||
this.StableVersion = stableVersion;
|
||||
this.BetaVersion = betaVersion;
|
||||
this.LastUpdated = DateTimeOffset.UtcNow;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
{
|
||||
|
@ -19,6 +20,13 @@ namespace StardewModdingAPI.Web.Framework.Clients.Chucklefish
|
|||
private readonly IClient Client;
|
||||
|
||||
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>The unique key for the mod site.</summary>
|
||||
public ModSiteKey SiteKey => ModSiteKey.Chucklefish;
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
|
@ -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>
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
}
|
|
@ -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 { }
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
@ -20,6 +20,13 @@ namespace StardewModdingAPI.Web.Framework.Clients.CurseForge
|
|||
private readonly Regex VersionInNamePattern = new Regex(@"^(?:.+? | *)v?(\d+\.\d+(?:\.\d+)?(?:-.+?)?) *(?:\.(?:zip|rar|7z))?$", RegexOptions.Compiled);
|
||||
|
||||
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>The unique key for the mod site.</summary>
|
||||
public ModSiteKey SiteKey => ModSiteKey.CurseForge;
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
|
@ -31,60 +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))
|
||||
{
|
||||
if (invalidVersion == null)
|
||||
invalidVersion = raw;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// track latest version
|
||||
if (latest == null || version.IsNewerThan(latest))
|
||||
latest = version;
|
||||
downloads.Add(
|
||||
new GenericModDownload(name: file.DisplayName ?? file.FileName, description: null, version: this.GetRawVersion(file))
|
||||
);
|
||||
}
|
||||
|
||||
// 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.";
|
||||
}
|
||||
|
||||
// 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>
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
}
|
|
@ -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 { }
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
{
|
||||
|
@ -16,6 +17,13 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub
|
|||
private readonly IClient Client;
|
||||
|
||||
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>The unique key for the mod site.</summary>
|
||||
public ModSiteKey SiteKey => ModSiteKey.GitHub;
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
|
@ -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()
|
||||
{
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 mod site.</summary>
|
||||
internal interface IModSiteClient
|
||||
{
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>The unique key for the mod site.</summary>
|
||||
public ModSiteKey SiteKey { get; }
|
||||
|
||||
|
||||
/*********
|
||||
** Methods
|
||||
*********/
|
||||
/// <summary>Get update check info about a mod.</summary>
|
||||
/// <param name="id">The mod ID.</param>
|
||||
Task<IModPage> GetModData(string id);
|
||||
}
|
||||
}
|
|
@ -1,17 +1,7 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework.Clients.ModDrop
|
||||
{
|
||||
/// <summary>An HTTP client for fetching mod metadata from the ModDrop API.</summary>
|
||||
internal interface IModDropClient : IDisposable
|
||||
{
|
||||
/*********
|
||||
** Methods
|
||||
*********/
|
||||
/// <summary>Get metadata about a mod.</summary>
|
||||
/// <param name="id">The ModDrop mod ID.</param>
|
||||
/// <returns>Returns the mod info if found, else <c>null</c>.</returns>
|
||||
Task<ModDropMod> GetModAsync(long id);
|
||||
}
|
||||
internal interface IModDropClient : IDisposable, IModSiteClient { }
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Pathoschild.Http.Client;
|
||||
using StardewModdingAPI.Toolkit;
|
||||
using StardewModdingAPI.Toolkit.Framework.UpdateData;
|
||||
using StardewModdingAPI.Web.Framework.Clients.ModDrop.ResponseModels;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework.Clients.ModDrop
|
||||
|
@ -18,6 +19,13 @@ namespace StardewModdingAPI.Web.Framework.Clients.ModDrop
|
|||
private readonly string ModUrlFormat;
|
||||
|
||||
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>The unique key for the mod site.</summary>
|
||||
public ModSiteKey SiteKey => ModSiteKey.ModDrop;
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
|
@ -31,60 +39,45 @@ namespace StardewModdingAPI.Web.Framework.Clients.ModDrop
|
|||
this.ModUrlFormat = modUrlFormat;
|
||||
}
|
||||
|
||||
/// <summary>Get metadata about a mod.</summary>
|
||||
/// <param name="id">The ModDrop mod ID.</param>
|
||||
/// <returns>Returns the mod info if found, else <c>null</c>.</returns>
|
||||
public async Task<ModDropMod> 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)
|
||||
{
|
||||
var page = new GenericModPage(this.SiteKey, id);
|
||||
|
||||
if (!long.TryParse(id, out long parsedId))
|
||||
return page.SetError(RemoteModStatus.DoesNotExist, $"The value '{id}' isn't a valid ModDrop mod ID, must be an integer ID.");
|
||||
|
||||
// get raw data
|
||||
ModListModel response = await this.Client
|
||||
.PostAsync("")
|
||||
.WithBody(new
|
||||
{
|
||||
ModIDs = new[] { id },
|
||||
ModIDs = new[] { parsedId },
|
||||
Files = true,
|
||||
Mods = true
|
||||
})
|
||||
.As<ModListModel>();
|
||||
ModModel mod = response.Mods[id];
|
||||
ModModel mod = response.Mods[parsedId];
|
||||
if (mod.Mod?.Title == null || mod.Mod.ErrorCode.HasValue)
|
||||
return null;
|
||||
|
||||
// get latest versions
|
||||
ISemanticVersion latest = null;
|
||||
ISemanticVersion optional = null;
|
||||
// get files
|
||||
var downloads = new List<IModDownload>();
|
||||
foreach (FileDataModel file in mod.Files)
|
||||
{
|
||||
if (file.IsOld || file.IsDeleted || file.IsHidden)
|
||||
continue;
|
||||
|
||||
if (!SemanticVersion.TryParse(file.Version, out ISemanticVersion version))
|
||||
continue;
|
||||
|
||||
if (file.IsDefault)
|
||||
{
|
||||
if (latest == null || version.IsNewerThan(latest))
|
||||
latest = version;
|
||||
}
|
||||
else if (optional == null || version.IsNewerThan(optional))
|
||||
optional = version;
|
||||
downloads.Add(
|
||||
new GenericModDownload(file.Name, file.Description, file.Version)
|
||||
);
|
||||
}
|
||||
if (latest == null)
|
||||
{
|
||||
latest = optional;
|
||||
optional = null;
|
||||
}
|
||||
if (optional != null && latest.IsNewerThan(optional))
|
||||
optional = null;
|
||||
|
||||
// generate result
|
||||
return new ModDropMod
|
||||
{
|
||||
Name = mod.Mod?.Title,
|
||||
LatestDefaultVersion = latest,
|
||||
LatestOptionalVersion = optional,
|
||||
Url = string.Format(this.ModUrlFormat, id)
|
||||
};
|
||||
// return info
|
||||
string name = mod.Mod?.Title;
|
||||
string url = string.Format(this.ModUrlFormat, id);
|
||||
return page.SetInfo(name: name, version: null, url: url, downloads: downloads);
|
||||
}
|
||||
|
||||
/// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
namespace StardewModdingAPI.Web.Framework.Clients.ModDrop
|
||||
{
|
||||
/// <summary>Mod metadata from the ModDrop API.</summary>
|
||||
internal class ModDropMod
|
||||
{
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>The mod name.</summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>The latest default file version.</summary>
|
||||
public ISemanticVersion LatestDefaultVersion { get; set; }
|
||||
|
||||
/// <summary>The latest optional file version.</summary>
|
||||
public ISemanticVersion LatestOptionalVersion { get; set; }
|
||||
|
||||
/// <summary>The mod's web URL.</summary>
|
||||
public string Url { get; set; }
|
||||
}
|
||||
}
|
|
@ -1,8 +1,21 @@
|
|||
using Newtonsoft.Json;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework.Clients.ModDrop.ResponseModels
|
||||
{
|
||||
/// <summary>Metadata from the ModDrop API about a mod file.</summary>
|
||||
public class FileDataModel
|
||||
{
|
||||
/// <summary>The file title.</summary>
|
||||
[JsonProperty("title")]
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>The file description.</summary>
|
||||
[JsonProperty("desc")]
|
||||
public string Description { get; set; }
|
||||
|
||||
/// <summary>The file version.</summary>
|
||||
public string Version { get; set; }
|
||||
|
||||
/// <summary>Whether the file is deleted.</summary>
|
||||
public bool IsDeleted { get; set; }
|
||||
|
||||
|
@ -14,8 +27,5 @@ namespace StardewModdingAPI.Web.Framework.Clients.ModDrop.ResponseModels
|
|||
|
||||
/// <summary>Whether this is an archived file.</summary>
|
||||
public bool IsOld { get; set; }
|
||||
|
||||
/// <summary>The file version.</summary>
|
||||
public string Version { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,17 +1,7 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework.Clients.Nexus
|
||||
{
|
||||
/// <summary>An HTTP client for fetching mod metadata from Nexus Mods.</summary>
|
||||
internal interface INexusClient : IDisposable
|
||||
{
|
||||
/*********
|
||||
** Methods
|
||||
*********/
|
||||
/// <summary>Get metadata about a mod.</summary>
|
||||
/// <param name="id">The Nexus mod ID.</param>
|
||||
/// <returns>Returns the mod info if found, else <c>null</c>.</returns>
|
||||
Task<NexusMod> GetModAsync(uint id);
|
||||
}
|
||||
internal interface INexusClient : IModSiteClient, IDisposable { }
|
||||
}
|
||||
|
|
|
@ -7,6 +7,8 @@ using HtmlAgilityPack;
|
|||
using Pathoschild.FluentNexus.Models;
|
||||
using Pathoschild.Http.Client;
|
||||
using StardewModdingAPI.Toolkit;
|
||||
using StardewModdingAPI.Toolkit.Framework.UpdateData;
|
||||
using StardewModdingAPI.Web.Framework.Clients.Nexus.ResponseModels;
|
||||
using FluentNexusClient = Pathoschild.FluentNexus.NexusClient;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework.Clients.Nexus
|
||||
|
@ -30,6 +32,13 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus
|
|||
private readonly FluentNexusClient ApiClient;
|
||||
|
||||
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>The unique key for the mod site.</summary>
|
||||
public ModSiteKey SiteKey => ModSiteKey.Nexus;
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
|
@ -48,20 +57,32 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus
|
|||
this.ApiClient = new FluentNexusClient(apiKey, "SMAPI", apiAppVersion);
|
||||
}
|
||||
|
||||
/// <summary>Get metadata about a mod.</summary>
|
||||
/// <param name="id">The Nexus mod ID.</param>
|
||||
/// <returns>Returns the mod info if found, else <c>null</c>.</returns>
|
||||
public async Task<NexusMod> 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);
|
||||
|
||||
if (!uint.TryParse(id, out uint parsedId))
|
||||
return page.SetError(RemoteModStatus.DoesNotExist, $"The value '{id}' isn't a valid Nexus mod ID, must be an integer ID.");
|
||||
|
||||
// Fetch from the Nexus website when possible, since it has no rate limits. Mods with
|
||||
// adult content are hidden for anonymous users, so fall back to the API in that case.
|
||||
// Note that the API has very restrictive rate limits which means we can't just use it
|
||||
// for all cases.
|
||||
NexusMod mod = await this.GetModFromWebsiteAsync(id);
|
||||
NexusMod mod = await this.GetModFromWebsiteAsync(parsedId);
|
||||
if (mod?.Status == NexusModStatus.AdultContentForbidden)
|
||||
mod = await this.GetModFromApiAsync(id);
|
||||
mod = await this.GetModFromApiAsync(parsedId);
|
||||
|
||||
return mod;
|
||||
// page doesn't exist
|
||||
if (mod == null || mod.Status == NexusModStatus.Hidden || mod.Status == NexusModStatus.NotPublished)
|
||||
return page.SetError(RemoteModStatus.DoesNotExist, "Found no Nexus mod with this ID.");
|
||||
|
||||
// return info
|
||||
page.SetInfo(name: mod.Name, url: mod.Url, version: mod.Version, downloads: mod.Downloads);
|
||||
if (mod.Status != NexusModStatus.Ok)
|
||||
page.SetError(RemoteModStatus.TemporaryError, mod.Error);
|
||||
return page;
|
||||
}
|
||||
|
||||
/// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
|
||||
|
@ -115,37 +136,28 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus
|
|||
|
||||
// extract mod info
|
||||
string url = this.GetModUrl(id);
|
||||
string name = doc.DocumentNode.SelectSingleNode("//h1")?.InnerText.Trim();
|
||||
string name = doc.DocumentNode.SelectSingleNode("//div[@id='pagetitle']//h1")?.InnerText.Trim();
|
||||
string version = doc.DocumentNode.SelectSingleNode("//ul[contains(@class, 'stats')]//li[@class='stat-version']//div[@class='stat']")?.InnerText.Trim();
|
||||
SemanticVersion.TryParse(version, out ISemanticVersion parsedVersion);
|
||||
|
||||
// extract file versions
|
||||
List<string> rawVersions = new List<string>();
|
||||
// extract files
|
||||
var downloads = new List<IModDownload>();
|
||||
foreach (var fileSection in doc.DocumentNode.SelectNodes("//div[contains(@class, 'files-tabs')]"))
|
||||
{
|
||||
string sectionName = fileSection.Descendants("h2").First().InnerText;
|
||||
if (sectionName != "Main files" && sectionName != "Optional files")
|
||||
continue;
|
||||
|
||||
rawVersions.AddRange(
|
||||
from statBox in fileSection.Descendants().Where(p => p.HasClass("stat-version"))
|
||||
from versionStat in statBox.Descendants().Where(p => p.HasClass("stat"))
|
||||
select versionStat.InnerText.Trim()
|
||||
);
|
||||
}
|
||||
foreach (var container in fileSection.Descendants("dt"))
|
||||
{
|
||||
string fileName = container.GetDataAttribute("name").Value;
|
||||
string fileVersion = container.GetDataAttribute("version").Value;
|
||||
string description = container.SelectSingleNode("following-sibling::*[1][self::dd]//div").InnerText?.Trim(); // get text of next <dd> tag; derived from https://stackoverflow.com/a/25535623/262123
|
||||
|
||||
// choose latest file version
|
||||
ISemanticVersion latestFileVersion = null;
|
||||
foreach (string rawVersion in rawVersions)
|
||||
{
|
||||
if (!SemanticVersion.TryParse(rawVersion, out ISemanticVersion cur))
|
||||
continue;
|
||||
if (parsedVersion != null && !cur.IsNewerThan(parsedVersion))
|
||||
continue;
|
||||
if (latestFileVersion != null && !cur.IsNewerThan(latestFileVersion))
|
||||
continue;
|
||||
|
||||
latestFileVersion = cur;
|
||||
downloads.Add(
|
||||
new GenericModDownload(fileName, description, fileVersion)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// yield info
|
||||
|
@ -153,8 +165,8 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus
|
|||
{
|
||||
Name = name,
|
||||
Version = parsedVersion?.ToString() ?? version,
|
||||
LatestFileVersion = latestFileVersion,
|
||||
Url = url
|
||||
Url = url,
|
||||
Downloads = downloads.ToArray()
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -167,29 +179,15 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus
|
|||
Mod mod = await this.ApiClient.Mods.GetMod("stardewvalley", (int)id);
|
||||
ModFileList files = await this.ApiClient.ModFiles.GetModFiles("stardewvalley", (int)id, FileCategory.Main, FileCategory.Optional);
|
||||
|
||||
// get versions
|
||||
if (!SemanticVersion.TryParse(mod.Version, out ISemanticVersion mainVersion))
|
||||
mainVersion = null;
|
||||
ISemanticVersion latestFileVersion = null;
|
||||
foreach (string rawVersion in files.Files.Select(p => p.FileVersion))
|
||||
{
|
||||
if (!SemanticVersion.TryParse(rawVersion, out ISemanticVersion cur))
|
||||
continue;
|
||||
if (mainVersion != null && !cur.IsNewerThan(mainVersion))
|
||||
continue;
|
||||
if (latestFileVersion != null && !cur.IsNewerThan(latestFileVersion))
|
||||
continue;
|
||||
|
||||
latestFileVersion = cur;
|
||||
}
|
||||
|
||||
// yield info
|
||||
return new NexusMod
|
||||
{
|
||||
Name = mod.Name,
|
||||
Version = SemanticVersion.TryParse(mod.Version, out ISemanticVersion version) ? version?.ToString() : mod.Version,
|
||||
LatestFileVersion = latestFileVersion,
|
||||
Url = this.GetModUrl(id)
|
||||
Url = this.GetModUrl(id),
|
||||
Downloads = files.Files
|
||||
.Select(file => (IModDownload)new GenericModDownload(file.Name, null, file.FileVersion))
|
||||
.ToArray()
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
using Newtonsoft.Json;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework.Clients.Nexus
|
||||
namespace StardewModdingAPI.Web.Framework.Clients.Nexus.ResponseModels
|
||||
{
|
||||
/// <summary>Mod metadata from Nexus Mods.</summary>
|
||||
internal class NexusMod
|
||||
|
@ -14,9 +14,6 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus
|
|||
/// <summary>The mod's semantic version number.</summary>
|
||||
public string Version { get; set; }
|
||||
|
||||
/// <summary>The latest file version.</summary>
|
||||
public ISemanticVersion LatestFileVersion { get; set; }
|
||||
|
||||
/// <summary>The mod's web URL.</summary>
|
||||
[JsonProperty("mod_page_uri")]
|
||||
public string Url { get; set; }
|
||||
|
@ -25,7 +22,11 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus
|
|||
[JsonIgnore]
|
||||
public NexusModStatus Status { get; set; } = NexusModStatus.Ok;
|
||||
|
||||
/// <summary>A user-friendly error which indicates why fetching the mod info failed (if applicable).</summary>
|
||||
/// <summary>The files available to download.</summary>
|
||||
[JsonIgnore]
|
||||
public IModDownload[] Downloads { get; set; }
|
||||
|
||||
/// <summary>A custom user-friendly error which indicates why fetching the mod info failed (if applicable).</summary>
|
||||
[JsonIgnore]
|
||||
public string Error { get; set; }
|
||||
}
|
|
@ -69,7 +69,7 @@ namespace StardewModdingAPI.Web.Framework.Compression
|
|||
return rawText;
|
||||
|
||||
// decompress
|
||||
using (MemoryStream memoryStream = new MemoryStream())
|
||||
using MemoryStream memoryStream = new MemoryStream();
|
||||
{
|
||||
// read length prefix
|
||||
int dataLength = BitConverter.ToInt32(zipBuffer, 0);
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
namespace StardewModdingAPI.Web.Framework.ConfigModels
|
||||
{
|
||||
/// <summary>The config settings for mod compatibility list.</summary>
|
||||
internal class MongoDbConfig
|
||||
{
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>The MongoDB connection string.</summary>
|
||||
public string ConnectionString { get; set; }
|
||||
|
||||
/// <summary>The database name.</summary>
|
||||
public string Database { get; set; }
|
||||
|
||||
|
||||
/*********
|
||||
** Public method
|
||||
*********/
|
||||
/// <summary>Get whether a MongoDB instance is configured.</summary>
|
||||
public bool IsConfigured()
|
||||
{
|
||||
return !string.IsNullOrWhiteSpace(this.ConnectionString);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,14 +1,24 @@
|
|||
using System;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.AspNetCore.Html;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Razor;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework
|
||||
{
|
||||
/// <summary>Provides extensions on ASP.NET Core types.</summary>
|
||||
public static class Extensions
|
||||
{
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/****
|
||||
** View helpers
|
||||
****/
|
||||
/// <summary>Get a URL with the absolute path for an action method. Unlike <see cref="IUrlHelper.Action"/>, only the specified <paramref name="values"/> are added to the URL without merging values from the current HTTP request.</summary>
|
||||
/// <param name="helper">The URL helper to extend.</param>
|
||||
/// <param name="action">The name of the action method.</param>
|
||||
|
@ -18,6 +28,7 @@ namespace StardewModdingAPI.Web.Framework
|
|||
/// <returns>The generated URL.</returns>
|
||||
public static string PlainAction(this IUrlHelper helper, [AspMvcAction] string action, [AspMvcController] string controller, object values = null, bool absoluteUrl = false)
|
||||
{
|
||||
// get route values
|
||||
RouteValueDictionary valuesDict = new RouteValueDictionary(values);
|
||||
foreach (var value in helper.ActionContext.RouteData.Values)
|
||||
{
|
||||
|
@ -25,14 +36,31 @@ namespace StardewModdingAPI.Web.Framework
|
|||
valuesDict[value.Key] = null; // explicitly remove it from the URL
|
||||
}
|
||||
|
||||
// get relative URL
|
||||
string url = helper.Action(action, controller, valuesDict);
|
||||
if (url == null && action.EndsWith("Async"))
|
||||
url = helper.Action(action[..^"Async".Length], controller, valuesDict);
|
||||
|
||||
// get absolute URL
|
||||
if (absoluteUrl)
|
||||
{
|
||||
HttpRequest request = helper.ActionContext.HttpContext.Request;
|
||||
Uri baseUri = new Uri($"{request.Scheme}://{request.Host}");
|
||||
url = new Uri(baseUri, url).ToString();
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
/// <summary>Get a serialized JSON representation of the value.</summary>
|
||||
/// <param name="page">The page to extend.</param>
|
||||
/// <param name="value">The value to serialize.</param>
|
||||
/// <returns>The serialized JSON.</returns>
|
||||
/// <remarks>This bypasses unnecessary validation (e.g. not allowing null values) in <see cref="IJsonHelper.Serialize"/>.</remarks>
|
||||
public static IHtmlContent ForJson(this RazorPageBase page, object value)
|
||||
{
|
||||
string json = JsonConvert.SerializeObject(value);
|
||||
return new HtmlString(json);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
namespace StardewModdingAPI.Web.Framework
|
||||
{
|
||||
/// <summary>Generic metadata about a file download on a mod page.</summary>
|
||||
internal interface IModDownload
|
||||
{
|
||||
/// <summary>The download's display name.</summary>
|
||||
string Name { get; }
|
||||
|
||||
/// <summary>The download's description.</summary>
|
||||
string Description { get; }
|
||||
|
||||
/// <summary>The download's file version.</summary>
|
||||
string Version { get; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
using System.Collections.Generic;
|
||||
using StardewModdingAPI.Toolkit.Framework.UpdateData;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework
|
||||
{
|
||||
/// <summary>Generic metadata about a mod page.</summary>
|
||||
internal interface IModPage
|
||||
{
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>The mod site containing the mod.</summary>
|
||||
ModSiteKey Site { get; }
|
||||
|
||||
/// <summary>The mod's unique ID within the site.</summary>
|
||||
string Id { get; }
|
||||
|
||||
/// <summary>The mod name.</summary>
|
||||
string Name { get; }
|
||||
|
||||
/// <summary>The mod's semantic version number.</summary>
|
||||
string Version { get; }
|
||||
|
||||
/// <summary>The mod's web URL.</summary>
|
||||
string Url { get; }
|
||||
|
||||
/// <summary>The mod downloads.</summary>
|
||||
IModDownload[] Downloads { get; }
|
||||
|
||||
/// <summary>The mod page status.</summary>
|
||||
RemoteModStatus Status { get; }
|
||||
|
||||
/// <summary>A user-friendly error which indicates why fetching the mod info failed (if applicable).</summary>
|
||||
string Error { get; }
|
||||
|
||||
|
||||
/*********
|
||||
** Methods
|
||||
*********/
|
||||
/// <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>
|
||||
IModPage SetInfo(string name, string version, string url, IEnumerable<IModDownload> downloads);
|
||||
|
||||
/// <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>
|
||||
IModPage SetError(RemoteModStatus status, string error);
|
||||
}
|
||||
}
|
|
@ -45,7 +45,7 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
|
|||
private readonly Regex ModUpdateListEntryPattern = new Regex(@"^ (?<name>.+?) (?<version>[^\s]+): (?<link>.+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
/// <summary>A regex pattern matching SMAPI's update line.</summary>
|
||||
private readonly Regex SMAPIUpdatePattern = new Regex(@"^You can update SMAPI to (?<version>[^\s]+): (?<link>.+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
private readonly Regex SmapiUpdatePattern = new Regex(@"^You can update SMAPI to (?<version>[^\s]+): (?<link>.+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
|
||||
/*********
|
||||
|
@ -181,9 +181,9 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
|
|||
message.Section = LogSection.ModUpdateList;
|
||||
}
|
||||
|
||||
else if (message.Level == LogLevel.Alert && this.SMAPIUpdatePattern.IsMatch(message.Text))
|
||||
else if (message.Level == LogLevel.Alert && this.SmapiUpdatePattern.IsMatch(message.Text))
|
||||
{
|
||||
Match match = this.SMAPIUpdatePattern.Match(message.Text);
|
||||
Match match = this.SmapiUpdatePattern.Match(message.Text);
|
||||
string version = match.Groups["version"].Value;
|
||||
string link = match.Groups["link"].Value;
|
||||
smapiMod.UpdateVersion = version;
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
namespace StardewModdingAPI.Web.Framework.ModRepositories
|
||||
using StardewModdingAPI.Web.Framework.Clients;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework
|
||||
{
|
||||
/// <summary>Generic metadata about a mod.</summary>
|
||||
internal class ModInfoModel
|
||||
|
@ -10,20 +12,14 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
|
|||
public string Name { get; set; }
|
||||
|
||||
/// <summary>The mod's latest version.</summary>
|
||||
public string Version { get; set; }
|
||||
public ISemanticVersion Version { get; set; }
|
||||
|
||||
/// <summary>The mod's latest optional or prerelease version, if newer than <see cref="Version"/>.</summary>
|
||||
public string PreviewVersion { get; set; }
|
||||
public ISemanticVersion PreviewVersion { get; set; }
|
||||
|
||||
/// <summary>The mod's web URL.</summary>
|
||||
public string Url { get; set; }
|
||||
|
||||
/// <summary>The license URL, if available.</summary>
|
||||
public string LicenseUrl { get; set; }
|
||||
|
||||
/// <summary>The license name, if available.</summary>
|
||||
public string LicenseName { get; set; }
|
||||
|
||||
/// <summary>The mod availability status on the remote site.</summary>
|
||||
public RemoteModStatus Status { get; set; } = RemoteModStatus.Ok;
|
||||
|
||||
|
@ -42,7 +38,7 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
|
|||
/// <param name="version">The semantic version for the mod's latest release.</param>
|
||||
/// <param name="previewVersion">The semantic version for the mod's latest preview release, if available and different from <see cref="Version"/>.</param>
|
||||
/// <param name="url">The mod's web URL.</param>
|
||||
public ModInfoModel(string name, string version, string url, string previewVersion = null)
|
||||
public ModInfoModel(string name, ISemanticVersion version, string url, ISemanticVersion previewVersion = null)
|
||||
{
|
||||
this
|
||||
.SetBasicInfo(name, url)
|
||||
|
@ -63,7 +59,7 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
|
|||
/// <summary>Set the mod version info.</summary>
|
||||
/// <param name="version">The semantic version for the mod's latest release.</param>
|
||||
/// <param name="previewVersion">The semantic version for the mod's latest preview release, if available and different from <see cref="Version"/>.</param>
|
||||
public ModInfoModel SetVersions(string version, string previewVersion = null)
|
||||
public ModInfoModel SetVersions(ISemanticVersion version, ISemanticVersion previewVersion = null)
|
||||
{
|
||||
this.Version = version;
|
||||
this.PreviewVersion = previewVersion;
|
||||
|
@ -71,17 +67,6 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
|
|||
return this;
|
||||
}
|
||||
|
||||
/// <summary>Set the license info, if available.</summary>
|
||||
/// <param name="url">The license URL.</param>
|
||||
/// <param name="name">The license name.</param>
|
||||
public ModInfoModel SetLicense(string url, string name)
|
||||
{
|
||||
this.LicenseUrl = url;
|
||||
this.LicenseName = name;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>Set a mod error.</summary>
|
||||
/// <param name="status">The mod availability status on the remote site.</param>
|
||||
/// <param name="error">The error message indicating why the mod is invalid (if applicable).</param>
|
|
@ -1,51 +0,0 @@
|
|||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using StardewModdingAPI.Toolkit.Framework.UpdateData;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework.ModRepositories
|
||||
{
|
||||
internal abstract class RepositoryBase : IModRepository
|
||||
{
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>The unique key for this vendor.</summary>
|
||||
public ModRepositoryKey VendorKey { get; }
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
|
||||
public abstract void Dispose();
|
||||
|
||||
/// <summary>Get metadata about a mod in the repository.</summary>
|
||||
/// <param name="id">The mod ID in this repository.</param>
|
||||
public abstract Task<ModInfoModel> GetModInfoAsync(string id);
|
||||
|
||||
|
||||
/*********
|
||||
** Protected methods
|
||||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="vendorKey">The unique key for this vendor.</param>
|
||||
protected RepositoryBase(ModRepositoryKey vendorKey)
|
||||
{
|
||||
this.VendorKey = vendorKey;
|
||||
}
|
||||
|
||||
/// <summary>Normalize a version string.</summary>
|
||||
/// <param name="version">The version to normalize.</param>
|
||||
protected 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using StardewModdingAPI.Toolkit.Framework.UpdateData;
|
||||
using StardewModdingAPI.Web.Framework.Clients.Chucklefish;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework.ModRepositories
|
||||
{
|
||||
/// <summary>An HTTP client for fetching mod metadata from the Chucklefish mod site.</summary>
|
||||
internal class ChucklefishRepository : RepositoryBase
|
||||
{
|
||||
/*********
|
||||
** Fields
|
||||
*********/
|
||||
/// <summary>The underlying HTTP client.</summary>
|
||||
private readonly IChucklefishClient Client;
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="client">The underlying HTTP client.</param>
|
||||
public ChucklefishRepository(IChucklefishClient client)
|
||||
: base(ModRepositoryKey.Chucklefish)
|
||||
{
|
||||
this.Client = client;
|
||||
}
|
||||
|
||||
/// <summary>Get metadata about a mod in the repository.</summary>
|
||||
/// <param name="id">The mod ID in this repository.</param>
|
||||
public override async Task<ModInfoModel> GetModInfoAsync(string id)
|
||||
{
|
||||
// validate ID format
|
||||
if (!uint.TryParse(id, out uint realID))
|
||||
return new ModInfoModel().SetError(RemoteModStatus.DoesNotExist, $"The value '{id}' isn't a valid Chucklefish mod ID, must be an integer ID.");
|
||||
|
||||
// fetch info
|
||||
try
|
||||
{
|
||||
var mod = await this.Client.GetModAsync(realID);
|
||||
return mod != null
|
||||
? new ModInfoModel(name: mod.Name, version: this.NormalizeVersion(mod.Version), url: mod.Url)
|
||||
: new ModInfoModel().SetError(RemoteModStatus.DoesNotExist, "Found no Chucklefish mod with this ID.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ModInfoModel().SetError(RemoteModStatus.TemporaryError, ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
|
||||
public override void Dispose()
|
||||
{
|
||||
this.Client.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,63 +0,0 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using StardewModdingAPI.Toolkit.Framework.UpdateData;
|
||||
using StardewModdingAPI.Web.Framework.Clients.CurseForge;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework.ModRepositories
|
||||
{
|
||||
/// <summary>An HTTP client for fetching mod metadata from CurseForge.</summary>
|
||||
internal class CurseForgeRepository : RepositoryBase
|
||||
{
|
||||
/*********
|
||||
** Fields
|
||||
*********/
|
||||
/// <summary>The underlying CurseForge API client.</summary>
|
||||
private readonly ICurseForgeClient Client;
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="client">The underlying CurseForge API client.</param>
|
||||
public CurseForgeRepository(ICurseForgeClient client)
|
||||
: base(ModRepositoryKey.CurseForge)
|
||||
{
|
||||
this.Client = client;
|
||||
}
|
||||
|
||||
/// <summary>Get metadata about a mod in the repository.</summary>
|
||||
/// <param name="id">The mod ID in this repository.</param>
|
||||
public override async Task<ModInfoModel> GetModInfoAsync(string id)
|
||||
{
|
||||
// validate ID format
|
||||
if (!uint.TryParse(id, out uint curseID))
|
||||
return new ModInfoModel().SetError(RemoteModStatus.DoesNotExist, $"The value '{id}' isn't a valid CurseForge mod ID, must be an integer ID.");
|
||||
|
||||
// fetch info
|
||||
try
|
||||
{
|
||||
CurseForgeMod mod = await this.Client.GetModAsync(curseID);
|
||||
if (mod == null)
|
||||
return new ModInfoModel().SetError(RemoteModStatus.DoesNotExist, "Found no CurseForge mod with this ID.");
|
||||
if (mod.Error != null)
|
||||
{
|
||||
RemoteModStatus remoteStatus = RemoteModStatus.InvalidData;
|
||||
return new ModInfoModel().SetError(remoteStatus, mod.Error);
|
||||
}
|
||||
|
||||
return new ModInfoModel(name: mod.Name, version: this.NormalizeVersion(mod.LatestVersion), url: mod.Url);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ModInfoModel().SetError(RemoteModStatus.TemporaryError, ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
|
||||
public override void Dispose()
|
||||
{
|
||||
this.Client.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,82 +0,0 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using StardewModdingAPI.Toolkit.Framework.UpdateData;
|
||||
using StardewModdingAPI.Web.Framework.Clients.GitHub;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework.ModRepositories
|
||||
{
|
||||
/// <summary>An HTTP client for fetching mod metadata from GitHub project releases.</summary>
|
||||
internal class GitHubRepository : RepositoryBase
|
||||
{
|
||||
/*********
|
||||
** Fields
|
||||
*********/
|
||||
/// <summary>The underlying GitHub API client.</summary>
|
||||
private readonly IGitHubClient Client;
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="client">The underlying GitHub API client.</param>
|
||||
public GitHubRepository(IGitHubClient client)
|
||||
: base(ModRepositoryKey.GitHub)
|
||||
{
|
||||
this.Client = client;
|
||||
}
|
||||
|
||||
/// <summary>Get metadata about a mod in the repository.</summary>
|
||||
/// <param name="id">The mod ID in this repository.</param>
|
||||
public override async Task<ModInfoModel> GetModInfoAsync(string id)
|
||||
{
|
||||
ModInfoModel result = new ModInfoModel().SetBasicInfo(id, $"https://github.com/{id}/releases");
|
||||
|
||||
// validate ID format
|
||||
if (!id.Contains("/") || id.IndexOf("/", StringComparison.InvariantCultureIgnoreCase) != id.LastIndexOf("/", StringComparison.InvariantCultureIgnoreCase))
|
||||
return result.SetError(RemoteModStatus.DoesNotExist, $"The value '{id}' isn't a valid GitHub mod ID, must be a username and project name like 'Pathoschild/LookupAnything'.");
|
||||
|
||||
// fetch info
|
||||
try
|
||||
{
|
||||
// fetch repo info
|
||||
GitRepo repository = await this.Client.GetRepositoryAsync(id);
|
||||
if (repository == null)
|
||||
return result.SetError(RemoteModStatus.DoesNotExist, "Found no GitHub repository for this ID.");
|
||||
result
|
||||
.SetBasicInfo(repository.FullName, $"{repository.WebUrl}/releases")
|
||||
.SetLicense(url: repository.License?.Url, name: repository.License?.SpdxId ?? repository.License?.Name);
|
||||
|
||||
// get latest release (whether preview or stable)
|
||||
GitRelease latest = await this.Client.GetLatestReleaseAsync(id, includePrerelease: true);
|
||||
if (latest == null)
|
||||
return result.SetError(RemoteModStatus.DoesNotExist, "Found no GitHub release for this ID.");
|
||||
|
||||
// split stable/prerelease if applicable
|
||||
GitRelease preview = null;
|
||||
if (latest.IsPrerelease)
|
||||
{
|
||||
GitRelease release = await this.Client.GetLatestReleaseAsync(id, includePrerelease: false);
|
||||
if (release != null)
|
||||
{
|
||||
preview = latest;
|
||||
latest = release;
|
||||
}
|
||||
}
|
||||
|
||||
// return data
|
||||
return result.SetVersions(version: this.NormalizeVersion(latest.Tag), previewVersion: this.NormalizeVersion(preview?.Tag));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return result.SetError(RemoteModStatus.TemporaryError, ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
|
||||
public override void Dispose()
|
||||
{
|
||||
this.Client.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using StardewModdingAPI.Toolkit.Framework.UpdateData;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework.ModRepositories
|
||||
{
|
||||
/// <summary>A repository which provides mod metadata.</summary>
|
||||
internal interface IModRepository : IDisposable
|
||||
{
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>The unique key for this vendor.</summary>
|
||||
ModRepositoryKey VendorKey { get; }
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Get metadata about a mod in the repository.</summary>
|
||||
/// <param name="id">The mod ID in this repository.</param>
|
||||
Task<ModInfoModel> GetModInfoAsync(string id);
|
||||
}
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using StardewModdingAPI.Toolkit.Framework.UpdateData;
|
||||
using StardewModdingAPI.Web.Framework.Clients.ModDrop;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework.ModRepositories
|
||||
{
|
||||
/// <summary>An HTTP client for fetching mod metadata from the ModDrop API.</summary>
|
||||
internal class ModDropRepository : RepositoryBase
|
||||
{
|
||||
/*********
|
||||
** Fields
|
||||
*********/
|
||||
/// <summary>The underlying ModDrop API client.</summary>
|
||||
private readonly IModDropClient Client;
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="client">The underlying Nexus Mods API client.</param>
|
||||
public ModDropRepository(IModDropClient client)
|
||||
: base(ModRepositoryKey.ModDrop)
|
||||
{
|
||||
this.Client = client;
|
||||
}
|
||||
|
||||
/// <summary>Get metadata about a mod in the repository.</summary>
|
||||
/// <param name="id">The mod ID in this repository.</param>
|
||||
public override async Task<ModInfoModel> GetModInfoAsync(string id)
|
||||
{
|
||||
// validate ID format
|
||||
if (!long.TryParse(id, out long modDropID))
|
||||
return new ModInfoModel().SetError(RemoteModStatus.DoesNotExist, $"The value '{id}' isn't a valid ModDrop mod ID, must be an integer ID.");
|
||||
|
||||
// fetch info
|
||||
try
|
||||
{
|
||||
ModDropMod mod = await this.Client.GetModAsync(modDropID);
|
||||
return mod != null
|
||||
? new ModInfoModel(name: mod.Name, version: mod.LatestDefaultVersion?.ToString(), previewVersion: mod.LatestOptionalVersion?.ToString(), url: mod.Url)
|
||||
: new ModInfoModel().SetError(RemoteModStatus.DoesNotExist, "Found no ModDrop mod with this ID.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ModInfoModel().SetError(RemoteModStatus.TemporaryError, ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
|
||||
public override void Dispose()
|
||||
{
|
||||
this.Client.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using StardewModdingAPI.Toolkit.Framework.UpdateData;
|
||||
using StardewModdingAPI.Web.Framework.Clients.Nexus;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework.ModRepositories
|
||||
{
|
||||
/// <summary>An HTTP client for fetching mod metadata from Nexus Mods.</summary>
|
||||
internal class NexusRepository : RepositoryBase
|
||||
{
|
||||
/*********
|
||||
** Fields
|
||||
*********/
|
||||
/// <summary>The underlying Nexus Mods API client.</summary>
|
||||
private readonly INexusClient Client;
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="client">The underlying Nexus Mods API client.</param>
|
||||
public NexusRepository(INexusClient client)
|
||||
: base(ModRepositoryKey.Nexus)
|
||||
{
|
||||
this.Client = client;
|
||||
}
|
||||
|
||||
/// <summary>Get metadata about a mod in the repository.</summary>
|
||||
/// <param name="id">The mod ID in this repository.</param>
|
||||
public override async Task<ModInfoModel> GetModInfoAsync(string id)
|
||||
{
|
||||
// validate ID format
|
||||
if (!uint.TryParse(id, out uint nexusID))
|
||||
return new ModInfoModel().SetError(RemoteModStatus.DoesNotExist, $"The value '{id}' isn't a valid Nexus mod ID, must be an integer ID.");
|
||||
|
||||
// fetch info
|
||||
try
|
||||
{
|
||||
NexusMod mod = await this.Client.GetModAsync(nexusID);
|
||||
if (mod == null)
|
||||
return new ModInfoModel().SetError(RemoteModStatus.DoesNotExist, "Found no Nexus mod with this ID.");
|
||||
if (mod.Error != null)
|
||||
{
|
||||
RemoteModStatus remoteStatus = mod.Status == NexusModStatus.Hidden || mod.Status == NexusModStatus.NotPublished
|
||||
? RemoteModStatus.DoesNotExist
|
||||
: RemoteModStatus.TemporaryError;
|
||||
return new ModInfoModel().SetError(remoteStatus, mod.Error);
|
||||
}
|
||||
|
||||
return new ModInfoModel(name: mod.Name, version: this.NormalizeVersion(mod.Version), previewVersion: mod.LatestFileVersion?.ToString(), url: mod.Url);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ModInfoModel().SetError(RemoteModStatus.TemporaryError, ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
|
||||
public override void Dispose()
|
||||
{
|
||||
this.Client.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
using System;
|
||||
using System.Net;
|
||||
using Microsoft.AspNetCore.Rewrite;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework.RedirectRules
|
||||
{
|
||||
/// <summary>Redirect hostnames to a URL if they match a condition.</summary>
|
||||
internal class RedirectHostsToUrlsRule : RedirectMatchRule
|
||||
{
|
||||
/*********
|
||||
** Fields
|
||||
*********/
|
||||
/// <summary>Maps a lowercase hostname to the resulting redirect URL.</summary>
|
||||
private readonly Func<string, string> Map;
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="statusCode">The status code to use for redirects.</param>
|
||||
/// <param name="map">Hostnames mapped to the resulting redirect URL.</param>
|
||||
public RedirectHostsToUrlsRule(HttpStatusCode statusCode, Func<string, string> map)
|
||||
{
|
||||
this.StatusCode = statusCode;
|
||||
this.Map = map ?? throw new ArgumentNullException(nameof(map));
|
||||
}
|
||||
|
||||
|
||||
/*********
|
||||
** Private methods
|
||||
*********/
|
||||
/// <summary>Get the new redirect URL.</summary>
|
||||
/// <param name="context">The rewrite context.</param>
|
||||
/// <returns>Returns the redirect URL, or <c>null</c> if the redirect doesn't apply.</returns>
|
||||
protected override string GetNewUrl(RewriteContext context)
|
||||
{
|
||||
// get requested host
|
||||
string host = context.HttpContext.Request.Host.Host;
|
||||
if (host == null)
|
||||
return null;
|
||||
|
||||
// get new host
|
||||
host = this.Map(host);
|
||||
if (host == null)
|
||||
return null;
|
||||
|
||||
// rewrite URL
|
||||
UriBuilder uri = this.GetUrl(context.HttpContext.Request);
|
||||
uri.Host = host;
|
||||
return uri.ToString();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
using System;
|
||||
using System.Net;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Rewrite;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework.RedirectRules
|
||||
{
|
||||
/// <summary>Redirect matching requests to a URL.</summary>
|
||||
internal abstract class RedirectMatchRule : IRule
|
||||
{
|
||||
/*********
|
||||
** Fields
|
||||
*********/
|
||||
/// <summary>The status code to use for redirects.</summary>
|
||||
protected HttpStatusCode StatusCode { get; set; } = HttpStatusCode.Redirect;
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Applies the rule. Implementations of ApplyRule should set the value for <see cref="RewriteContext.Result" /> (defaults to RuleResult.ContinueRules).</summary>
|
||||
/// <param name="context">The rewrite context.</param>
|
||||
public void ApplyRule(RewriteContext context)
|
||||
{
|
||||
string newUrl = this.GetNewUrl(context);
|
||||
if (newUrl == null)
|
||||
return;
|
||||
|
||||
HttpResponse response = context.HttpContext.Response;
|
||||
response.StatusCode = (int)HttpStatusCode.Redirect;
|
||||
response.Headers["Location"] = newUrl;
|
||||
context.Result = RuleResult.EndResponse;
|
||||
}
|
||||
|
||||
|
||||
/*********
|
||||
** Protected methods
|
||||
*********/
|
||||
/// <summary>Get the new redirect URL.</summary>
|
||||
/// <param name="context">The rewrite context.</param>
|
||||
/// <returns>Returns the redirect URL, or <c>null</c> if the redirect doesn't apply.</returns>
|
||||
protected abstract string GetNewUrl(RewriteContext context);
|
||||
|
||||
/// <summary>Get the full request URL.</summary>
|
||||
/// <param name="request">The request.</param>
|
||||
protected UriBuilder GetUrl(HttpRequest request)
|
||||
{
|
||||
return new UriBuilder
|
||||
{
|
||||
Scheme = request.Scheme,
|
||||
Host = request.Host.Host,
|
||||
Port = request.Host.Port ?? -1,
|
||||
Path = request.PathBase + request.Path,
|
||||
Query = request.QueryString.Value
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.AspNetCore.Rewrite;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework.RedirectRules
|
||||
{
|
||||
/// <summary>Redirect paths to URLs if they match a condition.</summary>
|
||||
internal class RedirectPathsToUrlsRule : RedirectMatchRule
|
||||
{
|
||||
/*********
|
||||
** Fields
|
||||
*********/
|
||||
/// <summary>Regex patterns matching the current URL mapped to the resulting redirect URL.</summary>
|
||||
private readonly IDictionary<Regex, string> Map;
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="map">Regex patterns matching the current URL mapped to the resulting redirect URL.</param>
|
||||
public RedirectPathsToUrlsRule(IDictionary<string, string> map)
|
||||
{
|
||||
this.StatusCode = HttpStatusCode.RedirectKeepVerb;
|
||||
this.Map = map.ToDictionary(
|
||||
p => new Regex(p.Key, RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||
p => p.Value
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/*********
|
||||
** Protected methods
|
||||
*********/
|
||||
/// <summary>Get the new redirect URL.</summary>
|
||||
/// <param name="context">The rewrite context.</param>
|
||||
/// <returns>Returns the redirect URL, or <c>null</c> if the redirect doesn't apply.</returns>
|
||||
protected override string GetNewUrl(RewriteContext context)
|
||||
{
|
||||
string path = context.HttpContext.Request.Path.Value;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
foreach ((Regex pattern, string url) in this.Map)
|
||||
{
|
||||
if (pattern.IsMatch(path))
|
||||
return pattern.Replace(path, url);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
using System;
|
||||
using System.Net;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Rewrite;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework.RedirectRules
|
||||
{
|
||||
/// <summary>Redirect requests to HTTPS.</summary>
|
||||
internal class RedirectToHttpsRule : RedirectMatchRule
|
||||
{
|
||||
/*********
|
||||
** Fields
|
||||
*********/
|
||||
/// <summary>Matches requests which should be ignored.</summary>
|
||||
private readonly Func<HttpRequest, bool> Except;
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="except">Matches requests which should be ignored.</param>
|
||||
public RedirectToHttpsRule(Func<HttpRequest, bool> except = null)
|
||||
{
|
||||
this.Except = except ?? (req => false);
|
||||
this.StatusCode = HttpStatusCode.RedirectKeepVerb;
|
||||
}
|
||||
|
||||
|
||||
/*********
|
||||
** Protected methods
|
||||
*********/
|
||||
/// <summary>Get the new redirect URL.</summary>
|
||||
/// <param name="context">The rewrite context.</param>
|
||||
/// <returns>Returns the redirect URL, or <c>null</c> if the redirect doesn't apply.</returns>
|
||||
protected override string GetNewUrl(RewriteContext context)
|
||||
{
|
||||
HttpRequest request = context.HttpContext.Request;
|
||||
if (request.IsHttps || this.Except(request))
|
||||
return null;
|
||||
|
||||
UriBuilder uri = this.GetUrl(request);
|
||||
uri.Scheme = "https";
|
||||
return uri.ToString();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
namespace StardewModdingAPI.Web.Framework.ModRepositories
|
||||
namespace StardewModdingAPI.Web.Framework
|
||||
{
|
||||
/// <summary>The mod availability status on a remote site.</summary>
|
||||
internal enum RemoteModStatus
|
|
@ -1,62 +0,0 @@
|
|||
using System;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Rewrite;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework.RewriteRules
|
||||
{
|
||||
/// <summary>Redirect requests to HTTPS.</summary>
|
||||
/// <remarks>Derived from <a href="https://stackoverflow.com/a/44526747/262123" /> and <see cref="Microsoft.AspNetCore.Rewrite.Internal.RedirectToHttpsRule"/>.</remarks>
|
||||
internal class ConditionalRedirectToHttpsRule : IRule
|
||||
{
|
||||
/*********
|
||||
** Fields
|
||||
*********/
|
||||
/// <summary>A predicate which indicates when the rule should be applied.</summary>
|
||||
private readonly Func<HttpRequest, bool> ShouldRewrite;
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="shouldRewrite">A predicate which indicates when the rule should be applied.</param>
|
||||
public ConditionalRedirectToHttpsRule(Func<HttpRequest, bool> shouldRewrite = null)
|
||||
{
|
||||
this.ShouldRewrite = shouldRewrite ?? (req => true);
|
||||
}
|
||||
|
||||
/// <summary>Applies the rule. Implementations of ApplyRule should set the value for <see cref="RewriteContext.Result" /> (defaults to RuleResult.ContinueRules).</summary>
|
||||
/// <param name="context">The rewrite context.</param>
|
||||
public void ApplyRule(RewriteContext context)
|
||||
{
|
||||
HttpRequest request = context.HttpContext.Request;
|
||||
|
||||
// check condition
|
||||
if (this.IsSecure(request) || !this.ShouldRewrite(request))
|
||||
return;
|
||||
|
||||
// redirect request
|
||||
HttpResponse response = context.HttpContext.Response;
|
||||
response.StatusCode = (int)HttpStatusCode.RedirectKeepVerb;
|
||||
response.Headers["Location"] = new StringBuilder()
|
||||
.Append("https://")
|
||||
.Append(request.Host.Host)
|
||||
.Append(request.PathBase)
|
||||
.Append(request.Path)
|
||||
.Append(request.QueryString)
|
||||
.ToString();
|
||||
context.Result = RuleResult.EndResponse;
|
||||
}
|
||||
|
||||
/// <summary>Get whether the request was received over HTTPS.</summary>
|
||||
/// <param name="request">The request to check.</param>
|
||||
public bool IsSecure(HttpRequest request)
|
||||
{
|
||||
return
|
||||
request.IsHttps // HTTPS to server
|
||||
|| string.Equals(request.Headers["x-forwarded-proto"], "HTTPS", StringComparison.OrdinalIgnoreCase); // HTTPS to AWS load balancer
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
using System;
|
||||
using System.Net;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Rewrite;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework.RewriteRules
|
||||
{
|
||||
/// <summary>Redirect requests to an external URL if they match a condition.</summary>
|
||||
internal class RedirectToUrlRule : IRule
|
||||
{
|
||||
/*********
|
||||
** Fields
|
||||
*********/
|
||||
/// <summary>Get the new URL to which to redirect (or <c>null</c> to skip).</summary>
|
||||
private readonly Func<HttpRequest, string> NewUrl;
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="shouldRewrite">A predicate which indicates when the rule should be applied.</param>
|
||||
/// <param name="url">The new URL to which to redirect.</param>
|
||||
public RedirectToUrlRule(Func<HttpRequest, bool> shouldRewrite, string url)
|
||||
{
|
||||
this.NewUrl = req => shouldRewrite(req) ? url : null;
|
||||
}
|
||||
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="pathRegex">A case-insensitive regex to match against the path.</param>
|
||||
/// <param name="url">The external URL.</param>
|
||||
public RedirectToUrlRule(string pathRegex, string url)
|
||||
{
|
||||
Regex regex = new Regex(pathRegex, RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
this.NewUrl = req => req.Path.HasValue ? regex.Replace(req.Path.Value, url) : null;
|
||||
}
|
||||
|
||||
/// <summary>Applies the rule. Implementations of ApplyRule should set the value for <see cref="RewriteContext.Result" /> (defaults to RuleResult.ContinueRules).</summary>
|
||||
/// <param name="context">The rewrite context.</param>
|
||||
public void ApplyRule(RewriteContext context)
|
||||
{
|
||||
HttpRequest request = context.HttpContext.Request;
|
||||
|
||||
// check rewrite
|
||||
string newUrl = this.NewUrl(request);
|
||||
if (newUrl == null || newUrl == request.Path.Value)
|
||||
return;
|
||||
|
||||
// redirect request
|
||||
HttpResponse response = context.HttpContext.Response;
|
||||
response.StatusCode = (int)HttpStatusCode.Redirect;
|
||||
response.Headers["Location"] = newUrl;
|
||||
context.Result = RuleResult.EndResponse;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
using Microsoft.AspNetCore;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace StardewModdingAPI.Web
|
||||
{
|
||||
|
@ -13,13 +13,13 @@ namespace StardewModdingAPI.Web
|
|||
/// <param name="args">The command-line arguments.</param>
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
// configure web server
|
||||
WebHost
|
||||
Host
|
||||
.CreateDefaultBuilder(args)
|
||||
.CaptureStartupErrors(true)
|
||||
.UseSetting("detailedErrors", "true")
|
||||
.UseKestrel().UseIISIntegration() // must be used together; fixes intermittent errors on Azure: https://stackoverflow.com/a/38312175/262123
|
||||
.UseStartup<Startup>()
|
||||
.ConfigureWebHostDefaults(builder => builder
|
||||
.CaptureStartupErrors(true)
|
||||
.UseSetting("detailedErrors", "true")
|
||||
.UseStartup<Startup>()
|
||||
)
|
||||
.Build()
|
||||
.Run();
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<PropertyGroup>
|
||||
<AssemblyName>SMAPI.Web</AssemblyName>
|
||||
<RootNamespace>StardewModdingAPI.Web</RootNamespace>
|
||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
|
@ -12,23 +12,17 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.Storage.Blobs" Version="12.4.0" />
|
||||
<PackageReference Include="Hangfire.AspNetCore" Version="1.7.9" />
|
||||
<PackageReference Include="Azure.Storage.Blobs" Version="12.4.2" />
|
||||
<PackageReference Include="Hangfire.AspNetCore" Version="1.7.11" />
|
||||
<PackageReference Include="Hangfire.MemoryStorage" Version="1.7.0" />
|
||||
<PackageReference Include="Hangfire.Mongo" Version="0.6.7" />
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.23" />
|
||||
<PackageReference Include="Humanizer.Core" Version="2.7.9" />
|
||||
<PackageReference Include="JetBrains.Annotations" Version="2019.1.3" />
|
||||
<PackageReference Include="Markdig" Version="0.18.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore" Version="2.2.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Rewrite" Version="2.2.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.2.0" />
|
||||
<PackageReference Include="Mongo2Go" Version="2.2.12" />
|
||||
<PackageReference Include="MongoDB.Driver" Version="2.10.2" />
|
||||
<PackageReference Include="Humanizer.Core" Version="2.8.11" />
|
||||
<PackageReference Include="JetBrains.Annotations" Version="2020.1.0" />
|
||||
<PackageReference Include="Markdig" Version="0.20.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.0.2" />
|
||||
<PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.13" />
|
||||
<PackageReference Include="Pathoschild.FluentNexus" Version="1.0.0" />
|
||||
<PackageReference Include="Pathoschild.Http.FluentClient" Version="3.3.1" />
|
||||
<PackageReference Include="Pathoschild.FluentNexus" Version="1.0.1" />
|
||||
<PackageReference Include="Pathoschild.Http.FluentClient" Version="4.0.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="1.0.1" />
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using Hangfire;
|
||||
using Hangfire.MemoryStorage;
|
||||
using Hangfire.Mongo;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Rewrite;
|
||||
|
@ -10,13 +9,9 @@ using Microsoft.AspNetCore.Routing;
|
|||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Mongo2Go;
|
||||
using MongoDB.Bson.Serialization;
|
||||
using MongoDB.Driver;
|
||||
using Newtonsoft.Json;
|
||||
using StardewModdingAPI.Toolkit.Serialization;
|
||||
using StardewModdingAPI.Web.Framework;
|
||||
using StardewModdingAPI.Web.Framework.Caching;
|
||||
using StardewModdingAPI.Web.Framework.Caching.Mods;
|
||||
using StardewModdingAPI.Web.Framework.Caching.Wiki;
|
||||
using StardewModdingAPI.Web.Framework.Clients.Chucklefish;
|
||||
|
@ -27,7 +22,7 @@ using StardewModdingAPI.Web.Framework.Clients.Nexus;
|
|||
using StardewModdingAPI.Web.Framework.Clients.Pastebin;
|
||||
using StardewModdingAPI.Web.Framework.Compression;
|
||||
using StardewModdingAPI.Web.Framework.ConfigModels;
|
||||
using StardewModdingAPI.Web.Framework.RewriteRules;
|
||||
using StardewModdingAPI.Web.Framework.RedirectRules;
|
||||
using StardewModdingAPI.Web.Framework.Storage;
|
||||
|
||||
namespace StardewModdingAPI.Web
|
||||
|
@ -47,7 +42,7 @@ namespace StardewModdingAPI.Web
|
|||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="env">The hosting environment.</param>
|
||||
public Startup(IHostingEnvironment env)
|
||||
public Startup(IWebHostEnvironment env)
|
||||
{
|
||||
this.Configuration = new ConfigurationBuilder()
|
||||
.SetBasePath(env.ContentRootPath)
|
||||
|
@ -67,22 +62,33 @@ namespace StardewModdingAPI.Web
|
|||
.Configure<BackgroundServicesConfig>(this.Configuration.GetSection("BackgroundServices"))
|
||||
.Configure<ModCompatibilityListConfig>(this.Configuration.GetSection("ModCompatibilityList"))
|
||||
.Configure<ModUpdateCheckConfig>(this.Configuration.GetSection("ModUpdateCheck"))
|
||||
.Configure<MongoDbConfig>(this.Configuration.GetSection("MongoDB"))
|
||||
.Configure<SiteConfig>(this.Configuration.GetSection("Site"))
|
||||
.Configure<RouteOptions>(options => options.ConstraintMap.Add("semanticVersion", typeof(VersionConstraint)))
|
||||
.AddLogging()
|
||||
.AddMemoryCache()
|
||||
.AddMvc()
|
||||
.ConfigureApplicationPartManager(manager => manager.FeatureProviders.Add(new InternalControllerFeatureProvider()))
|
||||
.AddJsonOptions(options =>
|
||||
{
|
||||
foreach (JsonConverter converter in new JsonHelper().JsonSettings.Converters)
|
||||
options.SerializerSettings.Converters.Add(converter);
|
||||
.AddMemoryCache();
|
||||
|
||||
options.SerializerSettings.Formatting = Formatting.Indented;
|
||||
options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
|
||||
// init MVC
|
||||
services
|
||||
.AddControllers()
|
||||
.AddNewtonsoftJson(options => this.ConfigureJsonNet(options.SerializerSettings))
|
||||
.ConfigureApplicationPartManager(manager => manager.FeatureProviders.Add(new InternalControllerFeatureProvider()));
|
||||
services
|
||||
.AddRazorPages();
|
||||
|
||||
// init storage
|
||||
services.AddSingleton<IModCacheRepository>(new ModCacheMemoryRepository());
|
||||
services.AddSingleton<IWikiCacheRepository>(new WikiCacheMemoryRepository());
|
||||
|
||||
// init Hangfire
|
||||
services
|
||||
.AddHangfire((serv, config) =>
|
||||
{
|
||||
config
|
||||
.SetDataCompatibilityLevel(CompatibilityLevel.Version_170)
|
||||
.UseSimpleAssemblyNameTypeSerializer()
|
||||
.UseRecommendedSerializerSettings()
|
||||
.UseMemoryStorage();
|
||||
});
|
||||
MongoDbConfig mongoConfig = this.Configuration.GetSection("MongoDB").Get<MongoDbConfig>();
|
||||
|
||||
// init background service
|
||||
{
|
||||
|
@ -91,46 +97,6 @@ namespace StardewModdingAPI.Web
|
|||
services.AddHostedService<BackgroundService>();
|
||||
}
|
||||
|
||||
// init MongoDB
|
||||
services.AddSingleton<MongoDbRunner>(serv => !mongoConfig.IsConfigured()
|
||||
? MongoDbRunner.Start()
|
||||
: throw new InvalidOperationException("The MongoDB connection is configured, so the local development version should not be used.")
|
||||
);
|
||||
services.AddSingleton<IMongoDatabase>(serv =>
|
||||
{
|
||||
// get connection string
|
||||
string connectionString = mongoConfig.IsConfigured()
|
||||
? mongoConfig.ConnectionString
|
||||
: serv.GetRequiredService<MongoDbRunner>().ConnectionString;
|
||||
|
||||
// get client
|
||||
BsonSerializer.RegisterSerializer(new UtcDateTimeOffsetSerializer());
|
||||
return new MongoClient(connectionString).GetDatabase(mongoConfig.Database);
|
||||
});
|
||||
services.AddSingleton<IModCacheRepository>(serv => new ModCacheRepository(serv.GetRequiredService<IMongoDatabase>()));
|
||||
services.AddSingleton<IWikiCacheRepository>(serv => new WikiCacheRepository(serv.GetRequiredService<IMongoDatabase>()));
|
||||
|
||||
// init Hangfire
|
||||
services
|
||||
.AddHangfire(config =>
|
||||
{
|
||||
config
|
||||
.SetDataCompatibilityLevel(CompatibilityLevel.Version_170)
|
||||
.UseSimpleAssemblyNameTypeSerializer()
|
||||
.UseRecommendedSerializerSettings();
|
||||
|
||||
if (mongoConfig.IsConfigured())
|
||||
{
|
||||
config.UseMongoStorage(mongoConfig.ConnectionString, $"{mongoConfig.Database}-hangfire", new MongoStorageOptions
|
||||
{
|
||||
MigrationOptions = new MongoMigrationOptions(MongoMigrationStrategy.Drop),
|
||||
CheckConnection = false // error on startup takes down entire process
|
||||
});
|
||||
}
|
||||
else
|
||||
config.UseMemoryStorage();
|
||||
});
|
||||
|
||||
// init API clients
|
||||
{
|
||||
ApiClientsConfig api = this.Configuration.GetSection("ApiClients").Get<ApiClientsConfig>();
|
||||
|
@ -142,6 +108,7 @@ namespace StardewModdingAPI.Web
|
|||
baseUrl: api.ChucklefishBaseUrl,
|
||||
modPageUrlFormat: api.ChucklefishModPageUrlFormat
|
||||
));
|
||||
|
||||
services.AddSingleton<ICurseForgeClient>(new CurseForgeClient(
|
||||
userAgent: userAgent,
|
||||
apiUrl: api.CurseForgeBaseUrl
|
||||
|
@ -188,8 +155,7 @@ namespace StardewModdingAPI.Web
|
|||
|
||||
/// <summary>The method called by the runtime to configure the HTTP request pipeline.</summary>
|
||||
/// <param name="app">The application builder.</param>
|
||||
/// <param name="env">The hosting environment.</param>
|
||||
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
|
||||
public void Configure(IApplicationBuilder app)
|
||||
{
|
||||
// basic config
|
||||
app.UseDeveloperExceptionPage();
|
||||
|
@ -201,7 +167,13 @@ namespace StardewModdingAPI.Web
|
|||
)
|
||||
.UseRewriter(this.GetRedirectRules())
|
||||
.UseStaticFiles() // wwwroot folder
|
||||
.UseMvc();
|
||||
.UseRouting()
|
||||
.UseAuthorization()
|
||||
.UseEndpoints(p =>
|
||||
{
|
||||
p.MapControllers();
|
||||
p.MapRazorPages();
|
||||
});
|
||||
|
||||
// enable Hangfire dashboard
|
||||
app.UseHangfireDashboard("/tasks", new DashboardOptions
|
||||
|
@ -215,29 +187,63 @@ namespace StardewModdingAPI.Web
|
|||
/*********
|
||||
** Private methods
|
||||
*********/
|
||||
/// <summary>Configure a Json.NET serializer.</summary>
|
||||
/// <param name="settings">The serializer settings to edit.</param>
|
||||
private void ConfigureJsonNet(JsonSerializerSettings settings)
|
||||
{
|
||||
foreach (JsonConverter converter in new JsonHelper().JsonSettings.Converters)
|
||||
settings.Converters.Add(converter);
|
||||
|
||||
settings.Formatting = Formatting.Indented;
|
||||
settings.NullValueHandling = NullValueHandling.Ignore;
|
||||
}
|
||||
|
||||
/// <summary>Get the redirect rules to apply.</summary>
|
||||
private RewriteOptions GetRedirectRules()
|
||||
{
|
||||
var redirects = new RewriteOptions();
|
||||
var redirects = new RewriteOptions()
|
||||
// shortcut paths
|
||||
.Add(new RedirectPathsToUrlsRule(new Dictionary<string, string>
|
||||
{
|
||||
[@"^/3\.0\.?$"] = "https://stardewvalleywiki.com/Modding:Migrate_to_SMAPI_3.0",
|
||||
[@"^/(?:buildmsg|package)(?:/?(.*))$"] = "https://github.com/Pathoschild/SMAPI/blob/develop/docs/technical/mod-package.md#$1", // buildmsg deprecated, remove when SDV 1.4 is released
|
||||
[@"^/community\.?$"] = "https://stardewvalleywiki.com/Modding:Community",
|
||||
[@"^/compat\.?$"] = "https://smapi.io/mods",
|
||||
[@"^/docs\.?$"] = "https://stardewvalleywiki.com/Modding:Index",
|
||||
[@"^/install\.?$"] = "https://stardewvalleywiki.com/Modding:Player_Guide/Getting_Started#Install_SMAPI",
|
||||
[@"^/troubleshoot(.*)$"] = "https://stardewvalleywiki.com/Modding:Player_Guide/Troubleshooting$1",
|
||||
[@"^/xnb\.?$"] = "https://stardewvalleywiki.com/Modding:Using_XNB_mods"
|
||||
}))
|
||||
|
||||
// redirect to HTTPS (except API for Linux/Mac Mono compatibility)
|
||||
redirects.Add(new ConditionalRedirectToHttpsRule(
|
||||
shouldRewrite: req =>
|
||||
req.Host.Host != "localhost"
|
||||
&& !req.Path.StartsWithSegments("/api")
|
||||
));
|
||||
// legacy paths
|
||||
.Add(new RedirectPathsToUrlsRule(this.GetLegacyPathRedirects()))
|
||||
|
||||
// shortcut redirects
|
||||
redirects.Add(new RedirectToUrlRule(@"^/3\.0\.?$", "https://stardewvalleywiki.com/Modding:Migrate_to_SMAPI_3.0"));
|
||||
redirects.Add(new RedirectToUrlRule(@"^/(?:buildmsg|package)(?:/?(.*))$", "https://github.com/Pathoschild/SMAPI/blob/develop/docs/technical/mod-package.md#$1")); // buildmsg deprecated, remove when SDV 1.4 is released
|
||||
redirects.Add(new RedirectToUrlRule(@"^/community\.?$", "https://stardewvalleywiki.com/Modding:Community"));
|
||||
redirects.Add(new RedirectToUrlRule(@"^/compat\.?$", "https://smapi.io/mods"));
|
||||
redirects.Add(new RedirectToUrlRule(@"^/docs\.?$", "https://stardewvalleywiki.com/Modding:Index"));
|
||||
redirects.Add(new RedirectToUrlRule(@"^/install\.?$", "https://stardewvalleywiki.com/Modding:Player_Guide/Getting_Started#Install_SMAPI"));
|
||||
redirects.Add(new RedirectToUrlRule(@"^/troubleshoot(.*)$", "https://stardewvalleywiki.com/Modding:Player_Guide/Troubleshooting$1"));
|
||||
redirects.Add(new RedirectToUrlRule(@"^/xnb\.?$", "https://stardewvalleywiki.com/Modding:Using_XNB_mods"));
|
||||
// subdomains
|
||||
.Add(new RedirectHostsToUrlsRule(HttpStatusCode.PermanentRedirect, host => host switch
|
||||
{
|
||||
"api.smapi.io" => "smapi.io/api",
|
||||
"json.smapi.io" => "smapi.io/json",
|
||||
"log.smapi.io" => "smapi.io/log",
|
||||
"mods.smapi.io" => "smapi.io/mods",
|
||||
_ => host.EndsWith(".smapi.io")
|
||||
? "smapi.io"
|
||||
: null
|
||||
}))
|
||||
|
||||
// redirect legacy canimod.com URLs
|
||||
// redirect to HTTPS (except API for Linux/Mac Mono compatibility)
|
||||
.Add(
|
||||
new RedirectToHttpsRule(except: req => req.Host.Host == "localhost" || req.Path.StartsWithSegments("/api"))
|
||||
);
|
||||
|
||||
return redirects;
|
||||
}
|
||||
|
||||
/// <summary>Get the redirects for legacy paths that have been moved elsewhere.</summary>
|
||||
private IDictionary<string, string> GetLegacyPathRedirects()
|
||||
{
|
||||
var redirects = new Dictionary<string, string>();
|
||||
|
||||
// canimod.com => wiki
|
||||
var wikiRedirects = new Dictionary<string, string[]>
|
||||
{
|
||||
["Modding:Index#Migration_guides"] = new[] { "^/for-devs/updating-a-smapi-mod", "^/guides/updating-a-smapi-mod" },
|
||||
|
@ -251,10 +257,10 @@ namespace StardewModdingAPI.Web
|
|||
["Modding:Object_data"] = new[] { "^/for-devs/object-data", "^/guides/object-data" },
|
||||
["Modding:Weather_data"] = new[] { "^/for-devs/weather", "^/guides/weather" }
|
||||
};
|
||||
foreach (KeyValuePair<string, string[]> pair in wikiRedirects)
|
||||
foreach ((string page, string[] patterns) in wikiRedirects)
|
||||
{
|
||||
foreach (string pattern in pair.Value)
|
||||
redirects.Add(new RedirectToUrlRule(pattern, "https://stardewvalleywiki.com/" + pair.Key));
|
||||
foreach (string pattern in patterns)
|
||||
redirects.Add(pattern, "https://stardewvalleywiki.com/" + page);
|
||||
}
|
||||
|
||||
return redirects;
|
||||
|
|
|
@ -10,6 +10,9 @@ namespace StardewModdingAPI.Web.ViewModels.JsonValidator
|
|||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>Whether to show the edit view.</summary>
|
||||
public bool IsEditView { get; set; }
|
||||
|
||||
/// <summary>The paste ID.</summary>
|
||||
public string PasteID { get; set; }
|
||||
|
||||
|
@ -51,11 +54,13 @@ namespace StardewModdingAPI.Web.ViewModels.JsonValidator
|
|||
/// <param name="pasteID">The stored file ID.</param>
|
||||
/// <param name="schemaName">The schema name with which the JSON was validated.</param>
|
||||
/// <param name="schemaFormats">The supported JSON schemas (names indexed by ID).</param>
|
||||
public JsonValidatorModel(string pasteID, string schemaName, IDictionary<string, string> schemaFormats)
|
||||
/// <param name="isEditView">Whether to show the edit view.</param>
|
||||
public JsonValidatorModel(string pasteID, string schemaName, IDictionary<string, string> schemaFormats, bool isEditView)
|
||||
{
|
||||
this.PasteID = pasteID;
|
||||
this.SchemaName = schemaName;
|
||||
this.SchemaFormats = schemaFormats;
|
||||
this.IsEditView = isEditView;
|
||||
}
|
||||
|
||||
/// <summary>Set the validated content.</summary>
|
||||
|
|
|
@ -26,7 +26,7 @@ namespace StardewModdingAPI.Web.ViewModels
|
|||
public bool IsStale { get; set; }
|
||||
|
||||
/// <summary>Whether the mod metadata is available.</summary>
|
||||
public bool HasData => this.Mods != null;
|
||||
public bool HasData => this.Mods?.Any() == true;
|
||||
|
||||
|
||||
/*********
|
||||
|
|
|
@ -22,6 +22,9 @@ namespace StardewModdingAPI.Web.ViewModels
|
|||
/// <summary>The mod author's alternative names, if any.</summary>
|
||||
public string AlternateAuthors { get; set; }
|
||||
|
||||
/// <summary>The GitHub repo, if any.</summary>
|
||||
public string GitHubRepo { get; set; }
|
||||
|
||||
/// <summary>The URL to the mod's source code, if any.</summary>
|
||||
public string SourceUrl { get; set; }
|
||||
|
||||
|
@ -62,6 +65,7 @@ namespace StardewModdingAPI.Web.ViewModels
|
|||
this.AlternateNames = string.Join(", ", entry.Name.Skip(1).ToArray());
|
||||
this.Author = entry.Author.FirstOrDefault();
|
||||
this.AlternateAuthors = string.Join(", ", entry.Author.Skip(1).ToArray());
|
||||
this.GitHubRepo = entry.GitHubRepo;
|
||||
this.SourceUrl = this.GetSourceUrl(entry);
|
||||
this.Compatibility = new ModCompatibilityModel(entry.Compatibility);
|
||||
this.BetaCompatibility = entry.BetaCompatibility != null ? new ModCompatibilityModel(entry.BetaCompatibility) : null;
|
||||
|
@ -102,7 +106,7 @@ namespace StardewModdingAPI.Web.ViewModels
|
|||
if (entry.ModDropID.HasValue)
|
||||
{
|
||||
anyFound = true;
|
||||
yield return new ModLinkModel($"https://www.moddrop.com/sdv/mod/{entry.ModDropID}", "ModDrop");
|
||||
yield return new ModLinkModel($"https://www.moddrop.com/stardew-valley/mod/{entry.ModDropID}", "ModDrop");
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(entry.CurseForgeKey))
|
||||
{
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
}
|
||||
@section Head {
|
||||
<link rel="stylesheet" href="~/Content/css/index.css?r=20200105" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/jquery@3.3.1/dist/jquery.min.js" crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1" crossorigin="anonymous"></script>
|
||||
<script src="~/Content/js/index.js?r=20200105"></script>
|
||||
}
|
||||
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
string newUploadUrl = this.Url.PlainAction("Index", "JsonValidator", new { schemaName = Model.SchemaName });
|
||||
string schemaDisplayName = null;
|
||||
bool isValidSchema = Model.SchemaName != null && Model.SchemaFormats.TryGetValue(Model.SchemaName, out schemaDisplayName) && schemaDisplayName?.ToLower() != "none";
|
||||
bool isEditView = Model.Content == null || Model.SchemaName?.ToLower() == "edit";
|
||||
|
||||
// build title
|
||||
ViewData["Title"] = "JSON validator";
|
||||
|
@ -32,7 +31,7 @@
|
|||
<link rel="stylesheet" href="~/Content/css/json-validator.css?r=202002" />
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/tmont/sunlight@1.22.0/src/themes/sunlight.default.min.css" />
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/jquery@3.3.1/dist/jquery.min.js" crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1" crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/gh/tmont/sunlight@1.22.0/src/sunlight.min.js" crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/gh/tmont/sunlight@1.22.0/src/plugins/sunlight-plugin.linenumbers.min.js" crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/gh/tmont/sunlight@1.22.0/src/lang/sunlight.javascript.min.js" crossorigin="anonymous"></script>
|
||||
|
@ -40,7 +39,7 @@
|
|||
<script src="~/Content/js/json-validator.js?r=202002"></script>
|
||||
<script>
|
||||
$(function() {
|
||||
smapi.jsonValidator(@Json.Serialize(this.Url.PlainAction("Index", "JsonValidator", new { schemaName = "$schemaName", id = "$id" })), @Json.Serialize(Model.PasteID));
|
||||
smapi.jsonValidator(@this.ForJson(this.Url.PlainAction("Index", "JsonValidator", new { schemaName = "$schemaName", id = "$id" })), @this.ForJson(Model.PasteID));
|
||||
});
|
||||
</script>
|
||||
}
|
||||
|
@ -63,7 +62,7 @@ else if (Model.ParseError != null)
|
|||
<small v-pre>Error details: @Model.ParseError</small>
|
||||
</div>
|
||||
}
|
||||
else if (!isEditView && Model.PasteID != null)
|
||||
else if (!Model.IsEditView && Model.PasteID != null)
|
||||
{
|
||||
<div class="banner success">
|
||||
<strong>Share this link to let someone else see this page:</strong> <code>@curPageUrl</code><br />
|
||||
|
@ -84,7 +83,7 @@ else if (!isEditView && Model.PasteID != null)
|
|||
}
|
||||
|
||||
@* upload new file *@
|
||||
@if (isEditView)
|
||||
@if (Model.IsEditView)
|
||||
{
|
||||
<h2>Upload a JSON file</h2>
|
||||
<form action="@this.Url.PlainAction("PostAsync", "JsonValidator")" method="post">
|
||||
|
@ -112,7 +111,7 @@ else if (!isEditView && Model.PasteID != null)
|
|||
}
|
||||
|
||||
@* validation results *@
|
||||
@if (!isEditView)
|
||||
@if (!Model.IsEditView)
|
||||
{
|
||||
<div id="output">
|
||||
@if (Model.UploadError == null)
|
||||
|
@ -158,7 +157,7 @@ else if (!isEditView && Model.PasteID != null)
|
|||
{
|
||||
<option value="@pair.Key" selected="@(Model.SchemaName == pair.Key)">@pair.Value</option>
|
||||
}
|
||||
</select>) or <a href="@(this.Url.PlainAction("Index", "JsonValidator", new { id = this.Model.PasteID, schemaName = "edit" }))">edit this file</a>.
|
||||
</select>) or <a href="@(this.Url.PlainAction("Index", "JsonValidator", new { id = this.Model.PasteID, schemaName = this.Model.SchemaName, operation = "edit" }))">edit this file</a>.
|
||||
</div>
|
||||
<pre id="raw-content" class="sunlight-highlight-javascript">@Model.Content</pre>
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
@using Humanizer
|
||||
@using Newtonsoft.Json
|
||||
@using StardewModdingAPI.Toolkit.Utilities
|
||||
@using StardewModdingAPI.Web.Framework
|
||||
@using StardewModdingAPI.Web.Framework.LogParsing.Models
|
||||
|
@ -12,7 +11,6 @@
|
|||
.GetValues(typeof(LogLevel))
|
||||
.Cast<LogLevel>()
|
||||
.ToDictionary(level => level.ToString().ToLower(), level => level != LogLevel.Trace);
|
||||
JsonSerializerSettings noFormatting = new JsonSerializerSettings { Formatting = Formatting.None };
|
||||
|
||||
string curPageUrl = this.Url.PlainAction("Index", "LogParser", new { id = Model.PasteID }, absoluteUrl: true);
|
||||
}
|
||||
|
@ -25,19 +23,19 @@
|
|||
<link rel="stylesheet" href="~/Content/css/file-upload.css?r=202002" />
|
||||
<link rel="stylesheet" href="~/Content/css/log-parser.css?r=202002" />
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.min.js" crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/jquery@3.3.1/dist/jquery.min.js" crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.11" crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1" crossorigin="anonymous"></script>
|
||||
<script src="~/Content/js/file-upload.js?r=202002"></script>
|
||||
<script src="~/Content/js/log-parser.js?r=202002"></script>
|
||||
<script>
|
||||
$(function() {
|
||||
smapi.logParser({
|
||||
logStarted: new Date(@Json.Serialize(Model.ParsedLog?.Timestamp)),
|
||||
showPopup: @Json.Serialize(Model.ParsedLog == null),
|
||||
showMods: @Json.Serialize(Model.ParsedLog?.Mods?.Select(p => Model.GetSlug(p.Name)).Distinct().ToDictionary(slug => slug, slug => true), noFormatting),
|
||||
showSections: @Json.Serialize(Enum.GetNames(typeof(LogSection)).ToDictionary(section => section, section => false), noFormatting),
|
||||
showLevels: @Json.Serialize(defaultFilters, noFormatting),
|
||||
enableFilters: @Json.Serialize(!Model.ShowRaw)
|
||||
logStarted: new Date(@this.ForJson(Model.ParsedLog?.Timestamp)),
|
||||
showPopup: @this.ForJson(Model.ParsedLog == null),
|
||||
showMods: @this.ForJson(Model.ParsedLog?.Mods?.Select(p => Model.GetSlug(p.Name)).Distinct().ToDictionary(slug => slug, slug => true)),
|
||||
showSections: @this.ForJson(Enum.GetNames(typeof(LogSection)).ToDictionary(section => section, section => false)),
|
||||
showLevels: @this.ForJson(defaultFilters),
|
||||
enableFilters: @this.ForJson(!Model.ShowRaw)
|
||||
}, '@this.Url.PlainAction("Index", "LogParser", values: null)');
|
||||
});
|
||||
</script>
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue