From a40f9b897ff9af5544d859e35e84ee5a1258f634 Mon Sep 17 00:00:00 2001 From: Dexrn ZacAttack Date: Mon, 5 Jan 2026 00:06:29 -0800 Subject: [PATCH 1/2] feat: implement Version Create API --- README.md | 2 +- .../Endpoints/Version/IVersionEndpoint.cs | 27 +++++++++ .../Endpoints/Version/VersionEndpoint.cs | 59 ++++++++++++++++++- .../ProjectVersionTypeExtensions.cs | 26 ++++++++ .../Extensions/VersionStatusExtensions.cs | 30 ++++++++++ src/Modrinth.Net/Modrinth.Net.csproj | 2 +- src/Modrinth.Net/UploadableFile.cs | 42 +++++++++++++ 7 files changed, 185 insertions(+), 3 deletions(-) create mode 100644 src/Modrinth.Net/Extensions/ProjectVersionTypeExtensions.cs create mode 100644 src/Modrinth.Net/Extensions/VersionStatusExtensions.cs create mode 100644 src/Modrinth.Net/UploadableFile.cs diff --git a/README.md b/README.md index 6258086..a0216eb 100644 --- a/README.md +++ b/README.md @@ -202,7 +202,7 @@ catch (ModrinthApiException e) | Modify a version | PATCH | ❌ | | Delete a version | DELETE | ⚠️ | | Get a version given a version number or ID | GET | ❌ | -| Create a version | POST | ❌ | +| Create a version | POST | ✅ | | Schedule a version | POST | ⚠️ | | Get multiple versions | GET | ✅ | | Add files to version | POST | ❌ | diff --git a/src/Modrinth.Net/Endpoints/Version/IVersionEndpoint.cs b/src/Modrinth.Net/Endpoints/Version/IVersionEndpoint.cs index 8e0c3ae..b43f638 100644 --- a/src/Modrinth.Net/Endpoints/Version/IVersionEndpoint.cs +++ b/src/Modrinth.Net/Endpoints/Version/IVersionEndpoint.cs @@ -1,4 +1,6 @@ using Modrinth.Exceptions; +using Modrinth.Models; +using Modrinth.Models.Enums.Project; using Modrinth.Models.Enums.Version; namespace Modrinth.Endpoints.Version; @@ -53,6 +55,31 @@ public interface IVersionEndpoint Task GetByVersionNumberAsync(string slugOrId, string versionNumber, CancellationToken cancellationToken = default); + /// + /// Creates a version + /// + /// Project ID to create the version on (does not support slugs) + /// List of files to add to this version (must include atleast 1 file) + /// Primary (featured) filename + /// Version name + /// Version number + /// Changelog + /// Modrinth project dependencies + /// Supported Minecraft versions + /// Version type + /// Supported Minecraft modloaders + /// Whether to feature it on the mod page + /// Unknown, but the mod seemingly doesn't show up if this is not VersionStatus.Listed. + /// Unknown + /// + /// The newly created Version + /// Thrown when the API returns an error or the request fails + Task CreateAsync(string projectId, List files, string primaryFile, string name, string versionNumber, + string? changelog, List dependencies, List gameVersions, + ProjectVersionType versionType, List loaders, bool featured, + VersionStatus status, VersionStatus? requestedStatus, + CancellationToken cancellationToken = default); + /// /// Deletes a version by its ID /// diff --git a/src/Modrinth.Net/Endpoints/Version/VersionEndpoint.cs b/src/Modrinth.Net/Endpoints/Version/VersionEndpoint.cs index bbb24bb..4e49dc8 100644 --- a/src/Modrinth.Net/Endpoints/Version/VersionEndpoint.cs +++ b/src/Modrinth.Net/Endpoints/Version/VersionEndpoint.cs @@ -1,7 +1,13 @@ -using Modrinth.Extensions; +using System.Text; +using System.Text.Json; +using Modrinth.Endpoints.Project; +using Modrinth.Extensions; using Modrinth.Helpers; using Modrinth.Http; +using Modrinth.Models; +using Modrinth.Models.Enums.Project; using Modrinth.Models.Enums.Version; +using File = System.IO.File; namespace Modrinth.Endpoints.Version; @@ -25,6 +31,57 @@ public VersionEndpoint(IRequester requester, ModrinthClientConfig config) : base return await Requester.GetJsonAsync(reqMsg, cancellationToken).ConfigureAwait(false); } + /// + public async Task CreateAsync(string projectId, List files, string primaryFile, string name, string versionNumber, + string? changelog, List dependencies, List gameVersions, + ProjectVersionType versionType, List loaders, bool featured, + VersionStatus status, VersionStatus? requestedStatus, + CancellationToken cancellationToken = default) { + // todo this is really messy, imo should do builder or maybe take struct/class directly and have serializer function in it + var reqMsg = new HttpRequestMessage(); + reqMsg.Method = HttpMethod.Post; + reqMsg.RequestUri = new Uri(VersionsPath, UriKind.Relative); + + // transform for web + // might be better to make some kind of base class like ModrinthApiSerializer and ModrinthApiDeserializer + // and then extend those + var deps = dependencies.Select(d => new + { + version_id = d.VersionId, + project_id = d.ProjectId, + file_name = d.FileName, + dependency_type = d.DependencyType + }).ToList(); + + // data + string j = JsonSerializer.Serialize(new + { + name = name, + version_number = versionNumber, + changelog = changelog, + dependencies = deps, + game_versions = gameVersions, + version_type = versionType.ToModrinthString(), + loaders = loaders, + featured = featured, + status = status.ToModrinthString(), + requested_status = requestedStatus?.ToModrinthString(), + project_id = projectId, + file_parts = files.Select(f => f.FileName), + primary_file = primaryFile + }); + + MultipartFormDataContent c = new(); + c.Add(new StringContent(j, Encoding.UTF8, "application/json"), "data"); + files.ForEach(f => { + c.Add(new StreamContent(f.Stream), f.FileName, f.FileName); + }); + + reqMsg.Content = c; + + return await Requester.GetJsonAsync(reqMsg, cancellationToken).ConfigureAwait(false); + } + /// public async Task GetProjectVersionListAsync(string slugOrId, string[]? loaders = null, string[]? gameVersions = null, bool? featured = null, CancellationToken cancellationToken = default) diff --git a/src/Modrinth.Net/Extensions/ProjectVersionTypeExtensions.cs b/src/Modrinth.Net/Extensions/ProjectVersionTypeExtensions.cs new file mode 100644 index 0000000..80c9cfd --- /dev/null +++ b/src/Modrinth.Net/Extensions/ProjectVersionTypeExtensions.cs @@ -0,0 +1,26 @@ +using Modrinth.Models.Enums.Project; + +namespace Modrinth.Extensions; + +/// +/// Extensions for +/// +public static class ProjectVersionTypeExtensions +{ + /// + /// Converts ProjectVersionType to a string fit for the Modrinth API + /// + /// + /// + public static string ToModrinthString(this ProjectVersionType projectVersionType) + { + return projectVersionType switch + { + ProjectVersionType.Alpha => "alpha", + ProjectVersionType.Beta => "beta", + ProjectVersionType.Release => "release", + // Return lower string, this should work for all, but it is not guaranteed + _ => projectVersionType.ToString().ToLower() + }; + } +} \ No newline at end of file diff --git a/src/Modrinth.Net/Extensions/VersionStatusExtensions.cs b/src/Modrinth.Net/Extensions/VersionStatusExtensions.cs new file mode 100644 index 0000000..538b37c --- /dev/null +++ b/src/Modrinth.Net/Extensions/VersionStatusExtensions.cs @@ -0,0 +1,30 @@ +using Modrinth.Models.Enums.Project; +using Modrinth.Models.Enums.Version; + +namespace Modrinth.Extensions; + +/// +/// Extensions for +/// +public static class VersionStatusExtensions +{ + /// + /// Converts VersionStatus to a string fit for the Modrinth API + /// + /// + /// + public static string ToModrinthString(this VersionStatus versionStatus) + { + return versionStatus switch + { + VersionStatus.Archived => "archived", + VersionStatus.Draft => "draft", + VersionStatus.Listed => "listed", + VersionStatus.Scheduled => "scheduled", + VersionStatus.Unknown => "unknown", + VersionStatus.Unlisted => "unlisted", + // Return lower string, this should work for all, but it is not guaranteed + _ => versionStatus.ToString().ToLower() + }; + } +} \ No newline at end of file diff --git a/src/Modrinth.Net/Modrinth.Net.csproj b/src/Modrinth.Net/Modrinth.Net.csproj index 59d6df0..828b5d7 100644 --- a/src/Modrinth.Net/Modrinth.Net.csproj +++ b/src/Modrinth.Net/Modrinth.Net.csproj @@ -12,7 +12,7 @@ MIT true README.md - 3.6.0 + 3.6.1 $(PackageVersion) net8.0 Modrinth diff --git a/src/Modrinth.Net/UploadableFile.cs b/src/Modrinth.Net/UploadableFile.cs new file mode 100644 index 0000000..769e4d9 --- /dev/null +++ b/src/Modrinth.Net/UploadableFile.cs @@ -0,0 +1,42 @@ +namespace Modrinth; + +// where do I put this??? +/// +/// Wraps a stream with a filename, used for uploading files to Modrinth. +/// +public class UploadableFile +{ + /// + /// The name of our file + /// + public string FileName { get; } + /// + /// The stream to access our file data + /// + public Stream Stream { get; } + + /// + /// Creates a new Uploadable file by wrapping a stream with a filename + /// + /// The filename to use when uploading to Modrinth + /// The stream containing file data to upload + public UploadableFile(string fileName, Stream stream) + { + FileName = fileName; + Stream = stream; + } + + /// + /// Creates a new Uploadable file by opening a FileStream with the provided path + /// + /// The file path to open a stream with + public UploadableFile(string path) + { + FileName = Path.GetFileName(path); + + if (!File.Exists(path)) + throw new FileNotFoundException(); + + Stream = new FileStream(path, FileMode.Open); + } +} \ No newline at end of file From cc91674bd771a901e5232049fc9b8579feb8e119 Mon Sep 17 00:00:00 2001 From: Dexrn ZacAttack Date: Mon, 5 Jan 2026 02:28:46 -0800 Subject: [PATCH 2/2] refactor: use IEnumerable instead of List --- .../Endpoints/Version/IVersionEndpoint.cs | 6 +-- .../Endpoints/Version/VersionEndpoint.cs | 46 ++++++++++--------- 2 files changed, 27 insertions(+), 25 deletions(-) diff --git a/src/Modrinth.Net/Endpoints/Version/IVersionEndpoint.cs b/src/Modrinth.Net/Endpoints/Version/IVersionEndpoint.cs index b43f638..7b9145a 100644 --- a/src/Modrinth.Net/Endpoints/Version/IVersionEndpoint.cs +++ b/src/Modrinth.Net/Endpoints/Version/IVersionEndpoint.cs @@ -74,9 +74,9 @@ public interface IVersionEndpoint /// /// The newly created Version /// Thrown when the API returns an error or the request fails - Task CreateAsync(string projectId, List files, string primaryFile, string name, string versionNumber, - string? changelog, List dependencies, List gameVersions, - ProjectVersionType versionType, List loaders, bool featured, + Task CreateAsync(string projectId, IEnumerable files, string primaryFile, string name, string versionNumber, + string? changelog, IEnumerable dependencies, IEnumerable gameVersions, + ProjectVersionType versionType, IEnumerable loaders, bool featured, VersionStatus status, VersionStatus? requestedStatus, CancellationToken cancellationToken = default); diff --git a/src/Modrinth.Net/Endpoints/Version/VersionEndpoint.cs b/src/Modrinth.Net/Endpoints/Version/VersionEndpoint.cs index 4e49dc8..43fa98d 100644 --- a/src/Modrinth.Net/Endpoints/Version/VersionEndpoint.cs +++ b/src/Modrinth.Net/Endpoints/Version/VersionEndpoint.cs @@ -32,9 +32,9 @@ public VersionEndpoint(IRequester requester, ModrinthClientConfig config) : base } /// - public async Task CreateAsync(string projectId, List files, string primaryFile, string name, string versionNumber, - string? changelog, List dependencies, List gameVersions, - ProjectVersionType versionType, List loaders, bool featured, + public async Task CreateAsync(string projectId, IEnumerable files, string primaryFile, string name, string versionNumber, + string? changelog, IEnumerable dependencies, IEnumerable gameVersions, + ProjectVersionType versionType, IEnumerable loaders, bool featured, VersionStatus status, VersionStatus? requestedStatus, CancellationToken cancellationToken = default) { // todo this is really messy, imo should do builder or maybe take struct/class directly and have serializer function in it @@ -54,28 +54,30 @@ public VersionEndpoint(IRequester requester, ModrinthClientConfig config) : base }).ToList(); // data - string j = JsonSerializer.Serialize(new - { - name = name, - version_number = versionNumber, - changelog = changelog, - dependencies = deps, - game_versions = gameVersions, - version_type = versionType.ToModrinthString(), - loaders = loaders, - featured = featured, - status = status.ToModrinthString(), - requested_status = requestedStatus?.ToModrinthString(), - project_id = projectId, - file_parts = files.Select(f => f.FileName), - primary_file = primaryFile - }); + var uploadableFiles = files as UploadableFile[] ?? files.ToArray(); + string j = JsonSerializer.Serialize(new + { + name = name, + version_number = versionNumber, + changelog = changelog, + dependencies = deps, + game_versions = gameVersions, + version_type = versionType.ToModrinthString(), + loaders = loaders, + featured = featured, + status = status.ToModrinthString(), + requested_status = requestedStatus?.ToModrinthString(), + project_id = projectId, + file_parts = uploadableFiles.Select(f => f.FileName), + primary_file = primaryFile + }); MultipartFormDataContent c = new(); c.Add(new StringContent(j, Encoding.UTF8, "application/json"), "data"); - files.ForEach(f => { - c.Add(new StreamContent(f.Stream), f.FileName, f.FileName); - }); + foreach (UploadableFile file in uploadableFiles) + { + c.Add(new StreamContent(file.Stream), file.FileName, file.FileName); + } reqMsg.Content = c;