Merge branch 'develop' into stable
This commit is contained in:
commit
e64ecc89f9
|
@ -30,8 +30,5 @@ _ReSharper*/
|
||||||
# sensitive files
|
# sensitive files
|
||||||
appsettings.Development.json
|
appsettings.Development.json
|
||||||
|
|
||||||
# AWS generated files
|
|
||||||
src/SMAPI.Web.LegacyRedirects/aws-beanstalk-tools-defaults.json
|
|
||||||
|
|
||||||
# Azure generated files
|
# Azure generated files
|
||||||
src/SMAPI.Web/Properties/PublishProfiles/*.pubxml
|
src/SMAPI.Web/Properties/PublishProfiles/*.pubxml
|
||||||
|
|
Binary file not shown.
|
@ -4,9 +4,10 @@
|
||||||
|
|
||||||
<!--set properties -->
|
<!--set properties -->
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Version>3.5.0</Version>
|
<Version>3.6.0</Version>
|
||||||
<Product>SMAPI</Product>
|
<Product>SMAPI</Product>
|
||||||
|
|
||||||
|
<LangVersion>latest</LangVersion>
|
||||||
<AssemblySearchPaths>$(AssemblySearchPaths);{GAC}</AssemblySearchPaths>
|
<AssemblySearchPaths>$(AssemblySearchPaths);{GAC}</AssemblySearchPaths>
|
||||||
<DefineConstants Condition="$(OS) == 'Windows_NT'">$(DefineConstants);SMAPI_FOR_WINDOWS</DefineConstants>
|
<DefineConstants Condition="$(OS) == 'Windows_NT'">$(DefineConstants);SMAPI_FOR_WINDOWS</DefineConstants>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
|
@ -1,6 +1,54 @@
|
||||||
← [README](README.md)
|
← [README](README.md)
|
||||||
|
|
||||||
# Release notes
|
# 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
|
## 3.5
|
||||||
Released 27 April 2020 for Stardew Valley 1.4.1 or later.
|
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)
|
* [Compiling from source](#compiling-from-source)
|
||||||
* [Debugging a local build](#debugging-a-local-build)
|
* [Debugging a local build](#debugging-a-local-build)
|
||||||
* [Preparing a release](#preparing-a-release)
|
* [Preparing a release](#preparing-a-release)
|
||||||
|
* [Using a custom Harmony build](#using-a-custom-harmony-build)
|
||||||
* [Release notes](#release-notes)
|
* [Release notes](#release-notes)
|
||||||
|
|
||||||
## Customisation
|
## Customisation
|
||||||
|
@ -57,24 +58,22 @@ SMAPI uses a small number of conditional compilation constants, which you can se
|
||||||
flag | purpose
|
flag | purpose
|
||||||
---- | -------
|
---- | -------
|
||||||
`SMAPI_FOR_WINDOWS` | Whether SMAPI is being compiled on Windows for players on Windows. Set automatically in `crossplatform.targets`.
|
`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
|
## For SMAPI developers
|
||||||
### Compiling from source
|
### 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
|
SMAPI uses build configuration derived from the [crossplatform mod config](https://smapi.io/package/readme)
|
||||||
[Visual Studio](https://www.visualstudio.com/vs/community/) on Windows,
|
to detect your current OS automatically and load the correct references. Compile output will be
|
||||||
[MonoDevelop](https://www.monodevelop.com/) on Linux,
|
placed in a `bin` folder at the root of the Git repository.
|
||||||
[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.
|
|
||||||
|
|
||||||
### Debugging a local build
|
### Debugging a local build
|
||||||
Rebuilding the solution in debug mode will copy the SMAPI files into your game folder. Starting
|
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 `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.
|
doesn't work in MonoDevelop on Linux, unfortunately.
|
||||||
|
|
||||||
### Preparing a release
|
### 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)
|
[crossplatforming info](https://stardewvalleywiki.com/Modding:Modder_Guide/Test_and_Troubleshoot#Testing_on_all_platforms)
|
||||||
on the wiki for the first-time setup.
|
on the wiki for the first-time setup.
|
||||||
|
|
||||||
1. Update the version number in `.root/build/common.targets` and `Constants::Version`. Make sure
|
1. Update the version numbers in `build/common.targets`, `Constants`, and the `manifest.json` for
|
||||||
you use a [semantic version](https://semver.org). Recommended format:
|
bundled mods. Make sure you use a [semantic version](https://semver.org). Recommended format:
|
||||||
|
|
||||||
build type | format | example
|
build type | format | example
|
||||||
:--------- | :----------------------- | :------
|
:--------- | :----------------------- | :------
|
||||||
dev build | `<version>-alpha.<date>` | `3.0-alpha.20171230`
|
dev build | `<version>-alpha.<date>` | `3.0.0-alpha.20171230`
|
||||||
prerelease | `<version>-beta.<count>` | `3.0-beta.2`
|
prerelease | `<version>-beta.<count>` | `3.0.0-beta.2`
|
||||||
release | `<version>` | `3.0`
|
release | `<version>` | `3.0.0`
|
||||||
|
|
||||||
2. In Windows:
|
2. In Windows:
|
||||||
1. Rebuild the solution in Release mode.
|
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`.
|
3. Rename the folders to `SMAPI <version> installer` and `SMAPI <version> installer for developers`.
|
||||||
4. Zip the two folders.
|
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
|
## Release notes
|
||||||
See [release notes](../release-notes.md).
|
See [release notes](../release-notes.md).
|
||||||
|
|
|
@ -110,8 +110,9 @@ Available schemas:
|
||||||
|
|
||||||
format | schema URL
|
format | schema URL
|
||||||
------ | ----------
|
------ | ----------
|
||||||
[SMAPI `manifest.json`](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Manifest) | https://smapi.io/schemas/manifest.json
|
[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: 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
|
## Web API
|
||||||
### Overview
|
### 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
|
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.
|
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
|
1. Edit `appsettings.Development.json` and set these options:
|
||||||
credentials empty to default to fetching data anonymously, and storing data in-memory and
|
|
||||||
on disk.
|
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.
|
2. Launch `SMAPI.Web` from Visual Studio to run a local version of the site.
|
||||||
|
|
||||||
### Production environment
|
### Production environment
|
||||||
|
@ -355,19 +366,15 @@ accordingly.
|
||||||
|
|
||||||
Initial setup:
|
Initial setup:
|
||||||
|
|
||||||
1. Launch an empty MongoDB server (e.g. using [MongoDB Atlas](https://www.mongodb.com/cloud/atlas))
|
1. Create an Azure Blob storage account for uploaded files.
|
||||||
for mod data.
|
2. Create an Azure App Services environment running the latest .NET Core on Linux or Windows.
|
||||||
2. Create an Azure Blob storage account for uploaded files.
|
3. Add these application settings in the new App Services environment:
|
||||||
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:
|
|
||||||
|
|
||||||
property name | description
|
property name | description
|
||||||
------------------------------- | -----------------
|
------------------------------- | -----------------
|
||||||
`ApiClients.AzureBlobConnectionString` | The connection string for the Azure Blob storage account created in step 2.
|
`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.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).
|
`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:
|
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: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.
|
`Site:SupporterList` | A list of Patreon supports to credit on the download page.
|
||||||
|
|
||||||
To deploy updates:
|
To deploy updates, just [redeploy the web project from Visual Studio](https://docs.microsoft.com/en-us/visualstudio/deployment/quickstart-deploy-to-azure).
|
||||||
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.)
|
|
||||||
|
|
|
@ -3,7 +3,6 @@ using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
|
||||||
using Microsoft.Win32;
|
using Microsoft.Win32;
|
||||||
using StardewModdingApi.Installer.Enums;
|
using StardewModdingApi.Installer.Enums;
|
||||||
using StardewModdingAPI.Installer.Framework;
|
using StardewModdingAPI.Installer.Framework;
|
||||||
|
@ -624,7 +623,7 @@ namespace StardewModdingApi.Installer
|
||||||
{
|
{
|
||||||
try
|
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;
|
break;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
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>
|
/// <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="print">A callback which prints a message to the console.</param>
|
||||||
/// <param name="message">The message to print.</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>
|
/// <param name="indent">The indentation to prefix to output.</param>
|
||||||
private string InteractivelyChoose(string message, string[] options, string indent = "", Action<string> print = null)
|
private string InteractivelyChoose(string message, string[] options, string indent = "", Action<string> print = null)
|
||||||
{
|
{
|
||||||
print = print ?? this.PrintInfo;
|
print ??= this.PrintInfo;
|
||||||
|
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,11 +1,8 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<AssemblyName>SMAPI.Installer</AssemblyName>
|
|
||||||
<RootNamespace>StardewModdingAPI.Installer</RootNamespace>
|
<RootNamespace>StardewModdingAPI.Installer</RootNamespace>
|
||||||
<Description>The SMAPI installer for players.</Description>
|
<Description>The SMAPI installer for players.</Description>
|
||||||
<TargetFramework>net45</TargetFramework>
|
<TargetFramework>net45</TargetFramework>
|
||||||
<LangVersion>latest</LangVersion>
|
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<PlatformTarget>x86</PlatformTarget>
|
<PlatformTarget>x86</PlatformTarget>
|
||||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||||
|
@ -16,13 +13,10 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Update="assets\*">
|
<None Update="assets\*" CopyToOutputDirectory="PreserveNewest" />
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<Import Project="..\SMAPI.Internal\SMAPI.Internal.projitems" Label="Shared" />
|
<Import Project="..\SMAPI.Internal\SMAPI.Internal.projitems" Label="Shared" />
|
||||||
<Import Project="..\..\build\common.targets" />
|
<Import Project="..\..\build\common.targets" />
|
||||||
<Import Project="..\..\build\prepare-install-package.targets" />
|
<Import Project="..\..\build\prepare-install-package.targets" />
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="2.10.0" />
|
<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="NUnit" Version="3.12.0" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="3.16.1">
|
<PackageReference Include="NUnit3TestAdapter" Version="3.16.1">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
|
|
@ -1,11 +1,8 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<AssemblyName>SMAPI.ModBuildConfig.Analyzer</AssemblyName>
|
|
||||||
<RootNamespace>StardewModdingAPI.ModBuildConfig.Analyzer</RootNamespace>
|
<RootNamespace>StardewModdingAPI.ModBuildConfig.Analyzer</RootNamespace>
|
||||||
<Version>3.0.0</Version>
|
<Version>3.0.0</Version>
|
||||||
<TargetFramework>netstandard2.0</TargetFramework>
|
<TargetFramework>netstandard2.0</TargetFramework>
|
||||||
<LangVersion>latest</LangVersion>
|
|
||||||
<IncludeBuildOutput>false</IncludeBuildOutput>
|
<IncludeBuildOutput>false</IncludeBuildOutput>
|
||||||
<OutputPath>bin</OutputPath>
|
<OutputPath>bin</OutputPath>
|
||||||
<LangVersion>latest</LangVersion>
|
<LangVersion>latest</LangVersion>
|
||||||
|
@ -19,5 +16,4 @@
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
|
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
@ -153,23 +153,22 @@ namespace StardewModdingAPI.ModBuildConfig
|
||||||
|
|
||||||
// create zip file
|
// create zip file
|
||||||
Directory.CreateDirectory(outputFolderPath);
|
Directory.CreateDirectory(outputFolderPath);
|
||||||
using (Stream zipStream = new FileStream(zipPath, FileMode.Create, FileAccess.Write))
|
using Stream zipStream = new FileStream(zipPath, FileMode.Create, FileAccess.Write);
|
||||||
using (ZipArchive archive = new ZipArchive(zipStream, ZipArchiveMode.Create))
|
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
|
// get file info
|
||||||
string filePath = file.FullName;
|
string filePath = file.FullName;
|
||||||
string entryName = folderName + '/' + relativePath.Replace(Path.DirectorySeparatorChar, '/');
|
string entryName = folderName + '/' + relativePath.Replace(Path.DirectorySeparatorChar, '/');
|
||||||
|
|
||||||
// add to zip
|
// add to zip
|
||||||
using (Stream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read))
|
using Stream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
|
||||||
using (Stream fileStreamInZip = archive.CreateEntry(entryName).Open())
|
using Stream fileStreamInZip = archive.CreateEntry(entryName).Open();
|
||||||
fileStream.CopyTo(fileStreamInZip);
|
fileStream.CopyTo(fileStreamInZip);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,24 +1,12 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<AssemblyName>SMAPI.ModBuildConfig</AssemblyName>
|
|
||||||
<RootNamespace>StardewModdingAPI.ModBuildConfig</RootNamespace>
|
<RootNamespace>StardewModdingAPI.ModBuildConfig</RootNamespace>
|
||||||
<Version>3.0.0</Version>
|
<Version>3.1.0</Version>
|
||||||
<TargetFramework>net45</TargetFramework>
|
<TargetFramework>net45</TargetFramework>
|
||||||
<LangVersion>latest</LangVersion>
|
|
||||||
<PlatformTarget>x86</PlatformTarget>
|
<PlatformTarget>x86</PlatformTarget>
|
||||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||||
</PropertyGroup>
|
</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>
|
<ItemGroup>
|
||||||
<Reference Include="Microsoft.Build" />
|
<Reference Include="Microsoft.Build" />
|
||||||
<Reference Include="Microsoft.Build.Framework" />
|
<Reference Include="Microsoft.Build.Framework" />
|
||||||
|
@ -28,19 +16,16 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="..\..\docs\technical\mod-package.md">
|
<ProjectReference Include="..\SMAPI.Toolkit\SMAPI.Toolkit.csproj" />
|
||||||
<Link>mod-package.md</Link>
|
|
||||||
</None>
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Update="assets\nuget-icon.png">
|
<None Include="..\..\build\find-game-folder.targets" Link="build\find-game-folder.targets" />
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<None Include="..\..\docs\technical\mod-package.md" Link="mod-package.md" />
|
||||||
</None>
|
<None Update="assets\nuget-icon.png" CopyToOutputDirectory="PreserveNewest" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<Import Project="..\SMAPI.Internal\SMAPI.Internal.projitems" Label="Shared" />
|
<Import Project="..\SMAPI.Internal\SMAPI.Internal.projitems" Label="Shared" />
|
||||||
<Import Project="..\..\build\common.targets" />
|
<Import Project="..\..\build\common.targets" />
|
||||||
<Import Project="..\..\build\prepare-nuget-package.targets" />
|
<Import Project="..\..\build\prepare-nuget-package.targets" />
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
@ -38,58 +38,26 @@
|
||||||
**********************************************-->
|
**********************************************-->
|
||||||
<!-- common -->
|
<!-- common -->
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Reference Include="$(GameExecutableName)">
|
<Reference Include="$(GameExecutableName)" HintPath="$(GamePath)\$(GameExecutableName).exe" Private="$(CopyModReferencesToBuildOutput)" />
|
||||||
<HintPath>$(GamePath)\$(GameExecutableName).exe</HintPath>
|
<Reference Include="StardewValley.GameData" HintPath="$(GamePath)\StardewValley.GameData.dll" Private="$(CopyModReferencesToBuildOutput)" />
|
||||||
<Private>$(CopyModReferencesToBuildOutput)</Private>
|
<Reference Include="StardewModdingAPI" HintPath="$(GamePath)\StardewModdingAPI.exe" Private="$(CopyModReferencesToBuildOutput)" />
|
||||||
</Reference>
|
<Reference Include="SMAPI.Toolkit.CoreInterfaces" HintPath="$(GamePath)\smapi-internal\SMAPI.Toolkit.CoreInterfaces.dll" Private="$(CopyModReferencesToBuildOutput)" />
|
||||||
<Reference Include="StardewValley.GameData">
|
<Reference Include="xTile" HintPath="$(GamePath)\xTile.dll" Private="$(CopyModReferencesToBuildOutput)" />
|
||||||
<HintPath>$(GamePath)\StardewValley.GameData.dll</HintPath>
|
<Reference Include="0Harmony" Condition="'$(EnableHarmony)' == 'true'" HintPath="$(GamePath)\smapi-internal\0Harmony.dll" Private="$(CopyModReferencesToBuildOutput)" />
|
||||||
<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>
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<!-- Windows -->
|
<!-- Windows -->
|
||||||
<ItemGroup Condition="$(OS) == 'Windows_NT'">
|
<ItemGroup Condition="$(OS) == 'Windows_NT'">
|
||||||
<Reference Include="Microsoft.Xna.Framework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86">
|
<Reference Include="Microsoft.Xna.Framework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="$(CopyModReferencesToBuildOutput)" />
|
||||||
<Private>$(CopyModReferencesToBuildOutput)</Private>
|
<Reference Include="Microsoft.Xna.Framework.Game, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="$(CopyModReferencesToBuildOutput)" />
|
||||||
</Reference>
|
<Reference Include="Microsoft.Xna.Framework.Graphics, 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">
|
<Reference Include="Microsoft.Xna.Framework.Xact, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="$(CopyModReferencesToBuildOutput)" />
|
||||||
<Private>$(CopyModReferencesToBuildOutput)</Private>
|
<Reference Include="Netcode" HintPath="$(GamePath)\Netcode.dll" Private="$(CopyModReferencesToBuildOutput)" />
|
||||||
</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>
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<!-- Linux/Mac -->
|
<!-- Linux/Mac -->
|
||||||
<ItemGroup Condition="$(OS) != 'Windows_NT'">
|
<ItemGroup Condition="$(OS) != 'Windows_NT'">
|
||||||
<Reference Include="MonoGame.Framework">
|
<Reference Include="MonoGame.Framework" HintPath="$(GamePath)\MonoGame.Framework.dll" Private="$(CopyModReferencesToBuildOutput)" />
|
||||||
<HintPath>$(GamePath)\MonoGame.Framework.dll</HintPath>
|
|
||||||
<Private>$(CopyModReferencesToBuildOutput)</Private>
|
|
||||||
</Reference>
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,72 +1,45 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<AssemblyName>ConsoleCommands</AssemblyName>
|
<AssemblyName>ConsoleCommands</AssemblyName>
|
||||||
<RootNamespace>StardewModdingAPI.Mods.ConsoleCommands</RootNamespace>
|
<RootNamespace>StardewModdingAPI.Mods.ConsoleCommands</RootNamespace>
|
||||||
<TargetFramework>net45</TargetFramework>
|
<TargetFramework>net45</TargetFramework>
|
||||||
<LangVersion>latest</LangVersion>
|
|
||||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||||
<PlatformTarget>x86</PlatformTarget>
|
<PlatformTarget>x86</PlatformTarget>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\SMAPI\SMAPI.csproj">
|
<ProjectReference Include="..\SMAPI\SMAPI.csproj" Private="False" />
|
||||||
<Private>False</Private>
|
|
||||||
</ProjectReference>
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Reference Include="$(GameExecutableName)">
|
<Reference Include="$(GameExecutableName)" HintPath="$(GamePath)\$(GameExecutableName).exe" Private="False" />
|
||||||
<HintPath>$(GamePath)\$(GameExecutableName).exe</HintPath>
|
<Reference Include="StardewValley.GameData" HintPath="$(GamePath)\StardewValley.GameData.dll" Private="False" />
|
||||||
<Private>False</Private>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="StardewValley.GameData">
|
|
||||||
<HintPath>$(GamePath)\StardewValley.GameData.dll</HintPath>
|
|
||||||
<Private>False</Private>
|
|
||||||
</Reference>
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<Choose>
|
<Choose>
|
||||||
<!-- Windows -->
|
<!-- Windows -->
|
||||||
<When Condition="$(OS) == 'Windows_NT'">
|
<When Condition="$(OS) == 'Windows_NT'">
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Reference Include="Netcode">
|
<Reference Include="Netcode" HintPath="$(GamePath)\Netcode.dll" Private="False" />
|
||||||
<HintPath>$(GamePath)\Netcode.dll</HintPath>
|
<Reference Include="Microsoft.Xna.Framework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="False" />
|
||||||
<Private>False</Private>
|
<Reference Include="Microsoft.Xna.Framework.Game, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="False" />
|
||||||
</Reference>
|
<Reference Include="Microsoft.Xna.Framework.Graphics, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="False" />
|
||||||
<Reference Include="Microsoft.Xna.Framework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86">
|
<Reference Include="Microsoft.Xna.Framework.Xact, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="False" />
|
||||||
<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>
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</When>
|
</When>
|
||||||
|
|
||||||
<!-- Linux/Mac -->
|
<!-- Linux/Mac -->
|
||||||
<Otherwise>
|
<Otherwise>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Reference Include="MonoGame.Framework">
|
<Reference Include="MonoGame.Framework" HintPath="$(GamePath)\MonoGame.Framework.dll" Private="False" />
|
||||||
<HintPath>$(GamePath)\MonoGame.Framework.dll</HintPath>
|
|
||||||
<Private>False</Private>
|
|
||||||
</Reference>
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Otherwise>
|
</Otherwise>
|
||||||
</Choose>
|
</Choose>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Update="manifest.json">
|
<None Update="manifest.json" CopyToOutputDirectory="PreserveNewest" />
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<Import Project="..\SMAPI.Internal\SMAPI.Internal.projitems" Label="Shared" />
|
<Import Project="..\SMAPI.Internal\SMAPI.Internal.projitems" Label="Shared" />
|
||||||
<Import Project="..\..\build\common.targets" />
|
<Import Project="..\..\build\common.targets" />
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
{
|
{
|
||||||
"Name": "Console Commands",
|
"Name": "Console Commands",
|
||||||
"Author": "SMAPI",
|
"Author": "SMAPI",
|
||||||
"Version": "3.5.0",
|
"Version": "3.6.0",
|
||||||
"Description": "Adds SMAPI console commands that let you manipulate the game.",
|
"Description": "Adds SMAPI console commands that let you manipulate the game.",
|
||||||
"UniqueID": "SMAPI.ConsoleCommands",
|
"UniqueID": "SMAPI.ConsoleCommands",
|
||||||
"EntryDll": "ConsoleCommands.dll",
|
"EntryDll": "ConsoleCommands.dll",
|
||||||
"MinimumApiVersion": "3.5.0"
|
"MinimumApiVersion": "3.6.0"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,34 +1,24 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<AssemblyName>SaveBackup</AssemblyName>
|
<AssemblyName>SaveBackup</AssemblyName>
|
||||||
<RootNamespace>StardewModdingAPI.Mods.SaveBackup</RootNamespace>
|
<RootNamespace>StardewModdingAPI.Mods.SaveBackup</RootNamespace>
|
||||||
<TargetFramework>net45</TargetFramework>
|
<TargetFramework>net45</TargetFramework>
|
||||||
<LangVersion>latest</LangVersion>
|
|
||||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||||
<PlatformTarget>x86</PlatformTarget>
|
<PlatformTarget>x86</PlatformTarget>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\SMAPI\SMAPI.csproj">
|
<ProjectReference Include="..\SMAPI\SMAPI.csproj" Private="False" />
|
||||||
<Private>False</Private>
|
|
||||||
</ProjectReference>
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Reference Include="$(GameExecutableName)">
|
<Reference Include="$(GameExecutableName)" HintPath="$(GamePath)\$(GameExecutableName).exe" Private="False" />
|
||||||
<HintPath>$(GamePath)\$(GameExecutableName).exe</HintPath>
|
|
||||||
<Private>False</Private>
|
|
||||||
</Reference>
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Update="manifest.json">
|
<None Update="manifest.json" CopyToOutputDirectory="PreserveNewest" />
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<Import Project="..\SMAPI.Internal\SMAPI.Internal.projitems" Label="Shared" />
|
<Import Project="..\SMAPI.Internal\SMAPI.Internal.projitems" Label="Shared" />
|
||||||
<Import Project="..\..\build\common.targets" />
|
<Import Project="..\..\build\common.targets" />
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
{
|
{
|
||||||
"Name": "Save Backup",
|
"Name": "Save Backup",
|
||||||
"Author": "SMAPI",
|
"Author": "SMAPI",
|
||||||
"Version": "3.5.0",
|
"Version": "3.6.0",
|
||||||
"Description": "Automatically backs up all your saves once per day into its folder.",
|
"Description": "Automatically backs up all your saves once per day into its folder.",
|
||||||
"UniqueID": "SMAPI.SaveBackup",
|
"UniqueID": "SMAPI.SaveBackup",
|
||||||
"EntryDll": "SaveBackup.dll",
|
"EntryDll": "SaveBackup.dll",
|
||||||
"MinimumApiVersion": "3.5.0"
|
"MinimumApiVersion": "3.6.0"
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<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="Newtonsoft.Json" Version="12.0.3" />
|
||||||
<PackageReference Include="NUnit" Version="3.12.0" />
|
<PackageReference Include="NUnit" Version="3.12.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
|
@ -1,15 +1,11 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<AssemblyName>SMAPI.Toolkit.CoreInterfaces</AssemblyName>
|
|
||||||
<RootNamespace>StardewModdingAPI</RootNamespace>
|
<RootNamespace>StardewModdingAPI</RootNamespace>
|
||||||
<Description>Provides toolkit interfaces which are available to SMAPI mods.</Description>
|
<Description>Provides toolkit interfaces which are available to SMAPI mods.</Description>
|
||||||
<TargetFrameworks>net4.5;netstandard2.0</TargetFrameworks>
|
<TargetFrameworks>net4.5;netstandard2.0</TargetFrameworks>
|
||||||
<LangVersion>latest</LangVersion>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\SMAPI.Toolkit.CoreInterfaces.xml</DocumentationFile>
|
|
||||||
<PlatformTarget Condition="'$(TargetFramework)' == 'net4.5'">x86</PlatformTarget>
|
<PlatformTarget Condition="'$(TargetFramework)' == 'net4.5'">x86</PlatformTarget>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<Import Project="..\..\build\common.targets" />
|
<Import Project="..\..\build\common.targets" />
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
@ -62,16 +62,15 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
|
||||||
private TResult Post<TBody, TResult>(string url, TBody content)
|
private TResult Post<TBody, TResult>(string url, TBody content)
|
||||||
{
|
{
|
||||||
// note: avoid HttpClient for Mac compatibility
|
// note: avoid HttpClient for Mac compatibility
|
||||||
using (WebClient client = new WebClient())
|
using WebClient client = new WebClient();
|
||||||
{
|
|
||||||
Uri fullUrl = new Uri(this.BaseUrl, url);
|
|
||||||
string data = JsonConvert.SerializeObject(content);
|
|
||||||
|
|
||||||
client.Headers["Content-Type"] = "application/json";
|
Uri fullUrl = new Uri(this.BaseUrl, url);
|
||||||
client.Headers["User-Agent"] = $"SMAPI/{this.Version}";
|
string data = JsonConvert.SerializeObject(content);
|
||||||
string response = client.UploadString(fullUrl, data);
|
|
||||||
return JsonConvert.DeserializeObject<TResult>(response, this.JsonSettings);
|
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");
|
string pullRequestUrl = this.GetAttribute(node, "data-pr");
|
||||||
IDictionary<string, string> mapLocalVersions = this.GetAttributeAsVersionMapping(node, "data-map-local-versions");
|
IDictionary<string, string> mapLocalVersions = this.GetAttributeAsVersionMapping(node, "data-map-local-versions");
|
||||||
IDictionary<string, string> mapRemoteVersions = this.GetAttributeAsVersionMapping(node, "data-map-remote-versions");
|
IDictionary<string, string> mapRemoteVersions = this.GetAttributeAsVersionMapping(node, "data-map-remote-versions");
|
||||||
|
string[] changeUpdateKeys = this.GetAttributeAsCsv(node, "data-change-update-keys");
|
||||||
|
|
||||||
// parse stable compatibility
|
// parse stable compatibility
|
||||||
WikiCompatibilityInfo compatibility = new WikiCompatibilityInfo
|
WikiCompatibilityInfo compatibility = new WikiCompatibilityInfo
|
||||||
|
@ -153,6 +154,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
|
||||||
Warnings = warnings,
|
Warnings = warnings,
|
||||||
PullRequestUrl = pullRequestUrl,
|
PullRequestUrl = pullRequestUrl,
|
||||||
DevNote = devNote,
|
DevNote = devNote,
|
||||||
|
ChangeUpdateKeys = changeUpdateKeys,
|
||||||
MapLocalVersions = mapLocalVersions,
|
MapLocalVersions = mapLocalVersions,
|
||||||
MapRemoteVersions = mapRemoteVersions,
|
MapRemoteVersions = mapRemoteVersions,
|
||||||
Anchor = anchor
|
Anchor = anchor
|
||||||
|
|
|
@ -3,25 +3,28 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
|
||||||
/// <summary>The compatibility status for a mod.</summary>
|
/// <summary>The compatibility status for a mod.</summary>
|
||||||
public enum WikiCompatibilityStatus
|
public enum WikiCompatibilityStatus
|
||||||
{
|
{
|
||||||
|
/// <summary>The status is unknown.</summary>
|
||||||
|
Unknown,
|
||||||
|
|
||||||
/// <summary>The mod is compatible.</summary>
|
/// <summary>The mod is compatible.</summary>
|
||||||
Ok = 0,
|
Ok,
|
||||||
|
|
||||||
/// <summary>The mod is compatible if you use an optional official download.</summary>
|
/// <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>
|
/// <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>
|
/// <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>
|
/// <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>
|
/// <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>
|
/// <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>
|
/// <summary>Special notes intended for developers who maintain unofficial updates or submit pull requests. </summary>
|
||||||
public string DevNote { get; set; }
|
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>
|
/// <summary>Maps local versions to a semantic version for update checks.</summary>
|
||||||
public IDictionary<string, string> MapLocalVersions { get; set; }
|
public IDictionary<string, string> MapLocalVersions { get; set; }
|
||||||
|
|
||||||
|
|
|
@ -124,8 +124,8 @@ namespace StardewModdingAPI.Toolkit.Framework.GameScanning
|
||||||
XElement root;
|
XElement root;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using (FileStream stream = file.OpenRead())
|
using FileStream stream = file.OpenRead();
|
||||||
root = XElement.Load(stream);
|
root = XElement.Load(stream);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
|
|
|
@ -22,7 +22,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning
|
||||||
{
|
{
|
||||||
// OS metadata files
|
// OS metadata files
|
||||||
new Regex(@"^__folder_managed_by_vortex$", RegexOptions.Compiled | RegexOptions.IgnoreCase), // Vortex mod manager
|
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(@"^(?:desktop\.ini|Thumbs\.db)$", RegexOptions.Compiled | RegexOptions.IgnoreCase), // Windows
|
||||||
new Regex(@"\.(?:url|lnk)$", RegexOptions.Compiled | RegexOptions.IgnoreCase), // Windows shortcut files
|
new Regex(@"\.(?:url|lnk)$", RegexOptions.Compiled | RegexOptions.IgnoreCase), // Windows shortcut files
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
namespace StardewModdingAPI.Toolkit.Framework.UpdateData
|
namespace StardewModdingAPI.Toolkit.Framework.UpdateData
|
||||||
{
|
{
|
||||||
/// <summary>A mod repository which SMAPI can check for updates.</summary>
|
/// <summary>A mod site which SMAPI can check for updates.</summary>
|
||||||
public enum ModRepositoryKey
|
public enum ModSiteKey
|
||||||
{
|
{
|
||||||
/// <summary>An unknown or invalid mod repository.</summary>
|
/// <summary>An unknown or invalid mod repository.</summary>
|
||||||
Unknown,
|
Unknown,
|
|
@ -11,12 +11,15 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData
|
||||||
/// <summary>The raw update key text.</summary>
|
/// <summary>The raw update key text.</summary>
|
||||||
public string RawText { get; }
|
public string RawText { get; }
|
||||||
|
|
||||||
/// <summary>The mod repository containing the mod.</summary>
|
/// <summary>The mod site containing the mod.</summary>
|
||||||
public ModRepositoryKey Repository { get; }
|
public ModSiteKey Site { get; }
|
||||||
|
|
||||||
/// <summary>The mod ID within the repository.</summary>
|
/// <summary>The mod ID within the repository.</summary>
|
||||||
public string ID { get; }
|
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>
|
/// <summary>Whether the update key seems to be valid.</summary>
|
||||||
public bool LooksValid { get; }
|
public bool LooksValid { get; }
|
||||||
|
|
||||||
|
@ -26,53 +29,71 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData
|
||||||
*********/
|
*********/
|
||||||
/// <summary>Construct an instance.</summary>
|
/// <summary>Construct an instance.</summary>
|
||||||
/// <param name="rawText">The raw update key text.</param>
|
/// <param name="rawText">The raw update key text.</param>
|
||||||
/// <param name="repository">The mod repository containing the mod.</param>
|
/// <param name="site">The mod site containing the mod.</param>
|
||||||
/// <param name="id">The mod ID within the repository.</param>
|
/// <param name="id">The mod ID within the site.</param>
|
||||||
public UpdateKey(string rawText, ModRepositoryKey repository, string id)
|
/// <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.RawText = rawText?.Trim();
|
||||||
this.Repository = repository;
|
this.Site = site;
|
||||||
this.ID = id;
|
this.ID = id?.Trim();
|
||||||
|
this.Subkey = subkey?.Trim();
|
||||||
this.LooksValid =
|
this.LooksValid =
|
||||||
repository != ModRepositoryKey.Unknown
|
site != ModSiteKey.Unknown
|
||||||
&& !string.IsNullOrWhiteSpace(id);
|
&& !string.IsNullOrWhiteSpace(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Construct an instance.</summary>
|
/// <summary>Construct an instance.</summary>
|
||||||
/// <param name="repository">The mod repository containing the mod.</param>
|
/// <param name="site">The mod site containing the mod.</param>
|
||||||
/// <param name="id">The mod ID within the repository.</param>
|
/// <param name="id">The mod ID within the site.</param>
|
||||||
public UpdateKey(ModRepositoryKey repository, string id)
|
/// <param name="subkey">If specified, a substring in download names/descriptions to match.</param>
|
||||||
: this($"{repository}:{id}", repository, id) { }
|
public UpdateKey(ModSiteKey site, string id, string subkey)
|
||||||
|
: this(UpdateKey.GetString(site, id, subkey), site, id, subkey) { }
|
||||||
|
|
||||||
/// <summary>Parse a raw update key.</summary>
|
/// <summary>Parse a raw update key.</summary>
|
||||||
/// <param name="raw">The raw update key to parse.</param>
|
/// <param name="raw">The raw update key to parse.</param>
|
||||||
public static UpdateKey Parse(string raw)
|
public static UpdateKey Parse(string raw)
|
||||||
{
|
{
|
||||||
// split parts
|
// extract site + ID
|
||||||
string[] parts = raw?.Split(':');
|
string rawSite;
|
||||||
if (parts == null || parts.Length != 2)
|
string id;
|
||||||
return new UpdateKey(raw, ModRepositoryKey.Unknown, null);
|
{
|
||||||
|
string[] parts = raw?.Trim().Split(':');
|
||||||
|
if (parts == null || parts.Length != 2)
|
||||||
|
return new UpdateKey(raw, ModSiteKey.Unknown, null, null);
|
||||||
|
|
||||||
// extract parts
|
rawSite = parts[0].Trim();
|
||||||
string repositoryKey = parts[0].Trim();
|
id = parts[1].Trim();
|
||||||
string id = parts[1].Trim();
|
}
|
||||||
if (string.IsNullOrWhiteSpace(id))
|
if (string.IsNullOrWhiteSpace(id))
|
||||||
id = null;
|
id = null;
|
||||||
|
|
||||||
// parse
|
// extract subkey
|
||||||
if (!Enum.TryParse(repositoryKey, true, out ModRepositoryKey repository))
|
string subkey = null;
|
||||||
return new UpdateKey(raw, ModRepositoryKey.Unknown, id);
|
if (id != null)
|
||||||
if (id == null)
|
{
|
||||||
return new UpdateKey(raw, repository, 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>
|
/// <summary>Get a string that represents the current object.</summary>
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return this.LooksValid
|
return this.LooksValid
|
||||||
? $"{this.Repository}:{this.ID}"
|
? UpdateKey.GetString(this.Site, this.ID, this.Subkey)
|
||||||
: this.RawText;
|
: this.RawText;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,10 +101,18 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData
|
||||||
/// <param name="other">An object to compare with this object.</param>
|
/// <param name="other">An object to compare with this object.</param>
|
||||||
public bool Equals(UpdateKey other)
|
public bool Equals(UpdateKey other)
|
||||||
{
|
{
|
||||||
|
if (!this.LooksValid)
|
||||||
|
{
|
||||||
|
return
|
||||||
|
other?.LooksValid == false
|
||||||
|
&& this.RawText.Equals(other.RawText, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
other != null
|
other != null
|
||||||
&& this.Repository == other.Repository
|
&& this.Site == other.Site
|
||||||
&& string.Equals(this.ID, other.ID, StringComparison.InvariantCultureIgnoreCase);
|
&& 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>
|
/// <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>
|
/// <returns>A hash code for the current object.</returns>
|
||||||
public override int GetHashCode()
|
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.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Runtime.CompilerServices;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using StardewModdingAPI.Toolkit.Framework.Clients.Wiki;
|
using StardewModdingAPI.Toolkit.Framework.Clients.Wiki;
|
||||||
|
@ -11,8 +10,6 @@ using StardewModdingAPI.Toolkit.Framework.ModData;
|
||||||
using StardewModdingAPI.Toolkit.Framework.ModScanning;
|
using StardewModdingAPI.Toolkit.Framework.ModScanning;
|
||||||
using StardewModdingAPI.Toolkit.Serialization;
|
using StardewModdingAPI.Toolkit.Serialization;
|
||||||
|
|
||||||
[assembly: InternalsVisibleTo("StardewModdingAPI")]
|
|
||||||
[assembly: InternalsVisibleTo("SMAPI.Web")]
|
|
||||||
namespace StardewModdingAPI.Toolkit
|
namespace StardewModdingAPI.Toolkit
|
||||||
{
|
{
|
||||||
/// <summary>A convenience wrapper for the various tools.</summary>
|
/// <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">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<AssemblyName>SMAPI.Toolkit</AssemblyName>
|
|
||||||
<RootNamespace>StardewModdingAPI.Toolkit</RootNamespace>
|
<RootNamespace>StardewModdingAPI.Toolkit</RootNamespace>
|
||||||
<Description>A library which encapsulates mod-handling logic for mod managers and tools. Not intended for use by mods.</Description>
|
<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>
|
<TargetFrameworks>net4.5;netstandard2.0</TargetFrameworks>
|
||||||
<LangVersion>latest</LangVersion>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\SMAPI.Toolkit.xml</DocumentationFile>
|
|
||||||
<PlatformTarget Condition="'$(TargetFramework)' == 'net4.5'">x86</PlatformTarget>
|
<PlatformTarget Condition="'$(TargetFramework)' == 'net4.5'">x86</PlatformTarget>
|
||||||
<RootNamespace>StardewModdingAPI.Toolkit</RootNamespace>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.23" />
|
<PackageReference Include="HtmlAgilityPack" Version="1.11.23" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
<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="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'" />
|
<PackageReference Include="Microsoft.Win32.Registry" Version="4.5.0" Condition="'$(OS)' == 'Windows_NT' AND '$(TargetFramework)' == 'netstandard2.0'" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
@ -24,5 +20,4 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<Import Project="..\..\build\common.targets" />
|
<Import Project="..\..\build\common.targets" />
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
@ -30,10 +30,7 @@ namespace StardewModdingAPI.Toolkit.Utilities
|
||||||
/// <summary>Detect the current OS.</summary>
|
/// <summary>Detect the current OS.</summary>
|
||||||
public static Platform DetectPlatform()
|
public static Platform DetectPlatform()
|
||||||
{
|
{
|
||||||
if (EnvironmentUtility.CachedPlatform == null)
|
return EnvironmentUtility.CachedPlatform ??= EnvironmentUtility.DetectPlatformImpl();
|
||||||
EnvironmentUtility.CachedPlatform = EnvironmentUtility.DetectPlatformImpl();
|
|
||||||
|
|
||||||
return EnvironmentUtility.CachedPlatform.Value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Hangfire;
|
using Hangfire;
|
||||||
|
@ -36,7 +37,9 @@ namespace StardewModdingAPI.Web
|
||||||
/// <summary>Construct an instance.</summary>
|
/// <summary>Construct an instance.</summary>
|
||||||
/// <param name="wikiCache">The cache in which to store wiki metadata.</param>
|
/// <param name="wikiCache">The cache in which to store wiki metadata.</param>
|
||||||
/// <param name="modCache">The cache in which to store mod data.</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.WikiCache = wikiCache;
|
||||||
BackgroundService.ModCache = modCache;
|
BackgroundService.ModCache = modCache;
|
||||||
|
@ -81,7 +84,7 @@ namespace StardewModdingAPI.Web
|
||||||
public static async Task UpdateWikiAsync()
|
public static async Task UpdateWikiAsync()
|
||||||
{
|
{
|
||||||
WikiModList wikiCompatList = await new ModToolkit().GetWikiCompatibilityListAsync();
|
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>
|
/// <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>
|
private readonly IDictionary<string, string> SchemaFormats = new Dictionary<string, string>
|
||||||
{
|
{
|
||||||
["none"] = "None",
|
["none"] = "None",
|
||||||
["manifest"] = "Manifest",
|
["manifest"] = "SMAPI: manifest",
|
||||||
|
["i18n"] = "SMAPI: translations (i18n)",
|
||||||
["content-patcher"] = "Content Patcher"
|
["content-patcher"] = "Content Patcher"
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>The schema ID to use if none was specified.</summary>
|
/// <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>
|
/// <summary>A token in an error message which indicates that the child errors should be displayed instead.</summary>
|
||||||
private readonly string TransparentToken = "$transparent";
|
private readonly string TransparentToken = "$transparent";
|
||||||
|
@ -57,16 +58,22 @@ namespace StardewModdingAPI.Web.Controllers
|
||||||
/// <summary>Render the schema validator UI.</summary>
|
/// <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="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="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]
|
[HttpGet]
|
||||||
[Route("json")]
|
[Route("json")]
|
||||||
[Route("json/{schemaName}")]
|
[Route("json/{schemaName}")]
|
||||||
[Route("json/{schemaName}/{id}")]
|
[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);
|
schemaName = this.NormalizeSchemaName(schemaName);
|
||||||
|
bool hasId = !string.IsNullOrWhiteSpace(id);
|
||||||
|
bool isEditView = !hasId || operation?.Trim().ToLower() == "edit";
|
||||||
|
|
||||||
var result = new JsonValidatorModel(id, schemaName, this.SchemaFormats);
|
// build result model
|
||||||
if (string.IsNullOrWhiteSpace(id))
|
var result = this.GetModel(id, schemaName, isEditView);
|
||||||
|
if (!hasId)
|
||||||
return this.View("Index", result);
|
return this.View("Index", result);
|
||||||
|
|
||||||
// fetch raw JSON
|
// fetch raw JSON
|
||||||
|
@ -76,7 +83,7 @@ namespace StardewModdingAPI.Web.Controllers
|
||||||
result.SetContent(file.Content, expiry: file.Expiry, uploadWarning: file.Warning);
|
result.SetContent(file.Content, expiry: file.Expiry, uploadWarning: file.Warning);
|
||||||
|
|
||||||
// skip parsing if we're going to the edit screen
|
// skip parsing if we're going to the edit screen
|
||||||
if (schemaName?.ToLower() == "edit")
|
if (isEditView)
|
||||||
return this.View("Index", result);
|
return this.View("Index", result);
|
||||||
|
|
||||||
// parse JSON
|
// parse JSON
|
||||||
|
@ -130,7 +137,7 @@ namespace StardewModdingAPI.Web.Controllers
|
||||||
public async Task<ActionResult> PostAsync(JsonValidatorRequestModel request)
|
public async Task<ActionResult> PostAsync(JsonValidatorRequestModel request)
|
||||||
{
|
{
|
||||||
if (request == null)
|
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
|
// normalize schema name
|
||||||
string schemaName = this.NormalizeSchemaName(request.SchemaName);
|
string schemaName = this.NormalizeSchemaName(request.SchemaName);
|
||||||
|
@ -138,12 +145,12 @@ namespace StardewModdingAPI.Web.Controllers
|
||||||
// get raw text
|
// get raw text
|
||||||
string input = request.Content;
|
string input = request.Content;
|
||||||
if (string.IsNullOrWhiteSpace(input))
|
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
|
// upload file
|
||||||
UploadResult result = await this.Storage.SaveAsync(input);
|
UploadResult result = await this.Storage.SaveAsync(input);
|
||||||
if (!result.Succeeded)
|
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
|
// redirect to view
|
||||||
return this.Redirect(this.Url.PlainAction("Index", "JsonValidator", new { schemaName = schemaName, id = result.ID }));
|
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>
|
/// <summary>Build a JSON validator model.</summary>
|
||||||
/// <param name="pasteID">The stored file ID.</param>
|
/// <param name="pasteID">The stored file ID.</param>
|
||||||
/// <param name="schemaName">The schema name with which the JSON was validated.</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>
|
/// <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);
|
errors = new Dictionary<string, string>(errors, StringComparer.InvariantCultureIgnoreCase);
|
||||||
|
|
||||||
// match error by type and message
|
// 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;
|
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]))
|
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
|
// match by type
|
||||||
if (errors.TryGetValue(error.ErrorType.ToString(), out string message))
|
return errors.TryGetValue(error.ErrorType.ToString(), out string message)
|
||||||
return message?.Trim();
|
? message?.Trim()
|
||||||
|
: null;
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return GetRawOverrideError()
|
return GetRawOverrideError()
|
||||||
|
@ -304,10 +311,10 @@ namespace StardewModdingAPI.Web.Controllers
|
||||||
{
|
{
|
||||||
if (schema.ExtensionData != null)
|
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))
|
if (curKey.Equals(key, StringComparison.InvariantCultureIgnoreCase))
|
||||||
return pair.Value.ToObject<T>();
|
return value.ToObject<T>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -318,14 +325,11 @@ namespace StardewModdingAPI.Web.Controllers
|
||||||
/// <param name="value">The value to format.</param>
|
/// <param name="value">The value to format.</param>
|
||||||
private string FormatValue(object value)
|
private string FormatValue(object value)
|
||||||
{
|
{
|
||||||
switch (value)
|
return value switch
|
||||||
{
|
{
|
||||||
case List<string> list:
|
List<string> list => string.Join(", ", list),
|
||||||
return string.Join(", ", list);
|
_ => value?.ToString() ?? "null"
|
||||||
|
};
|
||||||
default:
|
|
||||||
return value?.ToString() ?? "null";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,15 +12,16 @@ using StardewModdingAPI.Toolkit.Framework.Clients.Wiki;
|
||||||
using StardewModdingAPI.Toolkit.Framework.ModData;
|
using StardewModdingAPI.Toolkit.Framework.ModData;
|
||||||
using StardewModdingAPI.Toolkit.Framework.UpdateData;
|
using StardewModdingAPI.Toolkit.Framework.UpdateData;
|
||||||
using StardewModdingAPI.Web.Framework;
|
using StardewModdingAPI.Web.Framework;
|
||||||
|
using StardewModdingAPI.Web.Framework.Caching;
|
||||||
using StardewModdingAPI.Web.Framework.Caching.Mods;
|
using StardewModdingAPI.Web.Framework.Caching.Mods;
|
||||||
using StardewModdingAPI.Web.Framework.Caching.Wiki;
|
using StardewModdingAPI.Web.Framework.Caching.Wiki;
|
||||||
|
using StardewModdingAPI.Web.Framework.Clients;
|
||||||
using StardewModdingAPI.Web.Framework.Clients.Chucklefish;
|
using StardewModdingAPI.Web.Framework.Clients.Chucklefish;
|
||||||
using StardewModdingAPI.Web.Framework.Clients.CurseForge;
|
using StardewModdingAPI.Web.Framework.Clients.CurseForge;
|
||||||
using StardewModdingAPI.Web.Framework.Clients.GitHub;
|
using StardewModdingAPI.Web.Framework.Clients.GitHub;
|
||||||
using StardewModdingAPI.Web.Framework.Clients.ModDrop;
|
using StardewModdingAPI.Web.Framework.Clients.ModDrop;
|
||||||
using StardewModdingAPI.Web.Framework.Clients.Nexus;
|
using StardewModdingAPI.Web.Framework.Clients.Nexus;
|
||||||
using StardewModdingAPI.Web.Framework.ConfigModels;
|
using StardewModdingAPI.Web.Framework.ConfigModels;
|
||||||
using StardewModdingAPI.Web.Framework.ModRepositories;
|
|
||||||
|
|
||||||
namespace StardewModdingAPI.Web.Controllers
|
namespace StardewModdingAPI.Web.Controllers
|
||||||
{
|
{
|
||||||
|
@ -32,8 +33,8 @@ namespace StardewModdingAPI.Web.Controllers
|
||||||
/*********
|
/*********
|
||||||
** Fields
|
** Fields
|
||||||
*********/
|
*********/
|
||||||
/// <summary>The mod repositories which provide mod metadata.</summary>
|
/// <summary>The mod sites which provide mod metadata.</summary>
|
||||||
private readonly IDictionary<ModRepositoryKey, IModRepository> Repositories;
|
private readonly ModSiteManager ModSites;
|
||||||
|
|
||||||
/// <summary>The cache in which to store wiki data.</summary>
|
/// <summary>The cache in which to store wiki data.</summary>
|
||||||
private readonly IWikiCacheRepository WikiCache;
|
private readonly IWikiCacheRepository WikiCache;
|
||||||
|
@ -61,23 +62,14 @@ namespace StardewModdingAPI.Web.Controllers
|
||||||
/// <param name="github">The GitHub API client.</param>
|
/// <param name="github">The GitHub API client.</param>
|
||||||
/// <param name="modDrop">The ModDrop API client.</param>
|
/// <param name="modDrop">The ModDrop API client.</param>
|
||||||
/// <param name="nexus">The Nexus 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.ModDatabase = new ModToolkit().GetModDatabase(Path.Combine(environment.WebRootPath, "SMAPI.metadata.json"));
|
||||||
|
|
||||||
this.WikiCache = wikiCache;
|
this.WikiCache = wikiCache;
|
||||||
this.ModCache = modCache;
|
this.ModCache = modCache;
|
||||||
this.Config = config;
|
this.Config = config;
|
||||||
this.Repositories =
|
this.ModSites = new ModSiteManager(new IModSiteClient[] { chucklefish, curseForge, github, modDrop, nexus });
|
||||||
new IModRepository[]
|
|
||||||
{
|
|
||||||
new ChucklefishRepository(chucklefish),
|
|
||||||
new CurseForgeRepository(curseForge),
|
|
||||||
new GitHubRepository(github),
|
|
||||||
new ModDropRepository(modDrop),
|
|
||||||
new NexusRepository(nexus)
|
|
||||||
}
|
|
||||||
.ToDictionary(p => p.VendorKey);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Fetch version metadata for the given mods.</summary>
|
/// <summary>Fetch version metadata for the given mods.</summary>
|
||||||
|
@ -90,7 +82,7 @@ namespace StardewModdingAPI.Web.Controllers
|
||||||
return new ModEntryModel[0];
|
return new ModEntryModel[0];
|
||||||
|
|
||||||
// fetch wiki data
|
// 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);
|
IDictionary<string, ModEntryModel> mods = new Dictionary<string, ModEntryModel>(StringComparer.CurrentCultureIgnoreCase);
|
||||||
foreach (ModSearchEntryModel mod in model.Mods)
|
foreach (ModSearchEntryModel mod in model.Mods)
|
||||||
{
|
{
|
||||||
|
@ -143,45 +135,23 @@ namespace StardewModdingAPI.Web.Controllers
|
||||||
// validate update key
|
// validate update key
|
||||||
if (!updateKey.LooksValid)
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetch data
|
// fetch data
|
||||||
ModInfoModel data = await this.GetInfoForUpdateKeyAsync(updateKey, allowNonStandardVersions);
|
ModInfoModel data = await this.GetInfoForUpdateKeyAsync(updateKey, allowNonStandardVersions, wikiEntry?.MapRemoteVersions);
|
||||||
if (data.Error != null)
|
if (data.Status != RemoteModStatus.Ok)
|
||||||
{
|
{
|
||||||
errors.Add(data.Error);
|
errors.Add(data.Error ?? data.Status.ToString());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle main version
|
// handle versions
|
||||||
if (data.Version != null)
|
if (this.IsNewer(data.Version, main?.Version))
|
||||||
{
|
main = new ModEntryVersionModel(data.Version, data.Url);
|
||||||
ISemanticVersion version = this.GetMappedVersion(data.Version, wikiEntry?.MapRemoteVersions, allowNonStandardVersions);
|
if (this.IsNewer(data.PreviewVersion, optional?.Version))
|
||||||
if (version == null)
|
optional = new ModEntryVersionModel(data.PreviewVersion, data.Url);
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// get unofficial version
|
// get unofficial version
|
||||||
|
@ -221,7 +191,7 @@ namespace StardewModdingAPI.Web.Controllers
|
||||||
}
|
}
|
||||||
|
|
||||||
// get recommended update (if any)
|
// 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)
|
if (apiVersion != null && installedVersion != null)
|
||||||
{
|
{
|
||||||
// get newer versions
|
// get newer versions
|
||||||
|
@ -281,29 +251,27 @@ namespace StardewModdingAPI.Web.Controllers
|
||||||
/// <summary>Get the mod info for an update key.</summary>
|
/// <summary>Get the mod info for an update key.</summary>
|
||||||
/// <param name="updateKey">The namespaced update key.</param>
|
/// <param name="updateKey">The namespaced update key.</param>
|
||||||
/// <param name="allowNonStandardVersions">Whether to allow non-standard versions.</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
|
// get mod page
|
||||||
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))
|
IModPage page;
|
||||||
{
|
{
|
||||||
// get site
|
bool isCached =
|
||||||
if (!this.Repositories.TryGetValue(updateKey.Repository, out IModRepository repository))
|
this.ModCache.TryGetMod(updateKey.Site, updateKey.ID, out Cached<IModPage> cachedMod)
|
||||||
return new ModInfoModel().SetError(RemoteModStatus.DoesNotExist, $"There's no mod site with key '{updateKey.Repository}'. Expected one of [{string.Join(", ", this.Repositories.Keys)}].");
|
&& !this.ModCache.IsStale(cachedMod.LastUpdated, cachedMod.Data.Status == RemoteModStatus.TemporaryError ? this.Config.Value.ErrorCacheMinutes : this.Config.Value.SuccessCacheMinutes);
|
||||||
|
|
||||||
// fetch mod
|
if (isCached)
|
||||||
ModInfoModel result = await repository.GetModInfoAsync(updateKey.ID);
|
page = cachedMod.Data;
|
||||||
if (result.Error == null)
|
else
|
||||||
{
|
{
|
||||||
if (result.Version == null)
|
page = await this.ModSites.GetModPageAsync(updateKey);
|
||||||
result.SetError(RemoteModStatus.InvalidData, $"The update key '{updateKey}' matches a mod with no version number.");
|
this.ModCache.SaveMod(updateKey.Site, updateKey.ID, page);
|
||||||
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}'.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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>
|
/// <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>
|
/// <param name="entry">The mod's entry in the wiki list.</param>
|
||||||
private IEnumerable<UpdateKey> GetUpdateKeys(string[] specifiedKeys, ModDataRecord record, WikiModEntry entry)
|
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
|
var removeKeys = new HashSet<UpdateKey>(
|
||||||
if (specifiedKeys != null)
|
from key in entry?.ChangeUpdateKeys ?? new string[0]
|
||||||
{
|
where key.StartsWith('-')
|
||||||
foreach (string key in specifiedKeys)
|
select UpdateKey.Parse(key.Substring(1))
|
||||||
yield return key?.Trim();
|
);
|
||||||
}
|
if (removeKeys.Any())
|
||||||
|
updateKeys.RemoveAll(removeKeys.Contains);
|
||||||
// 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}";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
HashSet<UpdateKey> seen = new HashSet<UpdateKey>();
|
// 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
|
||||||
foreach (string rawKey in GetRaw())
|
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(rawKey))
|
var removeKeys = new HashSet<UpdateKey>();
|
||||||
continue;
|
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);
|
return updateKeys;
|
||||||
if (seen.Add(key))
|
}
|
||||||
|
|
||||||
|
/// <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;
|
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 System.Text.RegularExpressions;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
using StardewModdingAPI.Web.Framework.Caching;
|
||||||
using StardewModdingAPI.Web.Framework.Caching.Wiki;
|
using StardewModdingAPI.Web.Framework.Caching.Wiki;
|
||||||
using StardewModdingAPI.Web.Framework.ConfigModels;
|
using StardewModdingAPI.Web.Framework.ConfigModels;
|
||||||
using StardewModdingAPI.Web.ViewModels;
|
using StardewModdingAPI.Web.ViewModels;
|
||||||
|
@ -51,16 +52,16 @@ namespace StardewModdingAPI.Web.Controllers
|
||||||
public ModListModel FetchData()
|
public ModListModel FetchData()
|
||||||
{
|
{
|
||||||
// fetch cached data
|
// fetch cached data
|
||||||
if (!this.Cache.TryGetWikiMetadata(out CachedWikiMetadata metadata))
|
if (!this.Cache.TryGetWikiMetadata(out Cached<WikiMetadata> metadata))
|
||||||
return new ModListModel();
|
return new ModListModel();
|
||||||
|
|
||||||
// build model
|
// build model
|
||||||
return new ModListModel(
|
return new ModListModel(
|
||||||
stableVersion: metadata.StableVersion,
|
stableVersion: metadata.Data.StableVersion,
|
||||||
betaVersion: metadata.BetaVersion,
|
betaVersion: metadata.Data.BetaVersion,
|
||||||
mods: this.Cache
|
mods: this.Cache
|
||||||
.GetWikiMods()
|
.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
|
.OrderBy(p => Regex.Replace(p.Name.ToLower(), "[^a-z0-9]", "")), // ignore case, spaces, and special characters when sorting
|
||||||
lastUpdated: metadata.LastUpdated,
|
lastUpdated: metadata.LastUpdated,
|
||||||
isStale: this.Cache.IsStale(metadata.LastUpdated, this.StaleMinutes)
|
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 System;
|
||||||
using StardewModdingAPI.Toolkit.Framework.UpdateData;
|
using StardewModdingAPI.Toolkit.Framework.UpdateData;
|
||||||
using StardewModdingAPI.Web.Framework.ModRepositories;
|
using StardewModdingAPI.Web.Framework.Clients;
|
||||||
|
|
||||||
namespace StardewModdingAPI.Web.Framework.Caching.Mods
|
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
|
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="id">The mod's unique ID within the <paramref name="site"/>.</param>
|
||||||
/// <param name="mod">The fetched mod.</param>
|
/// <param name="mod">The fetched mod.</param>
|
||||||
/// <param name="markRequested">Whether to update the mod's 'last requested' date.</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>
|
/// <summary>Save data fetched for a mod.</summary>
|
||||||
/// <param name="site">The mod site on which the mod is found.</param>
|
/// <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="id">The mod's unique ID within the <paramref name="site"/>.</param>
|
||||||
/// <param name="mod">The mod data.</param>
|
/// <param name="mod">The mod data.</param>
|
||||||
/// <param name="cachedMod">The stored mod record.</param>
|
void SaveMod(ModSiteKey site, string id, IModPage mod);
|
||||||
void SaveMod(ModRepositoryKey site, string id, ModInfoModel mod, out CachedMod cachedMod);
|
|
||||||
|
|
||||||
/// <summary>Delete data for mods which haven't been requested within a given time limit.</summary>
|
/// <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>
|
/// <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;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq.Expressions;
|
|
||||||
using StardewModdingAPI.Toolkit.Framework.Clients.Wiki;
|
using StardewModdingAPI.Toolkit.Framework.Clients.Wiki;
|
||||||
|
|
||||||
namespace StardewModdingAPI.Web.Framework.Caching.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
|
internal interface IWikiCacheRepository : ICacheRepository
|
||||||
{
|
{
|
||||||
/*********
|
/*********
|
||||||
|
@ -13,18 +12,16 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki
|
||||||
*********/
|
*********/
|
||||||
/// <summary>Get the cached wiki metadata.</summary>
|
/// <summary>Get the cached wiki metadata.</summary>
|
||||||
/// <param name="metadata">The fetched metadata.</param>
|
/// <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>
|
/// <summary>Get the cached wiki mods.</summary>
|
||||||
/// <param name="filter">A filter to apply, if any.</param>
|
/// <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>
|
/// <summary>Save data fetched from the wiki compatibility list.</summary>
|
||||||
/// <param name="stableVersion">The current stable Stardew Valley version.</param>
|
/// <param name="stableVersion">The current stable Stardew Valley version.</param>
|
||||||
/// <param name="betaVersion">The current beta Stardew Valley version.</param>
|
/// <param name="betaVersion">The current beta Stardew Valley version.</param>
|
||||||
/// <param name="mods">The mod data.</param>
|
/// <param name="mods">The mod data.</param>
|
||||||
/// <param name="cachedMetadata">The stored metadata record.</param>
|
void SaveWikiData(string stableVersion, string betaVersion, IEnumerable<WikiModEntry> mods);
|
||||||
/// <param name="cachedMods">The stored mod records.</param>
|
|
||||||
void SaveWikiData(string stableVersion, string betaVersion, IEnumerable<WikiModEntry> mods, out CachedWikiMetadata cachedMetadata, out CachedWikiMod[] cachedMods);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
namespace StardewModdingAPI.Web.Framework.Caching.Wiki
|
||||||
{
|
{
|
||||||
/// <summary>The model for cached wiki metadata.</summary>
|
/// <summary>The model for cached wiki metadata.</summary>
|
||||||
internal class CachedWikiMetadata
|
internal class WikiMetadata
|
||||||
{
|
{
|
||||||
/*********
|
/*********
|
||||||
** Accessors
|
** 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>
|
/// <summary>The current stable Stardew Valley version.</summary>
|
||||||
public string StableVersion { get; set; }
|
public string StableVersion { get; set; }
|
||||||
|
|
||||||
|
@ -28,16 +17,15 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki
|
||||||
** Public methods
|
** Public methods
|
||||||
*********/
|
*********/
|
||||||
/// <summary>Construct an instance.</summary>
|
/// <summary>Construct an instance.</summary>
|
||||||
public CachedWikiMetadata() { }
|
public WikiMetadata() { }
|
||||||
|
|
||||||
/// <summary>Construct an instance.</summary>
|
/// <summary>Construct an instance.</summary>
|
||||||
/// <param name="stableVersion">The current stable Stardew Valley version.</param>
|
/// <param name="stableVersion">The current stable Stardew Valley version.</param>
|
||||||
/// <param name="betaVersion">The current beta 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.StableVersion = stableVersion;
|
||||||
this.BetaVersion = betaVersion;
|
this.BetaVersion = betaVersion;
|
||||||
this.LastUpdated = DateTimeOffset.UtcNow;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -3,6 +3,7 @@ using System.Net;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using HtmlAgilityPack;
|
using HtmlAgilityPack;
|
||||||
using Pathoschild.Http.Client;
|
using Pathoschild.Http.Client;
|
||||||
|
using StardewModdingAPI.Toolkit.Framework.UpdateData;
|
||||||
|
|
||||||
namespace StardewModdingAPI.Web.Framework.Clients.Chucklefish
|
namespace StardewModdingAPI.Web.Framework.Clients.Chucklefish
|
||||||
{
|
{
|
||||||
|
@ -19,6 +20,13 @@ namespace StardewModdingAPI.Web.Framework.Clients.Chucklefish
|
||||||
private readonly IClient Client;
|
private readonly IClient Client;
|
||||||
|
|
||||||
|
|
||||||
|
/*********
|
||||||
|
** Accessors
|
||||||
|
*********/
|
||||||
|
/// <summary>The unique key for the mod site.</summary>
|
||||||
|
public ModSiteKey SiteKey => ModSiteKey.Chucklefish;
|
||||||
|
|
||||||
|
|
||||||
/*********
|
/*********
|
||||||
** Public methods
|
** Public methods
|
||||||
*********/
|
*********/
|
||||||
|
@ -32,42 +40,40 @@ namespace StardewModdingAPI.Web.Framework.Clients.Chucklefish
|
||||||
this.Client = new FluentClient(baseUrl).SetUserAgent(userAgent);
|
this.Client = new FluentClient(baseUrl).SetUserAgent(userAgent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Get metadata about a mod.</summary>
|
/// <summary>Get update check info about a mod.</summary>
|
||||||
/// <param name="id">The Chucklefish mod ID.</param>
|
/// <param name="id">The mod ID.</param>
|
||||||
/// <returns>Returns the mod info if found, else <c>null</c>.</returns>
|
public async Task<IModPage> GetModData(string id)
|
||||||
public async Task<ChucklefishMod> GetModAsync(uint 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
|
// fetch HTML
|
||||||
string html;
|
string html;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
html = await this.Client
|
html = await this.Client
|
||||||
.GetAsync(string.Format(this.ModPageUrlFormat, id))
|
.GetAsync(string.Format(this.ModPageUrlFormat, parsedId))
|
||||||
.AsString();
|
.AsString();
|
||||||
}
|
}
|
||||||
catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound || ex.Status == HttpStatusCode.Forbidden)
|
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();
|
var doc = new HtmlDocument();
|
||||||
doc.LoadHtml(html);
|
doc.LoadHtml(html);
|
||||||
|
|
||||||
// extract mod info
|
// 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;
|
string name = doc.DocumentNode.SelectSingleNode("//meta[@name='twitter:title']").Attributes["content"].Value;
|
||||||
if (name.StartsWith("[SMAPI] "))
|
if (name.StartsWith("[SMAPI] "))
|
||||||
name = name.Substring("[SMAPI] ".Length);
|
name = name.Substring("[SMAPI] ".Length);
|
||||||
string version = doc.DocumentNode.SelectSingleNode("//h1/span")?.InnerText;
|
string version = doc.DocumentNode.SelectSingleNode("//h1/span")?.InnerText;
|
||||||
|
|
||||||
// create model
|
// return info
|
||||||
return new ChucklefishMod
|
return page.SetInfo(name: name, version: version, url: url, downloads: Array.Empty<IModDownload>());
|
||||||
{
|
|
||||||
Name = name,
|
|
||||||
Version = version,
|
|
||||||
Url = url
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
|
/// <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;
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace StardewModdingAPI.Web.Framework.Clients.Chucklefish
|
namespace StardewModdingAPI.Web.Framework.Clients.Chucklefish
|
||||||
{
|
{
|
||||||
/// <summary>An HTTP client for fetching mod metadata from the Chucklefish mod site.</summary>
|
/// <summary>An HTTP client for fetching mod metadata from the Chucklefish mod site.</summary>
|
||||||
internal interface IChucklefishClient : IDisposable
|
internal interface IChucklefishClient : IModSiteClient, 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
using System.Linq;
|
using System.Collections.Generic;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Pathoschild.Http.Client;
|
using Pathoschild.Http.Client;
|
||||||
using StardewModdingAPI.Toolkit;
|
using StardewModdingAPI.Toolkit.Framework.UpdateData;
|
||||||
using StardewModdingAPI.Web.Framework.Clients.CurseForge.ResponseModels;
|
using StardewModdingAPI.Web.Framework.Clients.CurseForge.ResponseModels;
|
||||||
|
|
||||||
namespace StardewModdingAPI.Web.Framework.Clients.CurseForge
|
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);
|
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
|
** Public methods
|
||||||
*********/
|
*********/
|
||||||
|
@ -31,60 +38,34 @@ namespace StardewModdingAPI.Web.Framework.Clients.CurseForge
|
||||||
this.Client = new FluentClient(apiUrl).SetUserAgent(userAgent);
|
this.Client = new FluentClient(apiUrl).SetUserAgent(userAgent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Get metadata about a mod.</summary>
|
/// <summary>Get update check info about a mod.</summary>
|
||||||
/// <param name="id">The CurseForge mod ID.</param>
|
/// <param name="id">The mod ID.</param>
|
||||||
/// <returns>Returns the mod info if found, else <c>null</c>.</returns>
|
public async Task<IModPage> GetModData(string id)
|
||||||
public async Task<CurseForgeMod> GetModAsync(long 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
|
// get raw data
|
||||||
ModModel mod = await this.Client
|
ModModel mod = await this.Client
|
||||||
.GetAsync($"addon/{id}")
|
.GetAsync($"addon/{parsedId}")
|
||||||
.As<ModModel>();
|
.As<ModModel>();
|
||||||
if (mod == null)
|
if (mod == null)
|
||||||
return null;
|
return page.SetError(RemoteModStatus.DoesNotExist, "Found no CurseForge mod with this ID.");
|
||||||
|
|
||||||
// get latest versions
|
// get downloads
|
||||||
string invalidVersion = null;
|
List<IModDownload> downloads = new List<IModDownload>();
|
||||||
ISemanticVersion latest = null;
|
|
||||||
foreach (ModFileModel file in mod.LatestFiles)
|
foreach (ModFileModel file in mod.LatestFiles)
|
||||||
{
|
{
|
||||||
// extract version
|
downloads.Add(
|
||||||
ISemanticVersion version;
|
new GenericModDownload(name: file.DisplayName ?? file.FileName, description: null, version: this.GetRawVersion(file))
|
||||||
{
|
);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// get error
|
// return info
|
||||||
string error = null;
|
return page.SetInfo(name: mod.Name, version: null, url: mod.WebsiteUrl, downloads: downloads);
|
||||||
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
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
|
/// <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;
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace StardewModdingAPI.Web.Framework.Clients.CurseForge
|
namespace StardewModdingAPI.Web.Framework.Clients.CurseForge
|
||||||
{
|
{
|
||||||
/// <summary>An HTTP client for fetching mod metadata from the CurseForge API.</summary>
|
/// <summary>An HTTP client for fetching mod metadata from the CurseForge API.</summary>
|
||||||
internal interface ICurseForgeClient : IDisposable
|
internal interface ICurseForgeClient : IModSiteClient, 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.Net;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Pathoschild.Http.Client;
|
using Pathoschild.Http.Client;
|
||||||
|
using StardewModdingAPI.Toolkit.Framework.UpdateData;
|
||||||
|
|
||||||
namespace StardewModdingAPI.Web.Framework.Clients.GitHub
|
namespace StardewModdingAPI.Web.Framework.Clients.GitHub
|
||||||
{
|
{
|
||||||
|
@ -16,6 +17,13 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub
|
||||||
private readonly IClient Client;
|
private readonly IClient Client;
|
||||||
|
|
||||||
|
|
||||||
|
/*********
|
||||||
|
** Accessors
|
||||||
|
*********/
|
||||||
|
/// <summary>The unique key for the mod site.</summary>
|
||||||
|
public ModSiteKey SiteKey => ModSiteKey.GitHub;
|
||||||
|
|
||||||
|
|
||||||
/*********
|
/*********
|
||||||
** Public methods
|
** 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>
|
/// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
|
|
|
@ -4,7 +4,7 @@ using System.Threading.Tasks;
|
||||||
namespace StardewModdingAPI.Web.Framework.Clients.GitHub
|
namespace StardewModdingAPI.Web.Framework.Clients.GitHub
|
||||||
{
|
{
|
||||||
/// <summary>An HTTP client for fetching metadata from GitHub.</summary>
|
/// <summary>An HTTP client for fetching metadata from GitHub.</summary>
|
||||||
internal interface IGitHubClient : IDisposable
|
internal interface IGitHubClient : IModSiteClient, IDisposable
|
||||||
{
|
{
|
||||||
/*********
|
/*********
|
||||||
** Methods
|
** 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;
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace StardewModdingAPI.Web.Framework.Clients.ModDrop
|
namespace StardewModdingAPI.Web.Framework.Clients.ModDrop
|
||||||
{
|
{
|
||||||
/// <summary>An HTTP client for fetching mod metadata from the ModDrop API.</summary>
|
/// <summary>An HTTP client for fetching mod metadata from the ModDrop API.</summary>
|
||||||
internal interface IModDropClient : IDisposable
|
internal interface IModDropClient : IDisposable, IModSiteClient { }
|
||||||
{
|
|
||||||
/*********
|
|
||||||
** 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Pathoschild.Http.Client;
|
using Pathoschild.Http.Client;
|
||||||
using StardewModdingAPI.Toolkit;
|
using StardewModdingAPI.Toolkit.Framework.UpdateData;
|
||||||
using StardewModdingAPI.Web.Framework.Clients.ModDrop.ResponseModels;
|
using StardewModdingAPI.Web.Framework.Clients.ModDrop.ResponseModels;
|
||||||
|
|
||||||
namespace StardewModdingAPI.Web.Framework.Clients.ModDrop
|
namespace StardewModdingAPI.Web.Framework.Clients.ModDrop
|
||||||
|
@ -18,6 +19,13 @@ namespace StardewModdingAPI.Web.Framework.Clients.ModDrop
|
||||||
private readonly string ModUrlFormat;
|
private readonly string ModUrlFormat;
|
||||||
|
|
||||||
|
|
||||||
|
/*********
|
||||||
|
** Accessors
|
||||||
|
*********/
|
||||||
|
/// <summary>The unique key for the mod site.</summary>
|
||||||
|
public ModSiteKey SiteKey => ModSiteKey.ModDrop;
|
||||||
|
|
||||||
|
|
||||||
/*********
|
/*********
|
||||||
** Public methods
|
** Public methods
|
||||||
*********/
|
*********/
|
||||||
|
@ -31,60 +39,45 @@ namespace StardewModdingAPI.Web.Framework.Clients.ModDrop
|
||||||
this.ModUrlFormat = modUrlFormat;
|
this.ModUrlFormat = modUrlFormat;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Get metadata about a mod.</summary>
|
/// <summary>Get update check info about a mod.</summary>
|
||||||
/// <param name="id">The ModDrop mod ID.</param>
|
/// <param name="id">The mod ID.</param>
|
||||||
/// <returns>Returns the mod info if found, else <c>null</c>.</returns>
|
public async Task<IModPage> GetModData(string id)
|
||||||
public async Task<ModDropMod> GetModAsync(long 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
|
// get raw data
|
||||||
ModListModel response = await this.Client
|
ModListModel response = await this.Client
|
||||||
.PostAsync("")
|
.PostAsync("")
|
||||||
.WithBody(new
|
.WithBody(new
|
||||||
{
|
{
|
||||||
ModIDs = new[] { id },
|
ModIDs = new[] { parsedId },
|
||||||
Files = true,
|
Files = true,
|
||||||
Mods = true
|
Mods = true
|
||||||
})
|
})
|
||||||
.As<ModListModel>();
|
.As<ModListModel>();
|
||||||
ModModel mod = response.Mods[id];
|
ModModel mod = response.Mods[parsedId];
|
||||||
if (mod.Mod?.Title == null || mod.Mod.ErrorCode.HasValue)
|
if (mod.Mod?.Title == null || mod.Mod.ErrorCode.HasValue)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
// get latest versions
|
// get files
|
||||||
ISemanticVersion latest = null;
|
var downloads = new List<IModDownload>();
|
||||||
ISemanticVersion optional = null;
|
|
||||||
foreach (FileDataModel file in mod.Files)
|
foreach (FileDataModel file in mod.Files)
|
||||||
{
|
{
|
||||||
if (file.IsOld || file.IsDeleted || file.IsHidden)
|
if (file.IsOld || file.IsDeleted || file.IsHidden)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (!SemanticVersion.TryParse(file.Version, out ISemanticVersion version))
|
downloads.Add(
|
||||||
continue;
|
new GenericModDownload(file.Name, file.Description, file.Version)
|
||||||
|
);
|
||||||
if (file.IsDefault)
|
|
||||||
{
|
|
||||||
if (latest == null || version.IsNewerThan(latest))
|
|
||||||
latest = version;
|
|
||||||
}
|
|
||||||
else if (optional == null || version.IsNewerThan(optional))
|
|
||||||
optional = version;
|
|
||||||
}
|
}
|
||||||
if (latest == null)
|
|
||||||
{
|
|
||||||
latest = optional;
|
|
||||||
optional = null;
|
|
||||||
}
|
|
||||||
if (optional != null && latest.IsNewerThan(optional))
|
|
||||||
optional = null;
|
|
||||||
|
|
||||||
// generate result
|
// return info
|
||||||
return new ModDropMod
|
string name = mod.Mod?.Title;
|
||||||
{
|
string url = string.Format(this.ModUrlFormat, id);
|
||||||
Name = mod.Mod?.Title,
|
return page.SetInfo(name: name, version: null, url: url, downloads: downloads);
|
||||||
LatestDefaultVersion = latest,
|
|
||||||
LatestOptionalVersion = optional,
|
|
||||||
Url = string.Format(this.ModUrlFormat, id)
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
|
/// <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
|
namespace StardewModdingAPI.Web.Framework.Clients.ModDrop.ResponseModels
|
||||||
{
|
{
|
||||||
/// <summary>Metadata from the ModDrop API about a mod file.</summary>
|
/// <summary>Metadata from the ModDrop API about a mod file.</summary>
|
||||||
public class FileDataModel
|
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>
|
/// <summary>Whether the file is deleted.</summary>
|
||||||
public bool IsDeleted { get; set; }
|
public bool IsDeleted { get; set; }
|
||||||
|
|
||||||
|
@ -14,8 +27,5 @@ namespace StardewModdingAPI.Web.Framework.Clients.ModDrop.ResponseModels
|
||||||
|
|
||||||
/// <summary>Whether this is an archived file.</summary>
|
/// <summary>Whether this is an archived file.</summary>
|
||||||
public bool IsOld { get; set; }
|
public bool IsOld { get; set; }
|
||||||
|
|
||||||
/// <summary>The file version.</summary>
|
|
||||||
public string Version { get; set; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace StardewModdingAPI.Web.Framework.Clients.Nexus
|
namespace StardewModdingAPI.Web.Framework.Clients.Nexus
|
||||||
{
|
{
|
||||||
/// <summary>An HTTP client for fetching mod metadata from Nexus Mods.</summary>
|
/// <summary>An HTTP client for fetching mod metadata from Nexus Mods.</summary>
|
||||||
internal interface INexusClient : IDisposable
|
internal interface INexusClient : IModSiteClient, 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,8 @@ using HtmlAgilityPack;
|
||||||
using Pathoschild.FluentNexus.Models;
|
using Pathoschild.FluentNexus.Models;
|
||||||
using Pathoschild.Http.Client;
|
using Pathoschild.Http.Client;
|
||||||
using StardewModdingAPI.Toolkit;
|
using StardewModdingAPI.Toolkit;
|
||||||
|
using StardewModdingAPI.Toolkit.Framework.UpdateData;
|
||||||
|
using StardewModdingAPI.Web.Framework.Clients.Nexus.ResponseModels;
|
||||||
using FluentNexusClient = Pathoschild.FluentNexus.NexusClient;
|
using FluentNexusClient = Pathoschild.FluentNexus.NexusClient;
|
||||||
|
|
||||||
namespace StardewModdingAPI.Web.Framework.Clients.Nexus
|
namespace StardewModdingAPI.Web.Framework.Clients.Nexus
|
||||||
|
@ -30,6 +32,13 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus
|
||||||
private readonly FluentNexusClient ApiClient;
|
private readonly FluentNexusClient ApiClient;
|
||||||
|
|
||||||
|
|
||||||
|
/*********
|
||||||
|
** Accessors
|
||||||
|
*********/
|
||||||
|
/// <summary>The unique key for the mod site.</summary>
|
||||||
|
public ModSiteKey SiteKey => ModSiteKey.Nexus;
|
||||||
|
|
||||||
|
|
||||||
/*********
|
/*********
|
||||||
** Public methods
|
** Public methods
|
||||||
*********/
|
*********/
|
||||||
|
@ -48,20 +57,32 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus
|
||||||
this.ApiClient = new FluentNexusClient(apiKey, "SMAPI", apiAppVersion);
|
this.ApiClient = new FluentNexusClient(apiKey, "SMAPI", apiAppVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Get metadata about a mod.</summary>
|
/// <summary>Get update check info about a mod.</summary>
|
||||||
/// <param name="id">The Nexus mod ID.</param>
|
/// <param name="id">The mod ID.</param>
|
||||||
/// <returns>Returns the mod info if found, else <c>null</c>.</returns>
|
public async Task<IModPage> GetModData(string id)
|
||||||
public async Task<NexusMod> GetModAsync(uint 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
|
// 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.
|
// 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
|
// Note that the API has very restrictive rate limits which means we can't just use it
|
||||||
// for all cases.
|
// for all cases.
|
||||||
NexusMod mod = await this.GetModFromWebsiteAsync(id);
|
NexusMod mod = await this.GetModFromWebsiteAsync(parsedId);
|
||||||
if (mod?.Status == NexusModStatus.AdultContentForbidden)
|
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>
|
/// <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
|
// extract mod info
|
||||||
string url = this.GetModUrl(id);
|
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();
|
string version = doc.DocumentNode.SelectSingleNode("//ul[contains(@class, 'stats')]//li[@class='stat-version']//div[@class='stat']")?.InnerText.Trim();
|
||||||
SemanticVersion.TryParse(version, out ISemanticVersion parsedVersion);
|
SemanticVersion.TryParse(version, out ISemanticVersion parsedVersion);
|
||||||
|
|
||||||
// extract file versions
|
// extract files
|
||||||
List<string> rawVersions = new List<string>();
|
var downloads = new List<IModDownload>();
|
||||||
foreach (var fileSection in doc.DocumentNode.SelectNodes("//div[contains(@class, 'files-tabs')]"))
|
foreach (var fileSection in doc.DocumentNode.SelectNodes("//div[contains(@class, 'files-tabs')]"))
|
||||||
{
|
{
|
||||||
string sectionName = fileSection.Descendants("h2").First().InnerText;
|
string sectionName = fileSection.Descendants("h2").First().InnerText;
|
||||||
if (sectionName != "Main files" && sectionName != "Optional files")
|
if (sectionName != "Main files" && sectionName != "Optional files")
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
rawVersions.AddRange(
|
foreach (var container in fileSection.Descendants("dt"))
|
||||||
from statBox in fileSection.Descendants().Where(p => p.HasClass("stat-version"))
|
{
|
||||||
from versionStat in statBox.Descendants().Where(p => p.HasClass("stat"))
|
string fileName = container.GetDataAttribute("name").Value;
|
||||||
select versionStat.InnerText.Trim()
|
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
|
downloads.Add(
|
||||||
ISemanticVersion latestFileVersion = null;
|
new GenericModDownload(fileName, description, fileVersion)
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// yield info
|
// yield info
|
||||||
|
@ -153,8 +165,8 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus
|
||||||
{
|
{
|
||||||
Name = name,
|
Name = name,
|
||||||
Version = parsedVersion?.ToString() ?? version,
|
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);
|
Mod mod = await this.ApiClient.Mods.GetMod("stardewvalley", (int)id);
|
||||||
ModFileList files = await this.ApiClient.ModFiles.GetModFiles("stardewvalley", (int)id, FileCategory.Main, FileCategory.Optional);
|
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
|
// yield info
|
||||||
return new NexusMod
|
return new NexusMod
|
||||||
{
|
{
|
||||||
Name = mod.Name,
|
Name = mod.Name,
|
||||||
Version = SemanticVersion.TryParse(mod.Version, out ISemanticVersion version) ? version?.ToString() : mod.Version,
|
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;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace StardewModdingAPI.Web.Framework.Clients.Nexus
|
namespace StardewModdingAPI.Web.Framework.Clients.Nexus.ResponseModels
|
||||||
{
|
{
|
||||||
/// <summary>Mod metadata from Nexus Mods.</summary>
|
/// <summary>Mod metadata from Nexus Mods.</summary>
|
||||||
internal class NexusMod
|
internal class NexusMod
|
||||||
|
@ -14,9 +14,6 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus
|
||||||
/// <summary>The mod's semantic version number.</summary>
|
/// <summary>The mod's semantic version number.</summary>
|
||||||
public string Version { get; set; }
|
public string Version { get; set; }
|
||||||
|
|
||||||
/// <summary>The latest file version.</summary>
|
|
||||||
public ISemanticVersion LatestFileVersion { get; set; }
|
|
||||||
|
|
||||||
/// <summary>The mod's web URL.</summary>
|
/// <summary>The mod's web URL.</summary>
|
||||||
[JsonProperty("mod_page_uri")]
|
[JsonProperty("mod_page_uri")]
|
||||||
public string Url { get; set; }
|
public string Url { get; set; }
|
||||||
|
@ -25,7 +22,11 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public NexusModStatus Status { get; set; } = NexusModStatus.Ok;
|
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]
|
[JsonIgnore]
|
||||||
public string Error { get; set; }
|
public string Error { get; set; }
|
||||||
}
|
}
|
|
@ -69,7 +69,7 @@ namespace StardewModdingAPI.Web.Framework.Compression
|
||||||
return rawText;
|
return rawText;
|
||||||
|
|
||||||
// decompress
|
// decompress
|
||||||
using (MemoryStream memoryStream = new MemoryStream())
|
using MemoryStream memoryStream = new MemoryStream();
|
||||||
{
|
{
|
||||||
// read length prefix
|
// read length prefix
|
||||||
int dataLength = BitConverter.ToInt32(zipBuffer, 0);
|
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 System;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
|
using Microsoft.AspNetCore.Html;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Razor;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||||
using Microsoft.AspNetCore.Routing;
|
using Microsoft.AspNetCore.Routing;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace StardewModdingAPI.Web.Framework
|
namespace StardewModdingAPI.Web.Framework
|
||||||
{
|
{
|
||||||
/// <summary>Provides extensions on ASP.NET Core types.</summary>
|
/// <summary>Provides extensions on ASP.NET Core types.</summary>
|
||||||
public static class Extensions
|
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>
|
/// <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="helper">The URL helper to extend.</param>
|
||||||
/// <param name="action">The name of the action method.</param>
|
/// <param name="action">The name of the action method.</param>
|
||||||
|
@ -18,6 +28,7 @@ namespace StardewModdingAPI.Web.Framework
|
||||||
/// <returns>The generated URL.</returns>
|
/// <returns>The generated URL.</returns>
|
||||||
public static string PlainAction(this IUrlHelper helper, [AspMvcAction] string action, [AspMvcController] string controller, object values = null, bool absoluteUrl = false)
|
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);
|
RouteValueDictionary valuesDict = new RouteValueDictionary(values);
|
||||||
foreach (var value in helper.ActionContext.RouteData.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
|
valuesDict[value.Key] = null; // explicitly remove it from the URL
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// get relative URL
|
||||||
string url = helper.Action(action, controller, valuesDict);
|
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)
|
if (absoluteUrl)
|
||||||
{
|
{
|
||||||
HttpRequest request = helper.ActionContext.HttpContext.Request;
|
HttpRequest request = helper.ActionContext.HttpContext.Request;
|
||||||
Uri baseUri = new Uri($"{request.Scheme}://{request.Host}");
|
Uri baseUri = new Uri($"{request.Scheme}://{request.Host}");
|
||||||
url = new Uri(baseUri, url).ToString();
|
url = new Uri(baseUri, url).ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
return url;
|
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);
|
private readonly Regex ModUpdateListEntryPattern = new Regex(@"^ (?<name>.+?) (?<version>[^\s]+): (?<link>.+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
/// <summary>A regex pattern matching SMAPI's update line.</summary>
|
/// <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;
|
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 version = match.Groups["version"].Value;
|
||||||
string link = match.Groups["link"].Value;
|
string link = match.Groups["link"].Value;
|
||||||
smapiMod.UpdateVersion = version;
|
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>
|
/// <summary>Generic metadata about a mod.</summary>
|
||||||
internal class ModInfoModel
|
internal class ModInfoModel
|
||||||
|
@ -10,20 +12,14 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
|
|
||||||
/// <summary>The mod's latest version.</summary>
|
/// <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>
|
/// <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>
|
/// <summary>The mod's web URL.</summary>
|
||||||
public string Url { get; set; }
|
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>
|
/// <summary>The mod availability status on the remote site.</summary>
|
||||||
public RemoteModStatus Status { get; set; } = RemoteModStatus.Ok;
|
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="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="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>
|
/// <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
|
this
|
||||||
.SetBasicInfo(name, url)
|
.SetBasicInfo(name, url)
|
||||||
|
@ -63,7 +59,7 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
|
||||||
/// <summary>Set the mod version info.</summary>
|
/// <summary>Set the mod version info.</summary>
|
||||||
/// <param name="version">The semantic version for the mod's latest release.</param>
|
/// <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="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.Version = version;
|
||||||
this.PreviewVersion = previewVersion;
|
this.PreviewVersion = previewVersion;
|
||||||
|
@ -71,17 +67,6 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
|
||||||
return this;
|
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>
|
/// <summary>Set a mod error.</summary>
|
||||||
/// <param name="status">The mod availability status on the remote site.</param>
|
/// <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>
|
/// <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>
|
/// <summary>The mod availability status on a remote site.</summary>
|
||||||
internal enum RemoteModStatus
|
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.AspNetCore.Hosting;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
|
||||||
namespace StardewModdingAPI.Web
|
namespace StardewModdingAPI.Web
|
||||||
{
|
{
|
||||||
|
@ -13,13 +13,13 @@ namespace StardewModdingAPI.Web
|
||||||
/// <param name="args">The command-line arguments.</param>
|
/// <param name="args">The command-line arguments.</param>
|
||||||
public static void Main(string[] args)
|
public static void Main(string[] args)
|
||||||
{
|
{
|
||||||
// configure web server
|
Host
|
||||||
WebHost
|
|
||||||
.CreateDefaultBuilder(args)
|
.CreateDefaultBuilder(args)
|
||||||
.CaptureStartupErrors(true)
|
.ConfigureWebHostDefaults(builder => builder
|
||||||
.UseSetting("detailedErrors", "true")
|
.CaptureStartupErrors(true)
|
||||||
.UseKestrel().UseIISIntegration() // must be used together; fixes intermittent errors on Azure: https://stackoverflow.com/a/38312175/262123
|
.UseSetting("detailedErrors", "true")
|
||||||
.UseStartup<Startup>()
|
.UseStartup<Startup>()
|
||||||
|
)
|
||||||
.Build()
|
.Build()
|
||||||
.Run();
|
.Run();
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<AssemblyName>SMAPI.Web</AssemblyName>
|
<AssemblyName>SMAPI.Web</AssemblyName>
|
||||||
<RootNamespace>StardewModdingAPI.Web</RootNamespace>
|
<RootNamespace>StardewModdingAPI.Web</RootNamespace>
|
||||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||||
<LangVersion>latest</LangVersion>
|
<LangVersion>latest</LangVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
@ -12,23 +12,17 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Azure.Storage.Blobs" Version="12.4.0" />
|
<PackageReference Include="Azure.Storage.Blobs" Version="12.4.2" />
|
||||||
<PackageReference Include="Hangfire.AspNetCore" Version="1.7.9" />
|
<PackageReference Include="Hangfire.AspNetCore" Version="1.7.11" />
|
||||||
<PackageReference Include="Hangfire.MemoryStorage" Version="1.7.0" />
|
<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="HtmlAgilityPack" Version="1.11.23" />
|
||||||
<PackageReference Include="Humanizer.Core" Version="2.7.9" />
|
<PackageReference Include="Humanizer.Core" Version="2.8.11" />
|
||||||
<PackageReference Include="JetBrains.Annotations" Version="2019.1.3" />
|
<PackageReference Include="JetBrains.Annotations" Version="2020.1.0" />
|
||||||
<PackageReference Include="Markdig" Version="0.18.3" />
|
<PackageReference Include="Markdig" Version="0.20.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore" Version="2.2.0" />
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.0.2" />
|
||||||
<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="Newtonsoft.Json.Schema" Version="3.0.13" />
|
<PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.13" />
|
||||||
<PackageReference Include="Pathoschild.FluentNexus" Version="1.0.0" />
|
<PackageReference Include="Pathoschild.FluentNexus" Version="1.0.1" />
|
||||||
<PackageReference Include="Pathoschild.Http.FluentClient" Version="3.3.1" />
|
<PackageReference Include="Pathoschild.Http.FluentClient" Version="4.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="1.0.1" />
|
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="1.0.1" />
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Net;
|
||||||
using Hangfire;
|
using Hangfire;
|
||||||
using Hangfire.MemoryStorage;
|
using Hangfire.MemoryStorage;
|
||||||
using Hangfire.Mongo;
|
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.AspNetCore.Rewrite;
|
using Microsoft.AspNetCore.Rewrite;
|
||||||
|
@ -10,13 +9,9 @@ using Microsoft.AspNetCore.Routing;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Mongo2Go;
|
|
||||||
using MongoDB.Bson.Serialization;
|
|
||||||
using MongoDB.Driver;
|
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using StardewModdingAPI.Toolkit.Serialization;
|
using StardewModdingAPI.Toolkit.Serialization;
|
||||||
using StardewModdingAPI.Web.Framework;
|
using StardewModdingAPI.Web.Framework;
|
||||||
using StardewModdingAPI.Web.Framework.Caching;
|
|
||||||
using StardewModdingAPI.Web.Framework.Caching.Mods;
|
using StardewModdingAPI.Web.Framework.Caching.Mods;
|
||||||
using StardewModdingAPI.Web.Framework.Caching.Wiki;
|
using StardewModdingAPI.Web.Framework.Caching.Wiki;
|
||||||
using StardewModdingAPI.Web.Framework.Clients.Chucklefish;
|
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.Clients.Pastebin;
|
||||||
using StardewModdingAPI.Web.Framework.Compression;
|
using StardewModdingAPI.Web.Framework.Compression;
|
||||||
using StardewModdingAPI.Web.Framework.ConfigModels;
|
using StardewModdingAPI.Web.Framework.ConfigModels;
|
||||||
using StardewModdingAPI.Web.Framework.RewriteRules;
|
using StardewModdingAPI.Web.Framework.RedirectRules;
|
||||||
using StardewModdingAPI.Web.Framework.Storage;
|
using StardewModdingAPI.Web.Framework.Storage;
|
||||||
|
|
||||||
namespace StardewModdingAPI.Web
|
namespace StardewModdingAPI.Web
|
||||||
|
@ -47,7 +42,7 @@ namespace StardewModdingAPI.Web
|
||||||
*********/
|
*********/
|
||||||
/// <summary>Construct an instance.</summary>
|
/// <summary>Construct an instance.</summary>
|
||||||
/// <param name="env">The hosting environment.</param>
|
/// <param name="env">The hosting environment.</param>
|
||||||
public Startup(IHostingEnvironment env)
|
public Startup(IWebHostEnvironment env)
|
||||||
{
|
{
|
||||||
this.Configuration = new ConfigurationBuilder()
|
this.Configuration = new ConfigurationBuilder()
|
||||||
.SetBasePath(env.ContentRootPath)
|
.SetBasePath(env.ContentRootPath)
|
||||||
|
@ -67,22 +62,33 @@ namespace StardewModdingAPI.Web
|
||||||
.Configure<BackgroundServicesConfig>(this.Configuration.GetSection("BackgroundServices"))
|
.Configure<BackgroundServicesConfig>(this.Configuration.GetSection("BackgroundServices"))
|
||||||
.Configure<ModCompatibilityListConfig>(this.Configuration.GetSection("ModCompatibilityList"))
|
.Configure<ModCompatibilityListConfig>(this.Configuration.GetSection("ModCompatibilityList"))
|
||||||
.Configure<ModUpdateCheckConfig>(this.Configuration.GetSection("ModUpdateCheck"))
|
.Configure<ModUpdateCheckConfig>(this.Configuration.GetSection("ModUpdateCheck"))
|
||||||
.Configure<MongoDbConfig>(this.Configuration.GetSection("MongoDB"))
|
|
||||||
.Configure<SiteConfig>(this.Configuration.GetSection("Site"))
|
.Configure<SiteConfig>(this.Configuration.GetSection("Site"))
|
||||||
.Configure<RouteOptions>(options => options.ConstraintMap.Add("semanticVersion", typeof(VersionConstraint)))
|
.Configure<RouteOptions>(options => options.ConstraintMap.Add("semanticVersion", typeof(VersionConstraint)))
|
||||||
.AddLogging()
|
.AddLogging()
|
||||||
.AddMemoryCache()
|
.AddMemoryCache();
|
||||||
.AddMvc()
|
|
||||||
.ConfigureApplicationPartManager(manager => manager.FeatureProviders.Add(new InternalControllerFeatureProvider()))
|
|
||||||
.AddJsonOptions(options =>
|
|
||||||
{
|
|
||||||
foreach (JsonConverter converter in new JsonHelper().JsonSettings.Converters)
|
|
||||||
options.SerializerSettings.Converters.Add(converter);
|
|
||||||
|
|
||||||
options.SerializerSettings.Formatting = Formatting.Indented;
|
// init MVC
|
||||||
options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
|
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
|
// init background service
|
||||||
{
|
{
|
||||||
|
@ -91,46 +97,6 @@ namespace StardewModdingAPI.Web
|
||||||
services.AddHostedService<BackgroundService>();
|
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
|
// init API clients
|
||||||
{
|
{
|
||||||
ApiClientsConfig api = this.Configuration.GetSection("ApiClients").Get<ApiClientsConfig>();
|
ApiClientsConfig api = this.Configuration.GetSection("ApiClients").Get<ApiClientsConfig>();
|
||||||
|
@ -142,6 +108,7 @@ namespace StardewModdingAPI.Web
|
||||||
baseUrl: api.ChucklefishBaseUrl,
|
baseUrl: api.ChucklefishBaseUrl,
|
||||||
modPageUrlFormat: api.ChucklefishModPageUrlFormat
|
modPageUrlFormat: api.ChucklefishModPageUrlFormat
|
||||||
));
|
));
|
||||||
|
|
||||||
services.AddSingleton<ICurseForgeClient>(new CurseForgeClient(
|
services.AddSingleton<ICurseForgeClient>(new CurseForgeClient(
|
||||||
userAgent: userAgent,
|
userAgent: userAgent,
|
||||||
apiUrl: api.CurseForgeBaseUrl
|
apiUrl: api.CurseForgeBaseUrl
|
||||||
|
@ -188,8 +155,7 @@ namespace StardewModdingAPI.Web
|
||||||
|
|
||||||
/// <summary>The method called by the runtime to configure the HTTP request pipeline.</summary>
|
/// <summary>The method called by the runtime to configure the HTTP request pipeline.</summary>
|
||||||
/// <param name="app">The application builder.</param>
|
/// <param name="app">The application builder.</param>
|
||||||
/// <param name="env">The hosting environment.</param>
|
public void Configure(IApplicationBuilder app)
|
||||||
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
|
|
||||||
{
|
{
|
||||||
// basic config
|
// basic config
|
||||||
app.UseDeveloperExceptionPage();
|
app.UseDeveloperExceptionPage();
|
||||||
|
@ -201,7 +167,13 @@ namespace StardewModdingAPI.Web
|
||||||
)
|
)
|
||||||
.UseRewriter(this.GetRedirectRules())
|
.UseRewriter(this.GetRedirectRules())
|
||||||
.UseStaticFiles() // wwwroot folder
|
.UseStaticFiles() // wwwroot folder
|
||||||
.UseMvc();
|
.UseRouting()
|
||||||
|
.UseAuthorization()
|
||||||
|
.UseEndpoints(p =>
|
||||||
|
{
|
||||||
|
p.MapControllers();
|
||||||
|
p.MapRazorPages();
|
||||||
|
});
|
||||||
|
|
||||||
// enable Hangfire dashboard
|
// enable Hangfire dashboard
|
||||||
app.UseHangfireDashboard("/tasks", new DashboardOptions
|
app.UseHangfireDashboard("/tasks", new DashboardOptions
|
||||||
|
@ -215,29 +187,63 @@ namespace StardewModdingAPI.Web
|
||||||
/*********
|
/*********
|
||||||
** Private methods
|
** 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>
|
/// <summary>Get the redirect rules to apply.</summary>
|
||||||
private RewriteOptions GetRedirectRules()
|
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)
|
// legacy paths
|
||||||
redirects.Add(new ConditionalRedirectToHttpsRule(
|
.Add(new RedirectPathsToUrlsRule(this.GetLegacyPathRedirects()))
|
||||||
shouldRewrite: req =>
|
|
||||||
req.Host.Host != "localhost"
|
|
||||||
&& !req.Path.StartsWithSegments("/api")
|
|
||||||
));
|
|
||||||
|
|
||||||
// shortcut redirects
|
// subdomains
|
||||||
redirects.Add(new RedirectToUrlRule(@"^/3\.0\.?$", "https://stardewvalleywiki.com/Modding:Migrate_to_SMAPI_3.0"));
|
.Add(new RedirectHostsToUrlsRule(HttpStatusCode.PermanentRedirect, host => host switch
|
||||||
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"));
|
"api.smapi.io" => "smapi.io/api",
|
||||||
redirects.Add(new RedirectToUrlRule(@"^/compat\.?$", "https://smapi.io/mods"));
|
"json.smapi.io" => "smapi.io/json",
|
||||||
redirects.Add(new RedirectToUrlRule(@"^/docs\.?$", "https://stardewvalleywiki.com/Modding:Index"));
|
"log.smapi.io" => "smapi.io/log",
|
||||||
redirects.Add(new RedirectToUrlRule(@"^/install\.?$", "https://stardewvalleywiki.com/Modding:Player_Guide/Getting_Started#Install_SMAPI"));
|
"mods.smapi.io" => "smapi.io/mods",
|
||||||
redirects.Add(new RedirectToUrlRule(@"^/troubleshoot(.*)$", "https://stardewvalleywiki.com/Modding:Player_Guide/Troubleshooting$1"));
|
_ => host.EndsWith(".smapi.io")
|
||||||
redirects.Add(new RedirectToUrlRule(@"^/xnb\.?$", "https://stardewvalleywiki.com/Modding:Using_XNB_mods"));
|
? "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[]>
|
var wikiRedirects = new Dictionary<string, string[]>
|
||||||
{
|
{
|
||||||
["Modding:Index#Migration_guides"] = new[] { "^/for-devs/updating-a-smapi-mod", "^/guides/updating-a-smapi-mod" },
|
["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:Object_data"] = new[] { "^/for-devs/object-data", "^/guides/object-data" },
|
||||||
["Modding:Weather_data"] = new[] { "^/for-devs/weather", "^/guides/weather" }
|
["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)
|
foreach (string pattern in patterns)
|
||||||
redirects.Add(new RedirectToUrlRule(pattern, "https://stardewvalleywiki.com/" + pair.Key));
|
redirects.Add(pattern, "https://stardewvalleywiki.com/" + page);
|
||||||
}
|
}
|
||||||
|
|
||||||
return redirects;
|
return redirects;
|
||||||
|
|
|
@ -10,6 +10,9 @@ namespace StardewModdingAPI.Web.ViewModels.JsonValidator
|
||||||
/*********
|
/*********
|
||||||
** Accessors
|
** Accessors
|
||||||
*********/
|
*********/
|
||||||
|
/// <summary>Whether to show the edit view.</summary>
|
||||||
|
public bool IsEditView { get; set; }
|
||||||
|
|
||||||
/// <summary>The paste ID.</summary>
|
/// <summary>The paste ID.</summary>
|
||||||
public string PasteID { get; set; }
|
public string PasteID { get; set; }
|
||||||
|
|
||||||
|
@ -51,11 +54,13 @@ namespace StardewModdingAPI.Web.ViewModels.JsonValidator
|
||||||
/// <param name="pasteID">The stored file ID.</param>
|
/// <param name="pasteID">The stored file ID.</param>
|
||||||
/// <param name="schemaName">The schema name with which the JSON was validated.</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>
|
/// <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.PasteID = pasteID;
|
||||||
this.SchemaName = schemaName;
|
this.SchemaName = schemaName;
|
||||||
this.SchemaFormats = schemaFormats;
|
this.SchemaFormats = schemaFormats;
|
||||||
|
this.IsEditView = isEditView;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Set the validated content.</summary>
|
/// <summary>Set the validated content.</summary>
|
||||||
|
|
|
@ -26,7 +26,7 @@ namespace StardewModdingAPI.Web.ViewModels
|
||||||
public bool IsStale { get; set; }
|
public bool IsStale { get; set; }
|
||||||
|
|
||||||
/// <summary>Whether the mod metadata is available.</summary>
|
/// <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>
|
/// <summary>The mod author's alternative names, if any.</summary>
|
||||||
public string AlternateAuthors { get; set; }
|
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>
|
/// <summary>The URL to the mod's source code, if any.</summary>
|
||||||
public string SourceUrl { get; set; }
|
public string SourceUrl { get; set; }
|
||||||
|
|
||||||
|
@ -62,6 +65,7 @@ namespace StardewModdingAPI.Web.ViewModels
|
||||||
this.AlternateNames = string.Join(", ", entry.Name.Skip(1).ToArray());
|
this.AlternateNames = string.Join(", ", entry.Name.Skip(1).ToArray());
|
||||||
this.Author = entry.Author.FirstOrDefault();
|
this.Author = entry.Author.FirstOrDefault();
|
||||||
this.AlternateAuthors = string.Join(", ", entry.Author.Skip(1).ToArray());
|
this.AlternateAuthors = string.Join(", ", entry.Author.Skip(1).ToArray());
|
||||||
|
this.GitHubRepo = entry.GitHubRepo;
|
||||||
this.SourceUrl = this.GetSourceUrl(entry);
|
this.SourceUrl = this.GetSourceUrl(entry);
|
||||||
this.Compatibility = new ModCompatibilityModel(entry.Compatibility);
|
this.Compatibility = new ModCompatibilityModel(entry.Compatibility);
|
||||||
this.BetaCompatibility = entry.BetaCompatibility != null ? new ModCompatibilityModel(entry.BetaCompatibility) : null;
|
this.BetaCompatibility = entry.BetaCompatibility != null ? new ModCompatibilityModel(entry.BetaCompatibility) : null;
|
||||||
|
@ -102,7 +106,7 @@ namespace StardewModdingAPI.Web.ViewModels
|
||||||
if (entry.ModDropID.HasValue)
|
if (entry.ModDropID.HasValue)
|
||||||
{
|
{
|
||||||
anyFound = true;
|
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))
|
if (!string.IsNullOrWhiteSpace(entry.CurseForgeKey))
|
||||||
{
|
{
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
}
|
}
|
||||||
@section Head {
|
@section Head {
|
||||||
<link rel="stylesheet" href="~/Content/css/index.css?r=20200105" />
|
<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>
|
<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 newUploadUrl = this.Url.PlainAction("Index", "JsonValidator", new { schemaName = Model.SchemaName });
|
||||||
string schemaDisplayName = null;
|
string schemaDisplayName = null;
|
||||||
bool isValidSchema = Model.SchemaName != null && Model.SchemaFormats.TryGetValue(Model.SchemaName, out schemaDisplayName) && schemaDisplayName?.ToLower() != "none";
|
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
|
// build title
|
||||||
ViewData["Title"] = "JSON validator";
|
ViewData["Title"] = "JSON validator";
|
||||||
|
@ -32,7 +31,7 @@
|
||||||
<link rel="stylesheet" href="~/Content/css/json-validator.css?r=202002" />
|
<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" />
|
<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/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/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>
|
<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 src="~/Content/js/json-validator.js?r=202002"></script>
|
||||||
<script>
|
<script>
|
||||||
$(function() {
|
$(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>
|
</script>
|
||||||
}
|
}
|
||||||
|
@ -63,7 +62,7 @@ else if (Model.ParseError != null)
|
||||||
<small v-pre>Error details: @Model.ParseError</small>
|
<small v-pre>Error details: @Model.ParseError</small>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
else if (!isEditView && Model.PasteID != null)
|
else if (!Model.IsEditView && Model.PasteID != null)
|
||||||
{
|
{
|
||||||
<div class="banner success">
|
<div class="banner success">
|
||||||
<strong>Share this link to let someone else see this page:</strong> <code>@curPageUrl</code><br />
|
<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 *@
|
@* upload new file *@
|
||||||
@if (isEditView)
|
@if (Model.IsEditView)
|
||||||
{
|
{
|
||||||
<h2>Upload a JSON file</h2>
|
<h2>Upload a JSON file</h2>
|
||||||
<form action="@this.Url.PlainAction("PostAsync", "JsonValidator")" method="post">
|
<form action="@this.Url.PlainAction("PostAsync", "JsonValidator")" method="post">
|
||||||
|
@ -112,7 +111,7 @@ else if (!isEditView && Model.PasteID != null)
|
||||||
}
|
}
|
||||||
|
|
||||||
@* validation results *@
|
@* validation results *@
|
||||||
@if (!isEditView)
|
@if (!Model.IsEditView)
|
||||||
{
|
{
|
||||||
<div id="output">
|
<div id="output">
|
||||||
@if (Model.UploadError == null)
|
@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>
|
<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>
|
</div>
|
||||||
<pre id="raw-content" class="sunlight-highlight-javascript">@Model.Content</pre>
|
<pre id="raw-content" class="sunlight-highlight-javascript">@Model.Content</pre>
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
@using Humanizer
|
@using Humanizer
|
||||||
@using Newtonsoft.Json
|
|
||||||
@using StardewModdingAPI.Toolkit.Utilities
|
@using StardewModdingAPI.Toolkit.Utilities
|
||||||
@using StardewModdingAPI.Web.Framework
|
@using StardewModdingAPI.Web.Framework
|
||||||
@using StardewModdingAPI.Web.Framework.LogParsing.Models
|
@using StardewModdingAPI.Web.Framework.LogParsing.Models
|
||||||
|
@ -12,7 +11,6 @@
|
||||||
.GetValues(typeof(LogLevel))
|
.GetValues(typeof(LogLevel))
|
||||||
.Cast<LogLevel>()
|
.Cast<LogLevel>()
|
||||||
.ToDictionary(level => level.ToString().ToLower(), level => level != LogLevel.Trace);
|
.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);
|
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/file-upload.css?r=202002" />
|
||||||
<link rel="stylesheet" href="~/Content/css/log-parser.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/vue@2.6.11" 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/jquery@3.5.1" crossorigin="anonymous"></script>
|
||||||
<script src="~/Content/js/file-upload.js?r=202002"></script>
|
<script src="~/Content/js/file-upload.js?r=202002"></script>
|
||||||
<script src="~/Content/js/log-parser.js?r=202002"></script>
|
<script src="~/Content/js/log-parser.js?r=202002"></script>
|
||||||
<script>
|
<script>
|
||||||
$(function() {
|
$(function() {
|
||||||
smapi.logParser({
|
smapi.logParser({
|
||||||
logStarted: new Date(@Json.Serialize(Model.ParsedLog?.Timestamp)),
|
logStarted: new Date(@this.ForJson(Model.ParsedLog?.Timestamp)),
|
||||||
showPopup: @Json.Serialize(Model.ParsedLog == null),
|
showPopup: @this.ForJson(Model.ParsedLog == null),
|
||||||
showMods: @Json.Serialize(Model.ParsedLog?.Mods?.Select(p => Model.GetSlug(p.Name)).Distinct().ToDictionary(slug => slug, slug => true), noFormatting),
|
showMods: @this.ForJson(Model.ParsedLog?.Mods?.Select(p => Model.GetSlug(p.Name)).Distinct().ToDictionary(slug => slug, slug => true)),
|
||||||
showSections: @Json.Serialize(Enum.GetNames(typeof(LogSection)).ToDictionary(section => section, section => false), noFormatting),
|
showSections: @this.ForJson(Enum.GetNames(typeof(LogSection)).ToDictionary(section => section, section => false)),
|
||||||
showLevels: @Json.Serialize(defaultFilters, noFormatting),
|
showLevels: @this.ForJson(defaultFilters),
|
||||||
enableFilters: @Json.Serialize(!Model.ShowRaw)
|
enableFilters: @this.ForJson(!Model.ShowRaw)
|
||||||
}, '@this.Url.PlainAction("Index", "LogParser", values: null)');
|
}, '@this.Url.PlainAction("Index", "LogParser", values: null)');
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue