This commit is contained in:
Łukasz Domeradzki
2025-01-05 02:32:05 +01:00
parent 8c9cf69353
commit 3f98337459
62 changed files with 342 additions and 588 deletions

View File

@@ -50,17 +50,16 @@ internal sealed class ExamplePlugin : IASF, IBot, IBotCommand2, IBotConnection,
// This is used for identification purposes, typically you want to use a friendly name of your plugin here, such as the name of your main class
// Please note that this property can have direct dependencies only on structures that were initialized by the constructor, as it's possible to be called before OnLoaded() takes place
[JsonInclude]
[Required]
public string Name => nameof(ExamplePlugin);
// This will be displayed to the user and written in the log file, typically you should point it to the version of your library, but alternatively you can do some more advanced logic if you'd like to
// Please note that this property can have direct dependencies only on structures that were initialized by the constructor, as it's possible to be called before OnLoaded() takes place
[JsonInclude]
[Required]
public Version Version => typeof(ExamplePlugin).Assembly.GetName().Version ?? throw new InvalidOperationException(nameof(Version));
// Plugins can expose custom properties for our GET /Api/Plugins API call, simply annotate them with [JsonProperty] (or keep public)
// Plugins can expose custom properties for our GET /Api/Plugins API call, simply annotate them with [JsonInclude] (or keep public)
[JsonInclude]
[JsonRequired]
[Required]
public bool CustomIsEnabledField { get; private init; } = true;

View File

@@ -22,7 +22,6 @@
// limitations under the License.
using System;
using System.ComponentModel.DataAnnotations;
using System.Composition;
using System.Runtime;
using System.Text.Json.Serialization;
@@ -43,11 +42,9 @@ internal sealed class PeriodicGCPlugin : IPlugin {
private static readonly Timer PeriodicGCTimer = new(PerformGC);
[JsonInclude]
[Required]
public string Name => nameof(PeriodicGCPlugin);
[JsonInclude]
[Required]
public Version Version => typeof(PeriodicGCPlugin).Assembly.GetName().Version ?? throw new InvalidOperationException(nameof(Version));
public Task OnLoaded() {

View File

@@ -22,7 +22,6 @@
// limitations under the License.
using System;
using System.ComponentModel.DataAnnotations;
using System.Composition;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
@@ -36,11 +35,9 @@ namespace ArchiSteamFarm.CustomPlugins.SignInWithSteam;
[UsedImplicitly]
internal sealed class SignInWithSteamPlugin : IPlugin {
[JsonInclude]
[Required]
public string Name => nameof(SignInWithSteamPlugin);
[JsonInclude]
[Required]
public Version Version => typeof(SignInWithSteamPlugin).Assembly.GetName().Version ?? throw new InvalidOperationException(nameof(Version));
public Task OnLoaded() {

View File

@@ -7,7 +7,6 @@
<PackageReference Include="JetBrains.Annotations" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.ResxSourceGenerator" PrivateAssets="all" />
<PackageReference Include="SteamKit2" IncludeAssets="compile" />
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" IncludeAssets="compile" />
<PackageReference Include="System.Composition.AttributedModel" IncludeAssets="compile" />
<PackageReference Include="System.Linq.Async" IncludeAssets="compile" />
</ItemGroup>

View File

@@ -25,7 +25,6 @@ using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Composition;
using System.Linq;
using System.Text.Json;
@@ -47,11 +46,9 @@ internal sealed class ItemsMatcherPlugin : OfficialPlugin, IBot, IBotCommand2, I
internal static readonly ConcurrentDictionary<Bot, RemoteCommunication> RemoteCommunications = new();
[JsonInclude]
[Required]
public override string Name => nameof(ItemsMatcherPlugin);
[JsonInclude]
[Required]
public override Version Version => typeof(ItemsMatcherPlugin).Assembly.GetName().Version ?? throw new InvalidOperationException(nameof(Version));
public async Task<string?> OnBotCommand(Bot bot, EAccess access, string message, string[] args, ulong steamID = 0) {

View File

@@ -24,7 +24,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Composition;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
@@ -42,11 +41,9 @@ namespace ArchiSteamFarm.OfficialPlugins.MobileAuthenticator;
[SuppressMessage("ReSharper", "MemberCanBeFileLocal")]
internal sealed class MobileAuthenticatorPlugin : OfficialPlugin, IBotCommand2, IBotSteamClient {
[JsonInclude]
[Required]
public override string Name => nameof(MobileAuthenticatorPlugin);
[JsonInclude]
[Required]
public override Version Version => typeof(MobileAuthenticatorPlugin).Assembly.GetName().Version ?? throw new InvalidOperationException(nameof(Version));
public async Task<string?> OnBotCommand(Bot bot, EAccess access, string message, string[] args, ulong steamID = 0) {

View File

@@ -25,7 +25,6 @@ using System;
using System.Collections.Concurrent;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Composition;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Metrics;
@@ -73,13 +72,11 @@ internal sealed class MonitoringPlugin : OfficialPlugin, IDisposable, IOfficialG
private static FrozenSet<Measurement<int>>? PluginMeasurements;
[JsonInclude]
[Required]
public override string Name => nameof(MonitoringPlugin);
public string RepositoryName => SharedInfo.GithubRepo;
[JsonInclude]
[Required]
public override Version Version => typeof(MonitoringPlugin).Assembly.GetName().Version ?? throw new InvalidOperationException(nameof(Version));
private readonly ConcurrentDictionary<Bot, TradeStatistics> TradeStatistics = new();

View File

@@ -7,7 +7,6 @@
<PackageReference Include="JetBrains.Annotations" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.ResxSourceGenerator" PrivateAssets="all" />
<PackageReference Include="SteamKit2" IncludeAssets="compile" />
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" IncludeAssets="compile" />
<PackageReference Include="System.Composition.AttributedModel" IncludeAssets="compile" />
</ItemGroup>

View File

@@ -23,8 +23,8 @@
using System.Net;
using ArchiSteamFarm.IPC.Controllers.Api;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Swashbuckle.AspNetCore.Annotations;
namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper;
@@ -32,6 +32,6 @@ namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper;
public sealed class SteamTokenDumperController : ArchiController {
[HttpGet(nameof(GlobalConfigExtension))]
[ProducesResponseType<GlobalConfigExtension>((int) HttpStatusCode.OK)]
[SwaggerOperation(Tags = [nameof(GlobalConfigExtension)])]
[Tags(nameof(GlobalConfigExtension))]
public ActionResult<GlobalConfigExtension> Get() => Ok(new GlobalConfigExtension());
}

View File

@@ -26,7 +26,6 @@ using System.Collections.Concurrent;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Composition;
using System.Linq;
using System.Net;
@@ -65,11 +64,9 @@ internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotC
private static DateTimeOffset LastUploadAt = DateTimeOffset.MinValue;
[JsonInclude]
[Required]
public override string Name => nameof(SteamTokenDumperPlugin);
[JsonInclude]
[Required]
public override Version Version => typeof(SteamTokenDumperPlugin).Assembly.GetName().Version ?? throw new InvalidOperationException(nameof(Version));
public Task<uint> GetPreferredChangeNumberToStartFrom() => Task.FromResult(GlobalCache?.LastChangeNumber ?? 0);

View File

@@ -1,7 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<DefaultItemExcludes>$(DefaultItemExcludes);config/**;debug/**;logs/**;overlay/**</DefaultItemExcludes>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<OutputType>Exe</OutputType>
</PropertyGroup>
@@ -11,13 +10,13 @@
<PackageReference Include="Humanizer" />
<PackageReference Include="JetBrains.Annotations" PrivateAssets="all" />
<PackageReference Include="Markdig.Signed" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" />
<PackageReference Include="Microsoft.CodeAnalysis.ResxSourceGenerator" PrivateAssets="all" />
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" />
<PackageReference Include="Nito.AsyncEx.Coordination" />
<PackageReference Include="NLog.Web.AspNetCore" />
<PackageReference Include="SteamKit2" />
<PackageReference Include="Swashbuckle.AspNetCore" />
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" />
<PackageReference Include="System.Composition" />
<PackageReference Include="System.Linq.Async" />
<PackageReference Include="System.Security.Cryptography.ProtectedData" />

View File

@@ -35,6 +35,7 @@ using ArchiSteamFarm.Core;
using ArchiSteamFarm.Helpers.Json;
using ArchiSteamFarm.IPC.Controllers.Api;
using ArchiSteamFarm.IPC.Integration;
using ArchiSteamFarm.IPC.OpenApi;
using ArchiSteamFarm.Localization;
using ArchiSteamFarm.NLog;
using ArchiSteamFarm.NLog.Targets;
@@ -52,7 +53,6 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
using Microsoft.OpenApi.Models;
using NLog.Web;
using IPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork;
@@ -118,7 +118,7 @@ internal static class ArchiKestrel {
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "PathString is a primitive, it's unlikely to be trimmed to the best of our knowledge")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3000", Justification = "We don't care about trimmed assemblies, as we need it to work only with the known (used) ones")]
private static void ConfigureApp([SuppressMessage("ReSharper", "SuggestBaseTypeForParameter")] ConfigurationManager configuration, IApplicationBuilder app) {
private static void ConfigureApp([SuppressMessage("ReSharper", "SuggestBaseTypeForParameter")] ConfigurationManager configuration, WebApplication app) {
ArgumentNullException.ThrowIfNull(configuration);
ArgumentNullException.ThrowIfNull(app);
@@ -252,10 +252,10 @@ internal static class ArchiKestrel {
}
// Finally register proper API endpoints once we're done with routing
app.UseEndpoints(static endpoints => endpoints.MapControllers());
app.MapControllers();
// Add support for swagger, responsible for automatic API documentation generation, this should be on the end, once we're done with API
app.UseSwagger();
// Add support for OpenAPI, responsible for automatic API documentation generation, this should be on the end, once we're done with API
app.MapOpenApi("/swagger/{documentName}/swagger.json");
// Add support for swagger UI, this should be after swagger, obviously
app.UseSwaggerUI(
@@ -331,66 +331,12 @@ internal static class ArchiKestrel {
services.AddCors(static options => options.AddDefaultPolicy(static policyBuilder => policyBuilder.AllowAnyOrigin()));
}
// Add support for swagger, responsible for automatic API documentation generation
services.AddSwaggerGen(
static options => {
options.AddSecurityDefinition(
nameof(GlobalConfig.IPCPassword), new OpenApiSecurityScheme {
Description = $"{nameof(GlobalConfig.IPCPassword)} authentication using request headers. Check {SharedInfo.ProjectURL}/wiki/IPC#authentication for more info.",
In = ParameterLocation.Header,
Name = ApiAuthenticationMiddleware.HeadersField,
Type = SecuritySchemeType.ApiKey
}
);
options.AddSecurityRequirement(
new OpenApiSecurityRequirement {
{
new OpenApiSecurityScheme {
Reference = new OpenApiReference {
Id = nameof(GlobalConfig.IPCPassword),
Type = ReferenceType.SecurityScheme
}
},
[]
}
}
);
// We require custom schema IDs due to conflicting type names, choosing the proper one is tricky as there is no good answer and any kind of convention has a potential to create conflict
// FullName and Name both do, ToString() for unknown to me reason doesn't, and I don't have courage to call our WebUtilities.GetUnifiedName() better than what .NET ships with (because it isn't)
// Let's use ToString() until we find a good enough reason to change it, also, the name must pass ^[a-zA-Z0-9.-_]+$ regex
options.CustomSchemaIds(static type => type.ToString().Replace('+', '-'));
options.EnableAnnotations(true, true);
options.SchemaFilter<CustomAttributesSchemaFilter>();
options.SchemaFilter<EnumSchemaFilter>();
options.SchemaFilter<ReadOnlyFixesSchemaFilter>();
options.SwaggerDoc(
SharedInfo.ASF, new OpenApiInfo {
Contact = new OpenApiContact {
Name = SharedInfo.GithubRepo,
Url = new Uri(SharedInfo.ProjectURL)
},
License = new OpenApiLicense {
Name = SharedInfo.LicenseName,
Url = new Uri(SharedInfo.LicenseURL)
},
Title = $"{SharedInfo.AssemblyName} API",
Version = SharedInfo.Version.ToString()
}
);
string xmlDocumentationFile = Path.Combine(AppContext.BaseDirectory, SharedInfo.AssemblyDocumentation);
if (File.Exists(xmlDocumentationFile)) {
options.IncludeXmlComments(xmlDocumentationFile);
}
// Add support for OpenAPI, responsible for automatic API documentation generation
services.AddOpenApi(
SharedInfo.ASF, static options => {
options.AddDocumentTransformer<DocumentTransformer>();
options.AddOperationTransformer<OperationTransformer>();
options.AddSchemaTransformer<SchemaTransformer>();
}
);
@@ -406,6 +352,16 @@ internal static class ArchiKestrel {
}
}
services.ConfigureHttpJsonOptions(
static options => {
JsonSerializerOptions jsonSerializerOptions = Debugging.IsUserDebugging ? JsonUtilities.IndentedJsonSerialierOptions : JsonUtilities.DefaultJsonSerialierOptions;
options.SerializerOptions.PropertyNamingPolicy = jsonSerializerOptions.PropertyNamingPolicy;
options.SerializerOptions.TypeInfoResolver = jsonSerializerOptions.TypeInfoResolver;
options.SerializerOptions.WriteIndented = jsonSerializerOptions.WriteIndented;
}
);
// We need MVC for /Api, but we're going to use only a small subset of all available features
IMvcBuilder mvc = services.AddControllers();

View File

@@ -34,6 +34,7 @@ using ArchiSteamFarm.IPC.Responses;
using ArchiSteamFarm.Localization;
using ArchiSteamFarm.Steam.Interaction;
using ArchiSteamFarm.Storage;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Hosting;
@@ -51,10 +52,7 @@ public sealed class ASFController : ArchiController {
ApplicationLifetime = applicationLifetime;
}
/// <summary>
/// Encrypts data with ASF encryption mechanisms using provided details.
/// </summary>
[Consumes("application/json")]
[EndpointSummary("Encrypts data with ASF encryption mechanisms using provided details.")]
[HttpPost("Encrypt")]
[ProducesResponseType<GenericResponse<string>>((int) HttpStatusCode.OK)]
[ProducesResponseType<GenericResponse>((int) HttpStatusCode.BadRequest)]
@@ -70,9 +68,7 @@ public sealed class ASFController : ArchiController {
return Ok(new GenericResponse<string>(encryptedString));
}
/// <summary>
/// Fetches common info related to ASF as a whole.
/// </summary>
[EndpointSummary("Fetches common info related to ASF as a whole")]
[HttpGet]
[ProducesResponseType<GenericResponse<ASFResponse>>((int) HttpStatusCode.OK)]
public ActionResult<GenericResponse<ASFResponse>> ASFGet() {
@@ -87,10 +83,7 @@ public sealed class ASFController : ArchiController {
return Ok(new GenericResponse<ASFResponse>(result));
}
/// <summary>
/// Hashes data with ASF hashing mechanisms using provided details.
/// </summary>
[Consumes("application/json")]
[EndpointSummary("Hashes data with ASF hashing mechanisms using provided details")]
[HttpPost("Hash")]
[ProducesResponseType<GenericResponse<string>>((int) HttpStatusCode.OK)]
[ProducesResponseType<GenericResponse>((int) HttpStatusCode.BadRequest)]
@@ -106,10 +99,7 @@ public sealed class ASFController : ArchiController {
return Ok(new GenericResponse<string>(hash));
}
/// <summary>
/// Updates ASF's global config.
/// </summary>
[Consumes("application/json")]
[EndpointSummary("Updates ASF's global config")]
[HttpPost]
[ProducesResponseType<GenericResponse>((int) HttpStatusCode.OK)]
[ProducesResponseType<GenericResponse>((int) HttpStatusCode.BadRequest)]
@@ -163,9 +153,7 @@ public sealed class ASFController : ArchiController {
return Ok(new GenericResponse(result));
}
/// <summary>
/// Makes ASF shutdown itself.
/// </summary>
[EndpointSummary("Makes ASF shutdown itself")]
[HttpPost("Exit")]
[ProducesResponseType<GenericResponse>((int) HttpStatusCode.OK)]
public ActionResult<GenericResponse> ExitPost() {
@@ -174,9 +162,7 @@ public sealed class ASFController : ArchiController {
return Ok(new GenericResponse(success, message));
}
/// <summary>
/// Makes ASF restart itself.
/// </summary>
[EndpointSummary("Makes ASF restart itself")]
[HttpPost("Restart")]
[ProducesResponseType<GenericResponse>((int) HttpStatusCode.OK)]
public ActionResult<GenericResponse> RestartPost() {
@@ -185,9 +171,7 @@ public sealed class ASFController : ArchiController {
return Ok(new GenericResponse(success, message));
}
/// <summary>
/// Makes ASF update itself.
/// </summary>
[EndpointSummary("Makes ASF update itself")]
[HttpPost("Update")]
[ProducesResponseType<GenericResponse<string>>((int) HttpStatusCode.OK)]
public async Task<ActionResult<GenericResponse<string>>> UpdatePost([FromBody] UpdateRequest request) {

View File

@@ -23,18 +23,16 @@
using System.Net;
using ArchiSteamFarm.IPC.Responses;
using ArchiSteamFarm.Storage;
using Microsoft.AspNetCore.Mvc;
using Swashbuckle.AspNetCore.Annotations;
namespace ArchiSteamFarm.IPC.Controllers.Api;
[ApiController]
[Produces("application/json")]
[ProducesResponseType<GenericResponse>((int) HttpStatusCode.BadRequest)]
[ProducesResponseType<GenericResponse<StatusCodeResponse>>((int) HttpStatusCode.Unauthorized)]
[ProducesResponseType<GenericResponse<StatusCodeResponse>>((int) HttpStatusCode.Forbidden)]
[ProducesResponseType((int) HttpStatusCode.InternalServerError)]
[ProducesResponseType((int) HttpStatusCode.ServiceUnavailable)]
[Route("Api")]
[SwaggerResponse((int) HttpStatusCode.BadRequest, $"The request has failed, check {nameof(GenericResponse.Message)} from response body for actual reason. Most of the time this is ASF, understanding the request, but refusing to execute it due to provided reason.", typeof(GenericResponse))]
[SwaggerResponse((int) HttpStatusCode.Unauthorized, $"ASF has {nameof(GlobalConfig.IPCPassword)} set, but you've failed to authenticate. See {SharedInfo.ProjectURL}/wiki/IPC#authentication.", typeof(GenericResponse<StatusCodeResponse>))]
[SwaggerResponse((int) HttpStatusCode.Forbidden, $"ASF lacks {nameof(GlobalConfig.IPCPassword)} and you're not permitted to access the API, or {nameof(GlobalConfig.IPCPassword)} is set and you've failed to authenticate too many times (try again in an hour). See {SharedInfo.ProjectURL}/wiki/IPC#authentication.", typeof(GenericResponse<StatusCodeResponse>))]
[SwaggerResponse((int) HttpStatusCode.InternalServerError, "ASF has encountered an unexpected error while serving the request. The log may include extra info related to this issue.")]
[SwaggerResponse((int) HttpStatusCode.ServiceUnavailable, "ASF has encountered an error while requesting a third-party resource. Try again later.")]
public abstract class ArchiController : ControllerBase;

View File

@@ -34,6 +34,7 @@ using ArchiSteamFarm.IPC.Responses;
using ArchiSteamFarm.Localization;
using ArchiSteamFarm.Steam;
using ArchiSteamFarm.Steam.Storage;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using SteamKit2;
using SteamKit2.Internal;
@@ -42,10 +43,7 @@ namespace ArchiSteamFarm.IPC.Controllers.Api;
[Route("Api/Bot")]
public sealed class BotController : ArchiController {
/// <summary>
/// Adds (free) licenses on given bots.
/// </summary>
[Consumes("application/json")]
[EndpointSummary("Adds (free) licenses on given bots")]
[HttpPost("{botNames:required}/AddLicense")]
[ProducesResponseType<GenericResponse<IReadOnlyDictionary<string, BotAddLicenseResponse>>>((int) HttpStatusCode.OK)]
[ProducesResponseType<GenericResponse>((int) HttpStatusCode.BadRequest)]
@@ -74,9 +72,7 @@ public sealed class BotController : ArchiController {
return Ok(new GenericResponse<IReadOnlyDictionary<string, BotAddLicenseResponse>>(result));
}
/// <summary>
/// Deletes all files related to given bots.
/// </summary>
[EndpointSummary("Deletes all files related to given bots")]
[HttpDelete("{botNames:required}")]
[ProducesResponseType<GenericResponse>((int) HttpStatusCode.OK)]
[ProducesResponseType<GenericResponse>((int) HttpStatusCode.BadRequest)]
@@ -94,9 +90,7 @@ public sealed class BotController : ArchiController {
return Ok(new GenericResponse(results.All(static result => result)));
}
/// <summary>
/// Fetches common info related to given bots.
/// </summary>
[EndpointSummary("Fetches common info related to given bots")]
[HttpGet("{botNames:required}")]
[ProducesResponseType<GenericResponse<IReadOnlyDictionary<string, Bot>>>((int) HttpStatusCode.OK)]
[ProducesResponseType<GenericResponse>((int) HttpStatusCode.BadRequest)]
@@ -112,10 +106,7 @@ public sealed class BotController : ArchiController {
return Ok(new GenericResponse<IReadOnlyDictionary<string, Bot>>(bots.Where(static bot => !string.IsNullOrEmpty(bot.BotName)).ToDictionary(static bot => bot.BotName, static bot => bot, Bot.BotsComparer)));
}
/// <summary>
/// Updates bot config of given bot.
/// </summary>
[Consumes("application/json")]
[EndpointSummary("Updates bot config of given bot")]
[HttpPost("{botNames:required}")]
[ProducesResponseType<GenericResponse<IReadOnlyDictionary<string, bool>>>((int) HttpStatusCode.OK)]
[ProducesResponseType<GenericResponse>((int) HttpStatusCode.BadRequest)]
@@ -185,9 +176,7 @@ public sealed class BotController : ArchiController {
return Ok(new GenericResponse<IReadOnlyDictionary<string, bool>>(result.Values.All(static value => value), result));
}
/// <summary>
/// Removes BGR output files of given bots.
/// </summary>
[EndpointSummary("Removes BGR output files of given bots")]
[HttpDelete("{botNames:required}/GamesToRedeemInBackground")]
[ProducesResponseType<GenericResponse>((int) HttpStatusCode.OK)]
[ProducesResponseType<GenericResponse>((int) HttpStatusCode.BadRequest)]
@@ -205,9 +194,7 @@ public sealed class BotController : ArchiController {
return Ok(results.All(static result => result) ? new GenericResponse(true) : new GenericResponse(false, Strings.WarningFailed));
}
/// <summary>
/// Fetches BGR output files of given bots.
/// </summary>
[EndpointSummary("Fetches BGR output files of given bots")]
[HttpGet("{botNames:required}/GamesToRedeemInBackground")]
[ProducesResponseType<GenericResponse<IReadOnlyDictionary<string, GamesToRedeemInBackgroundResponse>>>((int) HttpStatusCode.OK)]
[ProducesResponseType<GenericResponse>((int) HttpStatusCode.BadRequest)]
@@ -232,10 +219,7 @@ public sealed class BotController : ArchiController {
return Ok(new GenericResponse<IReadOnlyDictionary<string, GamesToRedeemInBackgroundResponse>>(result));
}
/// <summary>
/// Adds keys to redeem using BGR to given bot.
/// </summary>
[Consumes("application/json")]
[EndpointSummary("Adds keys to redeem using BGR to given bot")]
[HttpPost("{botNames:required}/GamesToRedeemInBackground")]
[ProducesResponseType<GenericResponse<IReadOnlyDictionary<string, IOrderedDictionary>>>((int) HttpStatusCode.OK)]
[ProducesResponseType<GenericResponse>((int) HttpStatusCode.BadRequest)]
@@ -270,10 +254,7 @@ public sealed class BotController : ArchiController {
return Ok(new GenericResponse<IReadOnlyDictionary<string, IOrderedDictionary>>(result));
}
/// <summary>
/// Provides input value to given bot for next usage.
/// </summary>
[Consumes("application/json")]
[EndpointSummary("Provides input value to given bot for next usage")]
[HttpPost("{botNames:required}/Input")]
[ProducesResponseType<GenericResponse>((int) HttpStatusCode.OK)]
[ProducesResponseType<GenericResponse>((int) HttpStatusCode.BadRequest)]
@@ -296,10 +277,7 @@ public sealed class BotController : ArchiController {
return Ok(results.All(static result => result) ? new GenericResponse(true) : new GenericResponse(false, Strings.WarningFailed));
}
/// <summary>
/// Pauses given bots.
/// </summary>
[Consumes("application/json")]
[EndpointSummary("Pauses given bots")]
[HttpPost("{botNames:required}/Pause")]
[ProducesResponseType<GenericResponse>((int) HttpStatusCode.OK)]
[ProducesResponseType<GenericResponse>((int) HttpStatusCode.BadRequest)]
@@ -318,10 +296,7 @@ public sealed class BotController : ArchiController {
return Ok(new GenericResponse(results.All(static result => result.Success), string.Join(Environment.NewLine, results.Select(static result => result.Message))));
}
/// <summary>
/// Redeems points on given bots.
/// </summary>
[Consumes("application/json")]
[EndpointSummary("Redeems points on given bots")]
[HttpPost("{botNames:required}/RedeemPoints/{definitionID:required}")]
[ProducesResponseType<GenericResponse<IReadOnlyDictionary<string, EResult>>>((int) HttpStatusCode.OK)]
[ProducesResponseType<GenericResponse>((int) HttpStatusCode.BadRequest)]
@@ -346,14 +321,8 @@ public sealed class BotController : ArchiController {
return Ok(new GenericResponse<IReadOnlyDictionary<string, EResult>>(result));
}
/// <summary>
/// Redeems cd-keys on given bot.
/// </summary>
/// <remarks>
/// Response contains a map that maps each provided cd-key to its redeem result.
/// Redeem result can be a null value, this means that ASF didn't even attempt to send a request (e.g. because of bot not being connected to Steam network).
/// </remarks>
[Consumes("application/json")]
[EndpointDescription("Response contains a map that maps each provided cd-key to its redeem result. Redeem result can be a null value, this means that ASF didn't even attempt to send a request (e.g. because of bot not being connected to Steam network)")]
[EndpointSummary("Redeems cd-keys on given bot")]
[HttpPost("{botNames:required}/Redeem")]
[ProducesResponseType<GenericResponse<IReadOnlyDictionary<string, IReadOnlyDictionary<string, CStore_RegisterCDKey_Response>>>>((int) HttpStatusCode.OK)]
[ProducesResponseType<GenericResponse>((int) HttpStatusCode.BadRequest)]
@@ -389,10 +358,7 @@ public sealed class BotController : ArchiController {
return Ok(new GenericResponse<IReadOnlyDictionary<string, IReadOnlyDictionary<string, CStore_RegisterCDKey_Response?>>>(result.Values.SelectMany(static responses => responses.Values).All(static value => value != null), result));
}
/// <summary>
/// Renames given bot along with all its related files.
/// </summary>
[Consumes("application/json")]
[EndpointSummary("Renames given bot along with all its related files")]
[HttpPost("{botName:required}/Rename")]
[ProducesResponseType<GenericResponse>((int) HttpStatusCode.OK)]
[ProducesResponseType<GenericResponse>((int) HttpStatusCode.BadRequest)]
@@ -417,9 +383,7 @@ public sealed class BotController : ArchiController {
return Ok(new GenericResponse(result));
}
/// <summary>
/// Resumes given bots.
/// </summary>
[EndpointSummary("Resumes given bots")]
[HttpPost("{botNames:required}/Resume")]
[ProducesResponseType<GenericResponse>((int) HttpStatusCode.OK)]
[ProducesResponseType<GenericResponse>((int) HttpStatusCode.BadRequest)]
@@ -437,9 +401,7 @@ public sealed class BotController : ArchiController {
return Ok(new GenericResponse(results.All(static result => result.Success), string.Join(Environment.NewLine, results.Select(static result => result.Message))));
}
/// <summary>
/// Starts given bots.
/// </summary>
[EndpointSummary("Starts given bots")]
[HttpPost("{botNames:required}/Start")]
[ProducesResponseType<GenericResponse>((int) HttpStatusCode.OK)]
[ProducesResponseType<GenericResponse>((int) HttpStatusCode.BadRequest)]
@@ -457,9 +419,7 @@ public sealed class BotController : ArchiController {
return Ok(new GenericResponse(results.All(static result => result.Success), string.Join(Environment.NewLine, results.Select(static result => result.Message))));
}
/// <summary>
/// Stops given bots.
/// </summary>
[EndpointSummary("Stops given bots")]
[HttpPost("{botNames:required}/Stop")]
[ProducesResponseType<GenericResponse>((int) HttpStatusCode.OK)]
[ProducesResponseType<GenericResponse>((int) HttpStatusCode.BadRequest)]

View File

@@ -31,6 +31,7 @@ using ArchiSteamFarm.IPC.Responses;
using ArchiSteamFarm.Localization;
using ArchiSteamFarm.Steam;
using ArchiSteamFarm.Storage;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Hosting;
@@ -46,14 +47,8 @@ public sealed class CommandController : ArchiController {
ApplicationLifetime = applicationLifetime;
}
/// <summary>
/// Executes a command.
/// </summary>
/// <remarks>
/// This API endpoint is supposed to be entirely replaced by ASF actions available under /Api/ASF/{action} and /Api/Bot/{bot}/{action}.
/// You should use "given bot" commands when executing this endpoint, omitting targets of the command will cause the command to be executed on first defined bot
/// </remarks>
[Consumes("application/json")]
[EndpointDescription($"This API endpoint is supposed to be entirely replaced by ASF actions available under /Api/ASF/{{action}} and /Api/Bot/{{bot}}/{{action}}. You should use \"given bot\" commands when executing this endpoint, omitting targets of the command will cause the command to be executed on {nameof(GlobalConfig.DefaultBot)}")]
[EndpointSummary("Executes a command")]
[HttpPost]
[ProducesResponseType<GenericResponse<string>>((int) HttpStatusCode.OK)]
[ProducesResponseType<GenericResponse>((int) HttpStatusCode.BadRequest)]

View File

@@ -32,18 +32,15 @@ using ArchiSteamFarm.Localization;
using ArchiSteamFarm.Web;
using ArchiSteamFarm.Web.GitHub;
using ArchiSteamFarm.Web.GitHub.Data;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace ArchiSteamFarm.IPC.Controllers.Api;
[Route("Api/WWW/GitHub")]
public sealed class GitHubController : ArchiController {
/// <summary>
/// Fetches the most recent GitHub release of ASF project.
/// </summary>
/// <remarks>
/// This is internal API being utilizied by our ASF-ui IPC frontend. You should not depend on existence of any /Api/WWW endpoints as they can disappear and change anytime.
/// </remarks>
[EndpointDescription("This is internal API being utilizied by our ASF-ui IPC frontend. You should not depend on existence of any /Api/WWW endpoints as they can disappear and change anytime")]
[EndpointSummary("Fetches the most recent GitHub release of ASF project")]
[HttpGet("Release")]
[ProducesResponseType<GenericResponse<GitHubReleaseResponse>>((int) HttpStatusCode.OK)]
[ProducesResponseType<GenericResponse>((int) HttpStatusCode.ServiceUnavailable)]
@@ -55,12 +52,8 @@ public sealed class GitHubController : ArchiController {
return releaseResponse != null ? Ok(new GenericResponse<GitHubReleaseResponse>(new GitHubReleaseResponse(releaseResponse))) : StatusCode((int) HttpStatusCode.ServiceUnavailable, new GenericResponse(false, Strings.FormatErrorRequestFailedTooManyTimes(WebBrowser.MaxTries)));
}
/// <summary>
/// Fetches specific GitHub release of ASF project. Use "latest" for latest stable release.
/// </summary>
/// <remarks>
/// This is internal API being utilizied by our ASF-ui IPC frontend. You should not depend on existence of any /Api/WWW endpoints as they can disappear and change anytime.
/// </remarks>
[EndpointDescription("This is internal API being utilizied by our ASF-ui IPC frontend. You should not depend on existence of any /Api/WWW endpoints as they can disappear and change anytime")]
[EndpointSummary("Fetches specific GitHub release of ASF project. Use \"latest\" for latest stable release")]
[HttpGet("Release/{version:required}")]
[ProducesResponseType<GenericResponse<GitHubReleaseResponse>>((int) HttpStatusCode.OK)]
[ProducesResponseType<GenericResponse>((int) HttpStatusCode.BadRequest)]
@@ -90,12 +83,8 @@ public sealed class GitHubController : ArchiController {
return releaseResponse != null ? Ok(new GenericResponse<GitHubReleaseResponse>(new GitHubReleaseResponse(releaseResponse))) : StatusCode((int) HttpStatusCode.ServiceUnavailable, new GenericResponse(false, Strings.FormatErrorRequestFailedTooManyTimes(WebBrowser.MaxTries)));
}
/// <summary>
/// Fetches history of specific GitHub page from ASF project.
/// </summary>
/// <remarks>
/// This is internal API being utilizied by our ASF-ui IPC frontend. You should not depend on existence of any /Api/WWW endpoints as they can disappear and change anytime.
/// </remarks>
[EndpointDescription("This is internal API being utilizied by our ASF-ui IPC frontend. You should not depend on existence of any /Api/WWW endpoints as they can disappear and change anytime")]
[EndpointSummary("Fetches history of specific GitHub page from ASF project")]
[HttpGet("Wiki/History/{page:required}")]
[ProducesResponseType<GenericResponse<string>>((int) HttpStatusCode.OK)]
[ProducesResponseType<GenericResponse>((int) HttpStatusCode.BadRequest)]
@@ -110,13 +99,8 @@ public sealed class GitHubController : ArchiController {
return revisions != null ? revisions.Count > 0 ? Ok(new GenericResponse<ImmutableDictionary<string, DateTime>>(revisions.ToImmutableDictionary())) : BadRequest(new GenericResponse(false, Strings.FormatErrorIsInvalid(nameof(page)))) : StatusCode((int) HttpStatusCode.ServiceUnavailable, new GenericResponse(false, Strings.FormatErrorRequestFailedTooManyTimes(WebBrowser.MaxTries)));
}
/// <summary>
/// Fetches specific GitHub page of ASF project.
/// </summary>
/// <remarks>
/// This is internal API being utilizied by our ASF-ui IPC frontend. You should not depend on existence of any /Api/WWW endpoints as they can disappear and change anytime.
/// Specifying revision is optional - when not specified, will fetch latest available. If specified revision is invalid, GitHub will automatically fetch the latest revision as well.
/// </remarks>
[EndpointDescription("This is internal API being utilizied by our ASF-ui IPC frontend. You should not depend on existence of any /Api/WWW endpoints as they can disappear and change anytime. Specifying revision is optional - when not specified, will fetch latest available. If specified revision is invalid, GitHub will automatically fetch the latest revision as well")]
[EndpointSummary("Fetches specific GitHub page of ASF project")]
[HttpGet("Wiki/Page/{page:required}")]
[ProducesResponseType<GenericResponse<string>>((int) HttpStatusCode.OK)]
[ProducesResponseType<GenericResponse>((int) HttpStatusCode.BadRequest)]

View File

@@ -26,6 +26,7 @@ using System.Net;
using System.Threading;
using System.Threading.Tasks;
using ArchiSteamFarm.IPC.Responses;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Diagnostics.HealthChecks;
@@ -43,6 +44,8 @@ public sealed class HealthCheckController : ControllerBase {
HealthCheckService = healthCheckService;
}
[EndpointDescription("This endpoint can be called in order to easily check whether the program is up")]
[EndpointSummary("Fetches current application's status")]
[HttpGet]
[ProducesResponseType(typeof(HealthCheckResponse), (int) HttpStatusCode.OK)]
[ProducesResponseType(typeof(HealthCheckResponse), (int) HttpStatusCode.ServiceUnavailable)]

View File

@@ -28,15 +28,14 @@ using System.Net;
using ArchiSteamFarm.IPC.Integration;
using ArchiSteamFarm.IPC.Responses;
using ArchiSteamFarm.Localization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace ArchiSteamFarm.IPC.Controllers.Api;
[Route("Api/IPC/Bans")]
public sealed class IPCBansController : ArchiController {
/// <summary>
/// Clears the list of all IP addresses currently blocked by ASFs IPC module
/// </summary>
[EndpointSummary("Clears the list of all IP addresses currently blocked by ASFs IPC module")]
[HttpDelete]
[ProducesResponseType<GenericResponse>((int) HttpStatusCode.OK)]
public ActionResult<GenericResponse> Delete() {
@@ -45,9 +44,7 @@ public sealed class IPCBansController : ArchiController {
return Ok(new GenericResponse(true));
}
/// <summary>
/// Removes an IP address from the list of addresses currently blocked by ASFs IPC module
/// </summary>
[EndpointSummary("Removes an IP address from the list of addresses currently blocked by ASFs IPC module")]
[HttpDelete("{ipAddress:required}")]
[ProducesResponseType<GenericResponse>((int) HttpStatusCode.OK)]
[ProducesResponseType<GenericResponse>((int) HttpStatusCode.BadRequest)]
@@ -67,9 +64,7 @@ public sealed class IPCBansController : ArchiController {
return Ok(new GenericResponse(true));
}
/// <summary>
/// Gets all IP addresses currently blocked by ASFs IPC module
/// </summary>
[EndpointSummary("Gets all IP addresses currently blocked by ASFs IPC module")]
[HttpGet]
[ProducesResponseType<GenericResponse<IReadOnlySet<string>>>((int) HttpStatusCode.OK)]
public ActionResult<GenericResponse<IReadOnlySet<string>>> Get() => Ok(new GenericResponse<IReadOnlySet<string>>(ApiAuthenticationMiddleware.GetCurrentlyBannedIPs().Select(static ip => ip.ToString()).ToHashSet()));

View File

@@ -6,7 +6,7 @@
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2025 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -24,6 +24,7 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Net;
using System.Net.WebSockets;
@@ -37,6 +38,7 @@ using ArchiSteamFarm.Localization;
using ArchiSteamFarm.NLog;
using ArchiSteamFarm.NLog.Targets;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Hosting;
@@ -54,16 +56,12 @@ public sealed class NLogController : ArchiController {
ApplicationLifetime = applicationLifetime;
}
/// <summary>
/// Fetches ASF log file, this works on assumption that the log file is in fact generated, as user could disable it through custom configuration.
/// </summary>
/// <param name="count">Maximum amount of lines from the log file returned. The respone naturally might have less amount than specified, if you've read whole file already.</param>
/// <param name="lastAt">Ending index, used for pagination. Omit it for the first request, then initialize to TotalLines returned, and on every following request subtract count that you've used in the previous request from it until you hit 0 or less, which means you've read whole file already.</param>
[EndpointSummary("Fetches ASF log file, this works on assumption that the log file is in fact generated, as user could disable it through custom configuration")]
[HttpGet("File")]
[ProducesResponseType<GenericResponse<GenericResponse<LogResponse>>>((int) HttpStatusCode.OK)]
[ProducesResponseType<GenericResponse>((int) HttpStatusCode.BadRequest)]
[ProducesResponseType<GenericResponse>((int) HttpStatusCode.ServiceUnavailable)]
public async Task<ActionResult<GenericResponse>> FileGet(int count = 100, int lastAt = 0) {
public async Task<ActionResult<GenericResponse>> FileGet([Description("Maximum amount of lines from the log file returned. The respone naturally might have less amount than specified, if you've read whole file already")] int count = 100, [Description("Ending index, used for pagination. Omit it for the first request, then initialize to TotalLines returned, and on every following request subtract count that you've used in the previous request from it until you hit 0 or less, which means you've read whole file already")] int lastAt = 0) {
if (count <= 0) {
return BadRequest(new GenericResponse(false, Strings.FormatErrorIsInvalid(nameof(count))));
}
@@ -91,12 +89,8 @@ public sealed class NLogController : ArchiController {
return Ok(new GenericResponse<LogResponse>(new LogResponse(lines.Length, lines[startFrom..lastAt])));
}
/// <summary>
/// Fetches ASF log in realtime.
/// </summary>
/// <remarks>
/// This API endpoint requires a websocket connection.
/// </remarks>
[EndpointDescription("This API endpoint requires a websocket connection")]
[EndpointSummary("Fetches ASF log in realtime")]
[HttpGet]
[ProducesResponseType<IEnumerable<GenericResponse<string>>>((int) HttpStatusCode.OK)]
[ProducesResponseType<GenericResponse>((int) HttpStatusCode.BadRequest)]

View File

@@ -31,15 +31,14 @@ using ArchiSteamFarm.Localization;
using ArchiSteamFarm.Plugins;
using ArchiSteamFarm.Plugins.Interfaces;
using ArchiSteamFarm.Steam.Interaction;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace ArchiSteamFarm.IPC.Controllers.Api;
[Route("Api/Plugins")]
public sealed class PluginsController : ArchiController {
/// <summary>
/// Gets active plugins loaded into the process.
/// </summary>
[EndpointSummary("Gets active plugins loaded into the process")]
[HttpGet]
[ProducesResponseType<GenericResponse<IReadOnlyCollection<IPlugin>>>((int) HttpStatusCode.OK)]
public ActionResult<GenericResponse<IReadOnlyCollection<IPlugin>>> PluginsGet([FromQuery] bool official = true, [FromQuery] bool custom = true) {
@@ -60,9 +59,7 @@ public sealed class PluginsController : ArchiController {
return Ok(new GenericResponse<IReadOnlyCollection<IPlugin>>(result));
}
/// <summary>
/// Makes ASF update selected plugins.
/// </summary>
[EndpointSummary("Makes ASF update selected plugins")]
[HttpPost("Update")]
[ProducesResponseType<GenericResponse<string>>((int) HttpStatusCode.OK)]
public async Task<ActionResult<GenericResponse<string>>> UpdatePost([FromBody] PluginUpdateRequest request) {

View File

@@ -26,15 +26,14 @@ using System.Net;
using System.Text.Json;
using ArchiSteamFarm.Core;
using ArchiSteamFarm.IPC.Responses;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace ArchiSteamFarm.IPC.Controllers.Api;
[Route("Api/Storage/{key:required}")]
public sealed class StorageController : ArchiController {
/// <summary>
/// Deletes entry under specified key from ASF's persistent KeyValue JSON storage.
/// </summary>
[EndpointSummary("Deletes entry under specified key from ASF's persistent KeyValue JSON storage")]
[HttpDelete]
[ProducesResponseType<GenericResponse>((int) HttpStatusCode.OK)]
public ActionResult<GenericResponse> StorageDelete(string key) {
@@ -49,9 +48,7 @@ public sealed class StorageController : ArchiController {
return Ok(new GenericResponse(true));
}
/// <summary>
/// Loads entry under specified key from ASF's persistent KeyValue JSON storage.
/// </summary>
[EndpointSummary("Loads entry under specified key from ASF's persistent KeyValue JSON storage")]
[HttpGet]
[ProducesResponseType<GenericResponse<JsonElement?>>((int) HttpStatusCode.OK)]
public ActionResult<GenericResponse> StorageGet(string key) {
@@ -66,10 +63,7 @@ public sealed class StorageController : ArchiController {
return Ok(new GenericResponse<JsonElement?>(true, value.ValueKind != JsonValueKind.Undefined ? value : null));
}
/// <summary>
/// Saves entry under specified key in ASF's persistent KeyValue JSON storage.
/// </summary>
[Consumes("application/json")]
[EndpointSummary("Saves entry under specified key in ASF's persistent KeyValue JSON storage")]
[HttpPost]
[ProducesResponseType<GenericResponse>((int) HttpStatusCode.OK)]
public ActionResult<GenericResponse> StoragePost(string key, [FromBody] JsonElement value) {

View File

@@ -26,18 +26,15 @@ using System.Diagnostics.CodeAnalysis;
using System.Net;
using ArchiSteamFarm.IPC.Responses;
using ArchiSteamFarm.Localization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace ArchiSteamFarm.IPC.Controllers.Api;
[Route("Api/Structure")]
public sealed class StructureController : ArchiController {
/// <summary>
/// Fetches structure of given type.
/// </summary>
/// <remarks>
/// Structure is defined as a representation of given object in its default state.
/// </remarks>
[EndpointDescription("Structure is defined as a representation of given object in its default state")]
[EndpointSummary("Fetches structure of given type")]
[HttpGet("{structure:required}")]
[ProducesResponseType<GenericResponse<object>>((int) HttpStatusCode.OK)]
[ProducesResponseType<GenericResponse>((int) HttpStatusCode.BadRequest)]

View File

@@ -33,15 +33,14 @@ using ArchiSteamFarm.Localization;
using ArchiSteamFarm.Steam;
using ArchiSteamFarm.Steam.Data;
using ArchiSteamFarm.Steam.Security;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace ArchiSteamFarm.IPC.Controllers.Api;
[Route("Api/Bot/{botNames:required}/TwoFactorAuthentication")]
public sealed class TwoFactorAuthenticationController : ArchiController {
/// <summary>
/// Fetches pending 2FA confirmations of given bots, requires ASF 2FA module to be active on them.
/// </summary>
[EndpointSummary("Fetches pending 2FA confirmations of given bots, requires ASF 2FA module to be active on them")]
[HttpGet("Confirmations")]
[ProducesResponseType<GenericResponse<IReadOnlyDictionary<string, GenericResponse<IReadOnlyCollection<Confirmation>>>>>((int) HttpStatusCode.OK)]
[ProducesResponseType<GenericResponse>((int) HttpStatusCode.BadRequest)]
@@ -66,10 +65,7 @@ public sealed class TwoFactorAuthenticationController : ArchiController {
return Ok(new GenericResponse<IReadOnlyDictionary<string, GenericResponse<IReadOnlyCollection<Confirmation>>>>(result));
}
/// <summary>
/// Handles 2FA confirmations of given bots, requires ASF 2FA module to be active on them.
/// </summary>
[Consumes("application/json")]
[EndpointSummary("Handles 2FA confirmations of given bots, requires ASF 2FA module to be active on them")]
[HttpPost("Confirmations")]
[ProducesResponseType<GenericResponse<IReadOnlyDictionary<string, GenericResponse<IReadOnlyCollection<Confirmation>>>>>((int) HttpStatusCode.OK)]
[ProducesResponseType<GenericResponse>((int) HttpStatusCode.BadRequest)]
@@ -99,9 +95,7 @@ public sealed class TwoFactorAuthenticationController : ArchiController {
return Ok(new GenericResponse<IReadOnlyDictionary<string, GenericResponse<IReadOnlyCollection<Confirmation>>>>(result));
}
/// <summary>
/// Deletes the MobileAuthenticator of given bots if an ASF 2FA module is active on them.
/// </summary>
[EndpointSummary("Deletes the MobileAuthenticator of given bots if an ASF 2FA module is active on them")]
[HttpDelete]
[ProducesResponseType<GenericResponse<IReadOnlyDictionary<string, GenericResponse<string>>>>((int) HttpStatusCode.OK)]
[ProducesResponseType<GenericResponse>((int) HttpStatusCode.BadRequest)]
@@ -126,10 +120,7 @@ public sealed class TwoFactorAuthenticationController : ArchiController {
return Ok(new GenericResponse<IReadOnlyDictionary<string, GenericResponse<string>>>(result));
}
/// <summary>
/// Imports a MobileAuthenticator into the ASF 2FA module of a given bot.
/// </summary>
[Consumes("application/json")]
[EndpointSummary("Imports a MobileAuthenticator into the ASF 2FA module of a given bot")]
[HttpPost]
[ProducesResponseType<GenericResponse<IReadOnlyDictionary<string, GenericResponse>>>((int) HttpStatusCode.OK)]
[ProducesResponseType<GenericResponse>((int) HttpStatusCode.BadRequest)]
@@ -155,9 +146,7 @@ public sealed class TwoFactorAuthenticationController : ArchiController {
return Ok(new GenericResponse<IReadOnlyDictionary<string, GenericResponse>>(result));
}
/// <summary>
/// Fetches 2FA tokens of given bots, requires ASF 2FA module to be active on them.
/// </summary>
[EndpointSummary("Fetches 2FA tokens of given bots, requires ASF 2FA module to be active on them")]
[HttpGet("Token")]
[ProducesResponseType<GenericResponse<IReadOnlyDictionary<string, GenericResponse<string>>>>((int) HttpStatusCode.OK)]
[ProducesResponseType<GenericResponse>((int) HttpStatusCode.BadRequest)]

View File

@@ -32,18 +32,15 @@ using System.Text.Json.Serialization;
using ArchiSteamFarm.Core;
using ArchiSteamFarm.IPC.Responses;
using ArchiSteamFarm.Localization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace ArchiSteamFarm.IPC.Controllers.Api;
[Route("Api/Type")]
public sealed class TypeController : ArchiController {
/// <summary>
/// Fetches type info of given type.
/// </summary>
/// <remarks>
/// Type info is defined as a representation of given object with its fields and properties being assigned to a string value that defines their type.
/// </remarks>
[EndpointDescription("Type info is defined as a representation of given object with its fields and properties being assigned to a string value that defines their type")]
[EndpointSummary("Fetches type info of given type")]
[HttpGet("{type:required}")]
[ProducesResponseType<GenericResponse<TypeResponse>>((int) HttpStatusCode.OK)]
[ProducesResponseType<GenericResponse>((int) HttpStatusCode.BadRequest)]

View File

@@ -1,52 +0,0 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// |
// http://www.apache.org/licenses/LICENSE-2.0
// |
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
using System;
using System.Reflection;
using JetBrains.Annotations;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace ArchiSteamFarm.IPC.Integration;
[UsedImplicitly]
internal sealed class CustomAttributesSchemaFilter : ISchemaFilter {
public void Apply(OpenApiSchema schema, SchemaFilterContext context) {
ArgumentNullException.ThrowIfNull(schema);
ArgumentNullException.ThrowIfNull(context);
ICustomAttributeProvider attributesProvider;
if (context.MemberInfo != null) {
attributesProvider = context.MemberInfo;
} else if (context.ParameterInfo != null) {
attributesProvider = context.ParameterInfo;
} else {
return;
}
foreach (CustomSwaggerAttribute customSwaggerAttribute in attributesProvider.GetCustomAttributes(typeof(CustomSwaggerAttribute), true)) {
customSwaggerAttribute.Apply(schema);
}
}
}

View File

@@ -1,42 +0,0 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// |
// http://www.apache.org/licenses/LICENSE-2.0
// |
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
using System;
using System.Reflection;
using JetBrains.Annotations;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace ArchiSteamFarm.IPC.Integration;
[UsedImplicitly]
internal sealed class ReadOnlyFixesSchemaFilter : ISchemaFilter {
public void Apply(OpenApiSchema schema, SchemaFilterContext context) {
ArgumentNullException.ThrowIfNull(schema);
ArgumentNullException.ThrowIfNull(context);
if (schema.ReadOnly && context.MemberInfo is PropertyInfo { CanWrite: true }) {
schema.ReadOnly = false;
}
}
}

View File

@@ -0,0 +1,58 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2022-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using ArchiSteamFarm.IPC.Integration;
using ArchiSteamFarm.Storage;
using JetBrains.Annotations;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi.Models;
namespace ArchiSteamFarm.IPC.OpenApi;
#pragma warning disable CA1812 // False positive, the class is used internally
[UsedImplicitly]
internal sealed class DocumentTransformer : IOpenApiDocumentTransformer {
public Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken) {
ArgumentNullException.ThrowIfNull(document);
ArgumentNullException.ThrowIfNull(context);
document.Info ??= new OpenApiInfo();
document.Info.Title = $"{SharedInfo.AssemblyName} API";
document.Info.Version = SharedInfo.Version.ToString();
document.Info.Contact ??= new OpenApiContact();
document.Info.Contact.Name = SharedInfo.GithubRepo;
document.Info.Contact.Url = new Uri(SharedInfo.ProjectURL);
document.Info.License ??= new OpenApiLicense();
document.Info.License.Name = SharedInfo.LicenseName;
document.Info.License.Url = new Uri(SharedInfo.LicenseURL);
document.Components ??= new OpenApiComponents();
document.Components.SecuritySchemes ??= new Dictionary<string, OpenApiSecurityScheme>(1);
document.Components.SecuritySchemes.Add(
nameof(GlobalConfig.IPCPassword), new OpenApiSecurityScheme {
Description = $"{nameof(GlobalConfig.IPCPassword)} authentication using request headers. Check {SharedInfo.ProjectURL}/wiki/IPC#authentication for more info.",
In = ParameterLocation.Header,
Name = ApiAuthenticationMiddleware.HeadersField,
Type = SecuritySchemeType.ApiKey
}
);
return Task.CompletedTask;
}
}
#pragma warning restore CA1812 // False positive, the class is used internally

View File

@@ -0,0 +1,52 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2022-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using ArchiSteamFarm.Storage;
using JetBrains.Annotations;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi.Models;
namespace ArchiSteamFarm.IPC.OpenApi;
#pragma warning disable CA1812 // False positive, the class is used internally
[UsedImplicitly]
internal sealed class OperationTransformer : IOpenApiOperationTransformer {
public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransformerContext context, CancellationToken cancellationToken) {
ArgumentNullException.ThrowIfNull(operation);
ArgumentNullException.ThrowIfNull(context);
if (context.Description.RelativePath?.StartsWith("Api", StringComparison.OrdinalIgnoreCase) == true) {
operation.Security ??= new List<OpenApiSecurityRequirement>(1);
operation.Security.Add(
new OpenApiSecurityRequirement {
{
new OpenApiSecurityScheme {
Reference = new OpenApiReference {
Id = nameof(GlobalConfig.IPCPassword),
Type = ReferenceType.SecurityScheme
}
},
Array.Empty<string>()
}
}
);
}
return Task.CompletedTask;
}
}
#pragma warning restore CA1812 // False positive, the class is used internally

View File

@@ -1,4 +1,4 @@
// ----------------------------------------------------------------------------------------------
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
@@ -6,7 +6,7 @@
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2025 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -23,36 +23,63 @@
using System;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
using ArchiSteamFarm.IPC.Integration;
using JetBrains.Annotations;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Extensions;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace ArchiSteamFarm.IPC.Integration;
namespace ArchiSteamFarm.IPC.OpenApi;
#pragma warning disable CA1812 // False positive, the class is used internally
[UsedImplicitly]
internal sealed class EnumSchemaFilter : ISchemaFilter {
public void Apply(OpenApiSchema schema, SchemaFilterContext context) {
internal sealed class SchemaTransformer : IOpenApiSchemaTransformer {
public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken) {
ArgumentNullException.ThrowIfNull(schema);
ArgumentNullException.ThrowIfNull(context);
if (context.Type is not { IsEnum: true }) {
ApplyCustomAttributes(schema, context);
ApplyEnumDefinition(schema, context);
return Task.CompletedTask;
}
private static void ApplyCustomAttributes(OpenApiSchema schema, OpenApiSchemaTransformerContext context) {
ArgumentNullException.ThrowIfNull(schema);
ArgumentNullException.ThrowIfNull(context);
if (context.JsonPropertyInfo?.AttributeProvider == null) {
return;
}
if (context.Type.IsDefined(typeof(FlagsAttribute), false)) {
foreach (CustomSwaggerAttribute customSwaggerAttribute in context.JsonPropertyInfo.AttributeProvider.GetCustomAttributes(typeof(CustomSwaggerAttribute), true)) {
customSwaggerAttribute.Apply(schema);
}
}
private static void ApplyEnumDefinition(OpenApiSchema schema, OpenApiSchemaTransformerContext context) {
ArgumentNullException.ThrowIfNull(schema);
ArgumentNullException.ThrowIfNull(context);
if (context.JsonTypeInfo.Type is not { IsEnum: true }) {
return;
}
if (context.JsonTypeInfo.Type.IsDefined(typeof(FlagsAttribute), false)) {
schema.Format = "flags";
}
OpenApiObject definition = new();
foreach (object? enumValue in context.Type.GetEnumValues()) {
foreach (object? enumValue in context.JsonTypeInfo.Type.GetEnumValues()) {
if (enumValue == null) {
throw new InvalidOperationException(nameof(enumValue));
}
string? enumName = Enum.GetName(context.Type, enumValue);
string? enumName = Enum.GetName(context.JsonTypeInfo.Type, enumValue);
if (string.IsNullOrEmpty(enumName)) {
// Fallback
@@ -68,7 +95,7 @@ internal sealed class EnumSchemaFilter : ISchemaFilter {
continue;
}
IOpenApiPrimitive enumObject;
IOpenApiAny enumObject;
if (TryCast(enumValue, out int intValue)) {
enumObject = new OpenApiInteger(intValue);
@@ -105,3 +132,4 @@ internal sealed class EnumSchemaFilter : ISchemaFilter {
}
}
}
#pragma warning restore CA1812 // False positive, the class is used internally

View File

@@ -21,6 +21,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
@@ -30,17 +31,13 @@ namespace ArchiSteamFarm.IPC.Requests;
[SuppressMessage("ReSharper", "ClassCannotBeInstantiated")]
public sealed class ASFEncryptRequest {
/// <summary>
/// Encryption method used for encrypting this string.
/// </summary>
[Description("Encryption method used for encrypting this string")]
[JsonInclude]
[JsonRequired]
[Required]
public ArchiCryptoHelper.ECryptoMethod CryptoMethod { get; private init; }
/// <summary>
/// String to encrypt with provided <see cref="CryptoMethod" />.
/// </summary>
[Description($"String to encrypt with provided {nameof(CryptoMethod)}")]
[JsonInclude]
[JsonRequired]
[Required]

View File

@@ -21,6 +21,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
@@ -30,17 +31,13 @@ namespace ArchiSteamFarm.IPC.Requests;
[SuppressMessage("ReSharper", "ClassCannotBeInstantiated")]
public sealed class ASFHashRequest {
/// <summary>
/// Hashing method used for hashing this string.
/// </summary>
[Description("Hashing method used for hashing this string")]
[JsonInclude]
[JsonRequired]
[Required]
public ArchiCryptoHelper.EHashingMethod HashingMethod { get; private init; }
/// <summary>
/// String to hash with provided <see cref="HashingMethod" />.
/// </summary>
[Description($"String to hash with provided {nameof(HashingMethod)}")]
[JsonInclude]
[JsonRequired]
[Required]

View File

@@ -21,6 +21,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
@@ -30,9 +31,7 @@ namespace ArchiSteamFarm.IPC.Requests;
[SuppressMessage("ReSharper", "ClassCannotBeInstantiated")]
public sealed class ASFRequest {
/// <summary>
/// ASF's global config structure.
/// </summary>
[Description("ASF's global config structure")]
[JsonInclude]
[JsonRequired]
[Required]

View File

@@ -22,6 +22,7 @@
// limitations under the License.
using System.Collections.Immutable;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
@@ -29,15 +30,11 @@ namespace ArchiSteamFarm.IPC.Requests;
[SuppressMessage("ReSharper", "ClassCannotBeInstantiated")]
public sealed class BotAddLicenseRequest {
/// <summary>
/// A collection (set) of apps (appIDs) to ask license for.
/// </summary>
[Description("A collection (set) of apps (appIDs) to ask license for")]
[JsonInclude]
public ImmutableList<uint>? Apps { get; private init; }
/// <summary>
/// A collection (set) of packages (subIDs) to ask license for.
/// </summary>
[Description("A collection (set) of packages (subIDs) to ask license for")]
[JsonInclude]
public ImmutableList<uint>? Packages { get; private init; }

View File

@@ -22,6 +22,7 @@
// limitations under the License.
using System.Collections.Specialized;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
@@ -30,13 +31,7 @@ namespace ArchiSteamFarm.IPC.Requests;
[SuppressMessage("ReSharper", "ClassCannotBeInstantiated")]
public sealed class BotGamesToRedeemInBackgroundRequest {
/// <summary>
/// A string-string map that maps cd-key to redeem (key) to its name (value).
/// </summary>
/// <remarks>
/// Key in the map must be a valid and unique Steam cd-key.
/// Value in the map must be a non-null and non-empty name of the key (e.g. game's name, but can be anything).
/// </remarks>
[Description("A string-string map that maps cd-key to redeem (key) to its name (value). Key in the map must be a valid and unique Steam cd-key. Value in the map must be a non-null and non-empty name of the key (e.g. game's name, but can be anything)")]
[JsonInclude]
[JsonRequired]
[Required]

View File

@@ -21,6 +21,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
@@ -30,17 +31,13 @@ namespace ArchiSteamFarm.IPC.Requests;
[SuppressMessage("ReSharper", "ClassCannotBeInstantiated")]
public sealed class BotInputRequest {
/// <summary>
/// Specifies the type of the input.
/// </summary>
[Description("Specifies the type of the input")]
[JsonInclude]
[JsonRequired]
[Required]
public ASF.EUserInputType Type { get; private init; }
/// <summary>
/// Specifies the value for given input type (declared in <see cref="Type" />)
/// </summary>
[Description($"Specifies the value for given input type (declared in {nameof(Type)})")]
[JsonInclude]
[JsonRequired]
[Required]

View File

@@ -21,6 +21,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
@@ -28,15 +29,11 @@ namespace ArchiSteamFarm.IPC.Requests;
[SuppressMessage("ReSharper", "ClassCannotBeInstantiated")]
public sealed class BotPauseRequest {
/// <summary>
/// Specifies if pause is permanent or temporary (default).
/// </summary>
[Description("Specifies if pause is permanent or temporary (default)")]
[JsonInclude]
public bool Permanent { get; private init; }
/// <summary>
/// Specifies automatic resume action in given seconds. Default value of 0 disables automatic resume.
/// </summary>
[Description("Specifies automatic resume action in given seconds. Default value of 0 disables automatic resume")]
[JsonInclude]
public ushort ResumeInSeconds { get; private init; }

View File

@@ -22,6 +22,7 @@
// limitations under the License.
using System.Collections.Immutable;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
@@ -30,9 +31,7 @@ namespace ArchiSteamFarm.IPC.Requests;
[SuppressMessage("ReSharper", "ClassCannotBeInstantiated")]
public sealed class BotRedeemRequest {
/// <summary>
/// A collection (set) of keys to redeem.
/// </summary>
[Description("A collection (set) of keys to redeem")]
[JsonInclude]
[JsonRequired]
[Required]

View File

@@ -21,6 +21,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
@@ -29,9 +30,7 @@ namespace ArchiSteamFarm.IPC.Requests;
[SuppressMessage("ReSharper", "ClassCannotBeInstantiated")]
public sealed class BotRenameRequest {
/// <summary>
/// Specifies the new name for the bot. The new name can't be "ASF", neither the one used by any existing bot.
/// </summary>
[Description($"Specifies the new name for the bot. The new name can't be {SharedInfo.ASF}, neither the one used by any existing bot")]
[JsonInclude]
[JsonRequired]
[Required]

View File

@@ -21,6 +21,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
@@ -30,9 +31,7 @@ namespace ArchiSteamFarm.IPC.Requests;
[SuppressMessage("ReSharper", "ClassCannotBeInstantiated")]
public sealed class BotRequest {
/// <summary>
/// ASF's bot config structure.
/// </summary>
[Description("ASF's bot config structure")]
[JsonInclude]
[JsonRequired]
[Required]

View File

@@ -21,6 +21,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
@@ -29,9 +30,7 @@ namespace ArchiSteamFarm.IPC.Requests;
[SuppressMessage("ReSharper", "ClassCannotBeInstantiated")]
public sealed class CommandRequest {
/// <summary>
/// Specifies the command that will be executed by ASF.
/// </summary>
[Description("Specifies the command that will be executed by ASF")]
[JsonInclude]
[JsonRequired]
[Required]

View File

@@ -22,6 +22,7 @@
// limitations under the License.
using System.Collections.Immutable;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
using ArchiSteamFarm.Storage;
@@ -30,21 +31,15 @@ namespace ArchiSteamFarm.IPC.Requests;
[SuppressMessage("ReSharper", "ClassCannotBeInstantiated")]
public sealed class PluginUpdateRequest {
/// <summary>
/// Target update channel. Not required, will default to <see cref="GlobalConfig.UpdateChannel" /> if not provided.
/// </summary>
[Description($"Target update channel. Not required, will default to {nameof(GlobalConfig.UpdateChannel)} if not provided")]
[JsonInclude]
public GlobalConfig.EUpdateChannel? Channel { get; private init; }
/// <summary>
/// Forced update. This allows ASF to potentially downgrade to previous version available on selected <see cref="Channel" />, which isn't permitted normally.
/// </summary>
[Description($"Forced update. This allows ASF to potentially downgrade to previous version available on selected {nameof(Channel)}, which isn't permitted normally")]
[JsonInclude]
public bool Forced { get; private init; }
/// <summary>
/// Target plugins. Not required, will default to plugin update configuration in <see cref="GlobalConfig" /> if not provided.
/// </summary>
[Description($"Target plugins. Not required, will default to plugin update configuration in {nameof(GlobalConfig)} if not provided")]
[JsonInclude]
public ImmutableHashSet<string>? Plugins { get; private init; }

View File

@@ -24,6 +24,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
@@ -38,30 +39,22 @@ namespace ArchiSteamFarm.IPC.Requests;
[SuppressMessage("ReSharper", "ClassCannotBeInstantiated")]
public sealed class TwoFactorAuthenticationConfirmationsRequest {
/// <summary>
/// Specifies the target action, whether we should accept the confirmations (true), or decline them (false).
/// </summary>
[Description("Specifies the target action, whether we should accept the confirmations (true), or decline them (false)")]
[JsonInclude]
[JsonRequired]
[Required]
public bool Accept { get; private init; }
/// <summary>
/// Specifies IDs of the confirmations that we're supposed to handle. CreatorID of the confirmation is equal to ID of the object that triggered it - e.g. ID of the trade offer, or ID of the market listing. If not provided, or empty array, all confirmation IDs are considered for an action.
/// </summary>
[Description("Specifies IDs of the confirmations that we're supposed to handle. CreatorID of the confirmation is equal to ID of the object that triggered it - e.g. ID of the trade offer, or ID of the market listing. If not provided, or empty array, all confirmation IDs are considered for an action")]
[JsonDisallowNull]
[JsonInclude]
public ImmutableHashSet<ulong> AcceptedCreatorIDs { get; private init; } = [];
/// <summary>
/// Specifies the type of confirmations to handle. If not provided, all confirmation types are considered for an action.
/// </summary>
[Description("Specifies the type of confirmations to handle. If not provided, all confirmation types are considered for an action")]
[JsonInclude]
public Confirmation.EConfirmationType? AcceptedType { get; private init; }
/// <summary>
/// A helper property which works the same as <see cref="AcceptedCreatorIDs" /> but with values written as strings - for javascript compatibility purposes. Use either this one, or <see cref="AcceptedCreatorIDs" />, not both.
/// </summary>
[Description($"A helper property which works the same as {nameof(AcceptedCreatorIDs)} but with values written as strings - for javascript compatibility purposes. Use either this one, or {nameof(AcceptedCreatorIDs)}, not both")]
[JsonDisallowNull]
[JsonInclude]
[JsonPropertyName($"{SharedInfo.UlongCompatibilityStringPrefix}{nameof(AcceptedCreatorIDs)}")]
@@ -87,9 +80,7 @@ public sealed class TwoFactorAuthenticationConfirmationsRequest {
}
}
/// <summary>
/// Specifies whether we should wait for the confirmations to arrive, in case they're not available immediately. This option makes sense only if <see cref="AcceptedCreatorIDs" /> is specified as well, and in this case ASF will add a few more tries if needed to ensure that all specified IDs are handled. Useful if confirmations are generated with a delay on Steam network side, which happens fairly often.
/// </summary>
[Description($"Specifies whether we should wait for the confirmations to arrive, in case they're not available immediately. This option makes sense only if {nameof(AcceptedCreatorIDs)} is specified as well, and in this case ASF will add a few more tries if needed to ensure that all specified IDs are handled. Useful if confirmations are generated with a delay on Steam network side, which happens fairly often")]
[JsonInclude]
public bool WaitIfNeeded { get; private init; }

View File

@@ -21,6 +21,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
using ArchiSteamFarm.Storage;
@@ -29,15 +30,11 @@ namespace ArchiSteamFarm.IPC.Requests;
[SuppressMessage("ReSharper", "ClassCannotBeInstantiated")]
public sealed class UpdateRequest {
/// <summary>
/// Target update channel. Not required, will default to UpdateChannel in GlobalConfig if not provided.
/// </summary>
[Description("Target update channel. Not required, will default to UpdateChannel in GlobalConfig if not provided")]
[JsonInclude]
public GlobalConfig.EUpdateChannel? Channel { get; private init; }
/// <summary>
/// Forced update. This allows ASF to potentially downgrade to previous version available on selected <see cref="Channel" />, which isn't permitted normally.
/// </summary>
[Description($"Forced update. This allows ASF to potentially downgrade to previous version available on selected {nameof(Channel)}, which isn't permitted normally")]
[JsonInclude]
public bool Forced { get; private init; }

View File

@@ -22,6 +22,7 @@
// limitations under the License.
using System;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
using ArchiSteamFarm.Storage;
@@ -29,57 +30,43 @@ using ArchiSteamFarm.Storage;
namespace ArchiSteamFarm.IPC.Responses;
public sealed class ASFResponse {
/// <summary>
/// ASF's build variant.
/// </summary>
[Description("ASF's build variant")]
[JsonInclude]
[JsonRequired]
[Required]
public string BuildVariant { get; private init; }
/// <summary>
/// A value specifying whether this variant of ASF is capable of auto-update.
/// </summary>
[Description("A value specifying whether this variant of ASF is capable of auto-update")]
[JsonInclude]
[JsonRequired]
[Required]
public bool CanUpdate { get; private init; }
/// <summary>
/// Currently loaded ASF's global config.
/// </summary>
[Description("Currently loaded ASF's global config")]
[JsonInclude]
[JsonRequired]
[Required]
public GlobalConfig GlobalConfig { get; private init; }
/// <summary>
/// Current amount of managed memory being used by the process, in kilobytes.
/// </summary>
[Description("Current amount of managed memory being used by the process, in kilobytes")]
[JsonInclude]
[JsonRequired]
[Required]
public uint MemoryUsage { get; private init; }
/// <summary>
/// Start date of the process.
/// </summary>
[Description("Start date of the process")]
[JsonInclude]
[JsonRequired]
[Required]
public DateTime ProcessStartTime { get; private init; }
/// <summary>
/// Boolean value specifying whether ASF has been started with a --service parameter.
/// </summary>
[Description("Boolean value specifying whether ASF has been started with a --service parameter")]
[JsonInclude]
[JsonRequired]
[Required]
public bool Service { get; private init; }
/// <summary>
/// ASF version of currently running binary.
/// </summary>
[Description("ASF version of currently running binary")]
[JsonInclude]
[JsonRequired]
[Required]

View File

@@ -30,11 +30,13 @@ using SteamKit2;
namespace ArchiSteamFarm.IPC.Responses;
public sealed class AddLicenseResult {
[Description("Additional result of the license request")]
[JsonInclude]
[JsonRequired]
[Required]
public EPurchaseResultDetail PurchaseResultDetail { get; private init; }
[Description("Main result of the license request")]
[JsonInclude]
[JsonRequired]
[Required]

View File

@@ -23,20 +23,17 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel;
using System.Text.Json.Serialization;
namespace ArchiSteamFarm.IPC.Responses;
public sealed class BotAddLicenseResponse {
/// <summary>
/// A collection (set) of apps (appIDs) to ask license for.
/// </summary>
[Description("A collection (set) of apps (appIDs) to ask license for")]
[JsonInclude]
public ImmutableDictionary<uint, AddLicenseResult>? Apps { get; private init; }
/// <summary>
/// A collection (set) of packages (subIDs) to ask license for.
/// </summary>
[Description("A collection (set) of packages (subIDs) to ask license for")]
[JsonInclude]
public ImmutableDictionary<uint, AddLicenseResult>? Packages { get; private init; }

View File

@@ -23,20 +23,17 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel;
using System.Text.Json.Serialization;
namespace ArchiSteamFarm.IPC.Responses;
public sealed class GamesToRedeemInBackgroundResponse {
/// <summary>
/// Keys that were redeemed and not used during the process, if available.
/// </summary>
[Description("Keys that were redeemed and not used during the process, if available")]
[JsonInclude]
public ImmutableDictionary<string, string>? UnusedKeys { get; private init; }
/// <summary>
/// Keys that were redeemed and used during the process, if available.
/// </summary>
[Description("Keys that were redeemed and used during the process, if available")]
[JsonInclude]
public ImmutableDictionary<string, string>? UsedKeys { get; private init; }

View File

@@ -21,6 +21,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
using ArchiSteamFarm.Localization;
@@ -28,12 +29,7 @@ using ArchiSteamFarm.Localization;
namespace ArchiSteamFarm.IPC.Responses;
public sealed class GenericResponse<T> : GenericResponse {
/// <summary>
/// The actual result of the request, if available.
/// </summary>
/// <remarks>
/// The type of the result depends on the API endpoint that you've called.
/// </remarks>
[Description("The actual result of the request, if available. The type of the result depends on the API endpoint that you've called")]
[JsonInclude]
public T? Result { get; private init; }
@@ -47,18 +43,11 @@ public sealed class GenericResponse<T> : GenericResponse {
}
public class GenericResponse {
/// <summary>
/// A message that describes what happened with the request, if available.
/// </summary>
/// <remarks>
/// This property will provide exact reason for majority of expected failures.
/// </remarks>
[Description("A message that describes what happened with the request, if available. This property will provide exact reason for majority of expected failures")]
[JsonInclude]
public string? Message { get; private init; }
/// <summary>
/// Boolean type that specifies if the request has succeeded.
/// </summary>
[Description("Boolean type that specifies if the request has succeeded")]
[JsonInclude]
[JsonRequired]
[Required]

View File

@@ -22,6 +22,7 @@
// limitations under the License.
using System;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
using ArchiSteamFarm.Web.GitHub.Data;
@@ -29,33 +30,25 @@ using ArchiSteamFarm.Web.GitHub.Data;
namespace ArchiSteamFarm.IPC.Responses;
public sealed class GitHubReleaseResponse {
/// <summary>
/// Changelog of the release rendered in HTML.
/// </summary>
[Description("Changelog of the release rendered in HTML")]
[JsonInclude]
[JsonRequired]
[Required]
public string ChangelogHTML { get; private init; }
/// <summary>
/// Date of the release.
/// </summary>
[Description("Date of the release")]
[JsonInclude]
[JsonRequired]
[Required]
public DateTime ReleasedAt { get; private init; }
/// <summary>
/// Boolean value that specifies whether the build is stable or not (pre-release).
/// </summary>
[Description("Boolean value that specifies whether the build is stable or not (pre-release)")]
[JsonInclude]
[JsonRequired]
[Required]
public bool Stable { get; private init; }
/// <summary>
/// Version of the release.
/// </summary>
[Description("Version of the release")]
[JsonInclude]
[JsonRequired]
[Required]

View File

@@ -22,6 +22,7 @@
// limitations under the License.
using System;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Diagnostics.HealthChecks;
@@ -29,10 +30,11 @@ using Microsoft.Extensions.Diagnostics.HealthChecks;
namespace ArchiSteamFarm.IPC.Responses;
public sealed class HealthCheckResponse {
[Description($"{nameof(Status)} written as text")]
[JsonInclude]
[Required]
public string StatusText => Status.ToString();
[Description("Health status of the application")]
[JsonInclude]
[JsonRequired]
[Required]

View File

@@ -24,23 +24,20 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
namespace ArchiSteamFarm.IPC.Responses;
public sealed class LogResponse {
/// <summary>
/// Content of the log file which consists of lines read from it - in chronological order.
/// </summary>
[Description("Content of the log file which consists of lines read from it - in chronological order")]
[JsonInclude]
[JsonRequired]
[Required]
public ImmutableList<string> Content { get; private init; }
/// <summary>
/// Total number of lines of the log file returned, can be used as an index for future requests.
/// </summary>
[Description("Total number of lines of the log file returned, can be used as an index for future requests")]
[JsonInclude]
[JsonRequired]
[Required]

View File

@@ -21,6 +21,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Net;
using System.Text.Json.Serialization;
@@ -28,17 +29,13 @@ using System.Text.Json.Serialization;
namespace ArchiSteamFarm.IPC.Responses;
public sealed class StatusCodeResponse {
/// <summary>
/// Value indicating whether the status is permanent. If yes, retrying the request with exactly the same payload doesn't make sense due to a permanent problem (e.g. ASF misconfiguration).
/// </summary>
[Description("Value indicating whether the status is permanent. If yes, retrying the request with exactly the same payload doesn't make sense due to a permanent problem (e.g. ASF misconfiguration)")]
[JsonInclude]
[JsonRequired]
[Required]
public bool Permanent { get; private init; }
/// <summary>
/// Status code transmitted in addition to the one in HTTP spec.
/// </summary>
[Description("Status code transmitted in addition to the one in HTTP spec")]
[JsonInclude]
[JsonRequired]
[Required]

View File

@@ -24,35 +24,21 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel;
using System.Text.Json.Serialization;
namespace ArchiSteamFarm.IPC.Responses;
public sealed class TypeProperties {
/// <summary>
/// Base type of given type, if available.
/// </summary>
/// <remarks>
/// This can be used for determining how the body of the response should be interpreted.
/// </remarks>
[Description("Base type of given type, if available. This can be used for determining how the body of the response should be interpreted")]
[JsonInclude]
public string? BaseType { get; private init; }
/// <summary>
/// Custom attributes of given type, if available.
/// </summary>
/// <remarks>
/// This can be used for determining main enum type if <see cref="BaseType" /> is <see cref="Enum" />.
/// </remarks>
[Description($"Custom attributes of given type, if available. This can be used for determining main enum type if {nameof(BaseType)} is {nameof(Enum)}")]
[JsonInclude]
public ImmutableHashSet<string>? CustomAttributes { get; private init; }
/// <summary>
/// Underlying type of given type, if available.
/// </summary>
/// <remarks>
/// This can be used for determining underlying enum type if <see cref="BaseType" /> is <see cref="Enum" />.
/// </remarks>
[Description($"Underlying type of given type, if available. This can be used for determining underlying enum type if {nameof(BaseType)} is {nameof(Enum)}")]
[JsonInclude]
public string? UnderlyingType { get; private init; }

View File

@@ -24,28 +24,20 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
namespace ArchiSteamFarm.IPC.Responses;
public sealed class TypeResponse {
/// <summary>
/// A string-string map representing a decomposition of given type.
/// </summary>
/// <remarks>
/// The actual structure of this field depends on the type that was requested. You can determine that type based on <see cref="Properties" /> metadata.
/// For enums, keys are friendly names while values are underlying values of those names.
/// For objects, keys are non-private fields and properties, while values are underlying types of those.
/// </remarks>
[Description($"A string-string map representing a decomposition of given type. The actual structure of this field depends on the type that was requested. You can determine that type based on {nameof(Properties)} metadata. For enums, keys are friendly names while values are underlying values of those names. For objects, keys are non-private fields and properties, while values are underlying types of those")]
[JsonInclude]
[JsonRequired]
[Required]
public ImmutableDictionary<string, string> Body { get; private init; }
/// <summary>
/// Metadata of given type.
/// </summary>
[Description("Metadata of given type")]
[JsonInclude]
[JsonRequired]
[Required]

View File

@@ -22,7 +22,6 @@
// limitations under the License.
using System;
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using JetBrains.Annotations;
@@ -39,7 +38,6 @@ public interface IPlugin {
/// </summary>
/// <returns>String that will be used as the name of this plugin.</returns>
[JsonInclude]
[Required]
string Name { get; }
/// <summary>
@@ -48,7 +46,6 @@ public interface IPlugin {
/// </summary>
/// <returns>Version that will be shown to the user when plugin is loaded.</returns>
[JsonInclude]
[Required]
Version Version { get; }
/// <summary>

View File

@@ -38,7 +38,6 @@ public static class SharedInfo {
internal const string ArchivalLogsDirectory = "logs";
internal const string ASF = nameof(ASF);
internal const ulong ASFGroupSteamID = 103582791440160998;
internal const string AssemblyDocumentation = $"{AssemblyName}.xml";
internal const string AssemblyName = nameof(ArchiSteamFarm);
internal const string DatabaseExtension = ".db";
internal const string DebugDirectory = "debug";

View File

@@ -106,12 +106,10 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
[JsonInclude]
[PublicAPI]
[Required]
public string BotName { get; }
[JsonInclude]
[PublicAPI]
[Required]
public CardsFarmer CardsFarmer { get; }
[JsonIgnore]
@@ -120,12 +118,10 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
[JsonInclude]
[PublicAPI]
[Required]
public uint GamesToRedeemInBackgroundCount => BotDatabase.GamesToRedeemInBackgroundCount;
[JsonInclude]
[PublicAPI]
[Required]
public bool HasMobileAuthenticator => BotDatabase.MobileAuthenticator != null;
[JsonIgnore]
@@ -138,12 +134,10 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
[JsonInclude]
[PublicAPI]
[Required]
public bool IsConnectedAndLoggedOn => SteamClient.SteamID != null;
[JsonInclude]
[PublicAPI]
[Required]
public bool IsPlayingPossible => !PlayingBlocked && !LibraryLocked;
[JsonInclude]
@@ -153,7 +147,6 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
[JsonInclude]
[JsonPropertyName($"{SharedInfo.UlongCompatibilityStringPrefix}{nameof(SteamID)}")]
[PublicAPI]
[Required]
public string SSteamID => SteamID.ToString(CultureInfo.InvariantCulture);
[JsonIgnore]

View File

@@ -67,18 +67,15 @@ public sealed class CardsFarmer : IAsyncDisposable, IDisposable {
[JsonInclude]
[JsonPropertyName(nameof(CurrentGamesFarming))]
[PublicAPI]
[Required]
public IReadOnlyCollection<Game> CurrentGamesFarmingReadOnly => CurrentGamesFarming;
[JsonInclude]
[JsonPropertyName(nameof(GamesToFarm))]
[PublicAPI]
[Required]
public IReadOnlyCollection<Game> GamesToFarmReadOnly => GamesToFarm;
[JsonInclude]
[PublicAPI]
[Required]
public TimeSpan TimeRemaining {
get {
if (GamesToFarm.Count == 0) {

View File

@@ -29,20 +29,20 @@ namespace ArchiSteamFarm.Steam.Cards;
public sealed class Game : IEquatable<Game> {
[JsonInclude]
[Required]
public uint AppID { get; }
[JsonInclude]
[Required]
public string GameName { get; }
internal readonly byte BadgeLevel;
[JsonInclude]
[JsonRequired]
[Required]
public ushort CardsRemaining { get; internal set; }
[JsonInclude]
[JsonRequired]
[Required]
public float HoursPlayed { get; internal set; }

View File

@@ -22,12 +22,18 @@
// limitations under the License.
using System;
using JetBrains.Annotations;
using SteamKit2;
namespace ArchiSteamFarm.Steam.Data;
public sealed record LicenseData {
[PublicAPI]
public required uint PackageID { get; init; }
[PublicAPI]
public required EPaymentMethod PaymentMethod { get; init; }
[PublicAPI]
public required DateTime TimeCreated { get; init; }
}

View File

@@ -5,6 +5,7 @@
<PackageVersion Include="Humanizer" Version="3.0.0-beta.54" />
<PackageVersion Include="JetBrains.Annotations" Version="2024.3.0" />
<PackageVersion Include="Markdig.Signed" Version="0.39.1" />
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="9.0.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.ResxSourceGenerator" Version="3.11.0-beta1.24527.2" />
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.3.0" />
<PackageVersion Include="MSTest" Version="3.7.0" />
@@ -16,8 +17,7 @@
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.10.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="1.10.0" />
<PackageVersion Include="SteamKit2" Version="3.0.1" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="7.2.0" />
<PackageVersion Include="Swashbuckle.AspNetCore.Annotations" Version="7.2.0" />
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerUI" Version="7.2.0" />
<PackageVersion Include="System.Composition" Version="9.0.0" />
<PackageVersion Include="System.Composition.AttributedModel" Version="9.0.0" />
<PackageVersion Include="System.Linq.Async" Version="6.0.1" />