From 346b0783d8292494fd86148d8b70d5c7eecedd8f Mon Sep 17 00:00:00 2001 From: CPTProgrammer <46586216+CPTProgrammer@users.noreply.github.com> Date: Mon, 6 Jul 2026 04:56:50 +0800 Subject: [PATCH] Add documentation for Mod Releaser project --- docs/curseforge/get.md | 130 ++ docs/curseforge/upload.md | 200 ++ docs/frontend.md | 118 ++ docs/index.md | 92 + docs/modrinth/get-meta.md | 284 +++ docs/modrinth/get.md | 402 ++++ docs/modrinth/openapi.yml | 3984 +++++++++++++++++++++++++++++++++++++ docs/modrinth/upload.md | 407 ++++ docs/workflow.md | 132 ++ 9 files changed, 5749 insertions(+) create mode 100644 docs/curseforge/get.md create mode 100644 docs/curseforge/upload.md create mode 100644 docs/frontend.md create mode 100644 docs/index.md create mode 100644 docs/modrinth/get-meta.md create mode 100644 docs/modrinth/get.md create mode 100644 docs/modrinth/openapi.yml create mode 100644 docs/modrinth/upload.md create mode 100644 docs/workflow.md diff --git a/docs/curseforge/get.md b/docs/curseforge/get.md new file mode 100644 index 0000000..dfaf230 --- /dev/null +++ b/docs/curseforge/get.md @@ -0,0 +1,130 @@ +# CurseForge Get Mod Files API + +> 来源:https://docs.curseforge.com/rest-api/ +> +> API 基础地址:`https://api.curseforge.com`,认证方式:Header `x-api-key: ` + +--- + +## Get Mod Files + +``` +GET https://api.curseforge.com/v1/mods/{modId}/files +``` + +获取指定项目的所有文件。 + +### 参数 + +| 参数 | 位置 | 类型 | 必填 | 说明 | +|------|------|------|------|------| +| `modId` | path | int32 | 是 | 项目 ID | +| `gameVersion` | query | string | 否 | 按游戏版本过滤 | +| `modLoaderType` | query | int | 否 | 按 Mod Loader 类型过滤(见下表) | +| `gameVersionTypeId` | query | int32 | 否 | 按 `gameVersionTypeId` 过滤 | +| `index` | query | int32 | 否 | 分页起始索引(从 0 开始,限制 `index + pageSize <= 10000`) | +| `pageSize` | query | int32 | 否 | 每页数量,默认/最大 50 | + +**modLoaderType 枚举值:** + +| 值 | 含义 | +|----|------| +| 0 | Any | +| 1 | Forge | +| 2 | Cauldron | +| 3 | LiteLoader | +| 4 | Fabric | +| 5 | Quilt | +| 6 | NeoForge | + +### 成功响应 + +```jsonc +{ + "data": [ + { + "id": 12345, // 文件 ID + "gameId": 432, // 游戏 ID + "modId": 789, // 项目 ID + "isAvailable": true, // 是否可下载 + "displayName": "My Mod v1.0", // 显示名称 + "fileName": "my-mod-1.0.jar", // 文件名 + "releaseType": 1, // 1=Release, 2=Beta, 3=Alpha + "fileStatus": 10, // 文件状态(见附表) + "hashes": [ + { "value": "abc123...", "algo": 1 } // algo: 1=Sha1, 2=Md5 + ], + "fileDate": "2025-01-01T00:00:00Z", // 文件时间戳 + "fileLength": 1048576, // 文件大小(字节) + "downloadCount": 5000, // 下载次数 + "fileSizeOnDisk": 1048576, // 磁盘占用 + "downloadUrl": "https://...", // 下载地址 + "gameVersions": ["1.21.5", "1.21.4"], // 关联的游戏版本(字符串列表) + "sortableGameVersions": [ // 排序用版本信息 + { + "gameVersionName": "1.21.5", + "gameVersionPadded": "0000000001.0000000021.0000000005", + "gameVersion": "1.21.5", + "gameVersionReleaseDate": "2025-04-01T00:00:00Z", + "gameVersionTypeId": 83806 + } + ], + "dependencies": [ // 依赖关系 + { + "modId": 123, // 依赖的项目 ID + "relationType": 3 // 关系类型(见附表) + } + ], + "exposeAsAlternative": false, + "parentProjectFileId": null, + "alternateFileId": null, + "isServerPack": false, + "serverPackFileId": null, + "isEarlyAccessContent": false, + "earlyAccessEndDate": null, + "fileFingerprint": 1234567890, + "modules": [ + { "name": "main", "fingerprint": 1234567890 } + ] + } + ], + "pagination": { + "index": 0, // 本次返回的起始索引 + "pageSize": 50, // 请求的页大小 + "resultCount": 1, // 本次实际返回数量 + "totalCount": 10 // 总文件数 + } +} +``` + +--- + +## 附表 + +### releaseType + +| 值 | 含义 | +|----|------| +| 1 | Release | +| 2 | Beta | +| 3 | Alpha | + +### relationType + +| 值 | 含义 | +|----|------| +| 1 | EmbeddedLibrary | +| 2 | OptionalDependency | +| 3 | RequiredDependency | +| 4 | Tool | +| 5 | Incompatible | +| 6 | Include | + +### fileStatus(常用值) + +| 值 | 含义 | +|----|------| +| 1 | Processing | +| 4 | Approved | +| 10 | Released | +| 12 | Deprecated | diff --git a/docs/curseforge/upload.md b/docs/curseforge/upload.md new file mode 100644 index 0000000..66183b9 --- /dev/null +++ b/docs/curseforge/upload.md @@ -0,0 +1,200 @@ +# CurseForge Upload API + +> 基于 CurseForge 官方 API 文档及实测整理。 +> +> API 基础地址:`https://minecraft.curseforge.com`,**必须带浏览器 `User-Agent` 请求头**,否则 Cloudflare 会拦截返回 403。 +> +> 推荐 User-Agent:`Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36` + +--- + +## 生成 Token + +在 [API Tokens](https://authors.curseforge.com/account/api-tokens) 页面生成 API Token。 + +## 认证 + +通过以下两种方式之一传递 Token: + +- HTTP Header:`X-Api-Token: ` +- Query String:`?token=` + +--- + +## Game Version Types API + +``` +GET https://minecraft.curseforge.com/api/game/version-types +``` + +> **注意:** 此端点必须带浏览器 `User-Agent` 请求头,否则会被 Cloudflare 拦截返回 403。 +> +> 推荐值:`Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36` + +返回版本类型列表,直接返回 JSON 数组,每个对象包含: + +| 字段 | 类型 | 说明 | +|------|------|------| +| `id` | int | 版本类型 ID(即 `gameVersionTypeID`) | +| `name` | string | 类型名称 | +| `slug` | string | 类型标识 | + +其中 `id: 68441, name: "Modloader"` 对应 Modloader 分类。 + +`id: 75208, name: "Environment"` 对应运行环境,包含 `name: "Client"` 和 `name: "Server"` 两个 Game Version。 + +## Game Versions API + +``` +GET https://minecraft.curseforge.com/api/game/versions +``` + +> **注意:** 此端点必须带浏览器 `User-Agent` 请求头,否则会被 Cloudflare 拦截返回 403。 +> +> 推荐值:`Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36` + +返回 Minecraft 版本列表,直接返回 JSON 数组(无 `{"data": ...}` 包装),每个对象包含: + +| 字段 | 类型 | 说明 | +|------|------|------| +| `id` | int | 版本 ID(用于上传时的 `gameVersions` 字段) | +| `gameVersionTypeID` | int | 版本类型 ID | +| `name` | string | 版本名称(如 `1.21.5`) | +| `slug` | string | 版本标识(如 `1-21-5`) | +| `apiVersion` | int \| null | API 版本号 | + +> **注意:** Game Versions 列表并非仅包含游戏版本号,而是将 Mod Loader(Forge、Fabric 等)、Environment(Client、Server)甚至 Java 版本都混在一起。具体某个版本属于哪个类型,看它的 `gameVersionTypeID`,对应 [Game Version Types API](#game-version-types-api) 中的 `id`。 + +--- + +## Project Upload File API + +``` +POST https://minecraft.curseforge.com/api/projects/{projectId}/upload-file +Content-Type: multipart/form-data +``` + +上传文件到项目。`projectId` 可在项目概览页面的 URL 中找到。 + +### 请求字段 + +| 字段 | 类型 | 说明 | +|------|------|------| +| `metadata` | string (JSON) | 文件元数据(见下方) | +| `file` | file | 要上传的实际文件 | + +### metadata JSON 结构 + +```jsonc +{ + "changelog": "更新日志内容", // 必填。支持 HTML / Markdown(需设置 changelogType) + "changelogType": "markdown", // 可选,默认 "text"。可选值: "text", "html", "markdown" + "displayName": "Foo", // 可选,站点上显示的文件友好名称 + "parentFileID": 42, // 可选,父文件 ID + "gameVersions": [157, 158], // 可选,支持的 Game Version ID 列表(通过 Game Versions API 获取)。若提供 parentFileID 则不支持此字段 + "releaseType": "release", // 必填。可选值: "alpha", "beta", "release" + "isMarkedForManualRelease": false, // 可选,若为 true,审核通过后不会立即发布,可手动选择发布时间 + "relations": { + "projects": [{ + "slug": "mantle", // 关联项目的 slug + "projectID": "74924", // 可选,用于精确匹配项目 + "type": "requiredDependency" // 关联类型,可选值见下方 + }] + } +} +``` + +**relations.type 可选值:** + +- `embeddedLibrary` — 内嵌库 +- `incompatible` — 不兼容 +- `optionalDependency` — 可选依赖 +- `requiredDependency` — 必需依赖 +- `tool` — 工具 + +### 成功响应 + +```jsonc +{ + "id": 20402 // 新建文件的 ID +} +``` + +--- + +## Project File Management API + +``` +POST https://minecraft.curseforge.com/api/projects/{projectId}/update-file +Content-Type: multipart/form-data +``` + +更新已上传的文件信息。 + +### 请求字段 + +| 字段 | 类型 | 说明 | +|------|------|------| +| `metadata` | string (JSON) | 文件更新元数据(见下方) | + +### metadata JSON 结构 + +```jsonc +{ + "fileID": 20402, // 必填,要更新的文件 ID + + // 以下字段均为可选,可任意组合,但至少要包含一个 + "changelog": "更新日志内容", + "changelogType": "markdown", // 可选值: "text", "html", "markdown",默认 "text" + "displayName": "Foo", + "gameVersions": [157, 158], + "releaseType": "release", // 可选值: "alpha", "beta", "release" + "relations": { + "projects": [{ + "slug": "mantle", + "projectID": "74924", + "type": "requiredDependency" + }] + } +} +``` + +> **注意:** 若未填写任何可选字段,API 将返回失败。 + +### 成功响应 + +```jsonc +{ + "id": 20402 // 被更新文件的 ID +} +``` + +--- + +## Maven + +CurseForge 提供 Maven 端点,可在构建脚本中引用依赖。 + +``` +https://www.curseforge.com/api/maven/{projectSlug}/{mavenArtifact}/{mavenVersion}/{projectFileNameArtifact}-{projectFileNameVersion}-{projectFileNameTag}.jar +``` + +**URL 参数说明:** + +| 参数 | 说明 | +|------|------| +| `{projectSlug}` | 项目的 slug | +| `{mavenArtifact}` | 文件名 artifact | +| `{mavenVersion}` | 版本号,或使用 `release` 获取最新版 | +| `{projectFileNameArtifact}` | 同 `mavenArtifact` | +| `{projectFileNameVersion}` | 同 `mavenVersion` | +| `{projectFileNameTag}` | 文件名标签(如 `universal`、`dev`) | + +> **注意:** Maven 认证不支持 Header 方式,需将 API Key 直接编入 Maven URL。 + +--- + +## 参考链接 + +- [CurseForge API Tokens](https://authors.curseforge.com/account/api-tokens) +- [官方文档原文](https://support.curseforge.com/support/solutions/articles/9000197321-curseforge-upload-api) diff --git a/docs/frontend.md b/docs/frontend.md new file mode 100644 index 0000000..12653d9 --- /dev/null +++ b/docs/frontend.md @@ -0,0 +1,118 @@ +# 前端页面 + +## 布局 + +单页面应用,无需路由。自上而下: + +``` +┌─────────────────────────────────────────┐ +│ ConfigSelector │ +│ [选择配置 ▼] [+ 添加配置] │ +├─────────────────────────────────────────┤ +│ VersionTable │ +│ ┌──────────┬────────────────┬────────┐ │ +│ │ MC 版本 │ 构建产物 │ 源码 │ │ +│ ├──────────┼────────────────┼────────┤ │ +│ │ 1.21 │ xxx-1.0.0-... │ xxx.. │ │ +│ │ 1.21.1 │ xxx-1.0.0-... │ xxx.. │ │ +│ └──────────┴────────────────┴────────┘ │ +├─────────────────────────────────────────┤ +│ [一键发布] │ +└─────────────────────────────────────────┘ +``` + +## 组件 + +### ConfigSelector + +- 下拉框列出 `configs/` 目录下所有配置(按 `name` 显示) +- 选中后触发后端解析,刷新 VersionTable +- 右侧 `[修改配置]` `[+ 添加配置]` 两个按钮,图标使用 Ant Design 图标库 + - 修改配置:弹出 ConfigModal,预填当前配置数据,保存时覆盖原文件 + - 添加配置:弹出 ConfigModal,空白表单,保存时创建新文件 +- ConfigModal 添加 / 修改共用同一个组件,通过是否传入已有配置数据区分模式 + +### VersionTable + +- 每行:MC 版本 | 构建产物文件名 | 源码文件名 +- MC 版本来自 `minecraft_properties_dir` 目录文件名 +- 构建产物和源码分别匹配 `filename_format` 和 `source_filename_format` +- 全部只读展示 +- 切换 config 后后台解析期间,Table 使用 Ant Design 自带的加载状态 + +### PublishModal + +点击 `[一键发布]` 后弹出,分两页。 + +#### 第一页:版本选择 + +1. 弹出后加载 Modrinth 和 CurseForge 已有版本数据,Table 使用 Ant Design 自带的加载状态 +2. 加载完成后,左右两栏布局: + - 左栏:Modrinth 版本列表 + - 右栏:CurseForge 版本列表 +3. 每栏顶部为**待发布版本**(绿色高亮),按顺序排列;下方为已有版本(另一颜色) +4. 每个版本行最左侧有复选框: + - 待发布版本:默认勾选,可选择取消 + - 已有版本:复选框禁用 + - 若某待发布版本在平台上已存在(版本号完全一致),显示为已有版本颜色,复选框禁用 +5. 点击"下一步"进入第二页 + +#### 第二页:发布设置 + +- **Changelog**:Markdown 编辑器,带 Code / Preview 切换(类似 GitHub 评论框) +- **version_type**:下拉选择 `release`(默认) / `beta` / `alpha` +- 平台不再勾选(第一页已通过版本复选框隐式确定:某平台无任何勾选的待发布版本即不发布该平台) +- 点击"确认发布"开始执行 +- 提供"上一步"按钮返回第一页修改选择 + +#### 发布执行与进度 + +- 一个 mod version 对应多个 MC version 变体,按顺序逐个提交,**不可并行** +- 任一请求失败则取消后续所有任务 +- 点击确认后显示两个进度条(仅显示有发布任务的平台): + - `平台名: 已发布数 / 总发布数` +- 全部完成后或失败后,弹出结果 Modal 显示最终状态 + +### ConfigModal + +添加 / 修改共用一个 Modal 组件。添加模式为空表单;修改模式预填当前配置数据,保存时覆盖原文件。 + +弹出后需请求 Modrinth 和 CurseForge API 获取可选值,Table / Select 使用 Ant Design 自带的加载状态。加载完成后显示表单: + +**通用字段** + +| 字段 | 控件 | 说明 | +|------|------|------| +| `name` | Input | 配置文件显示名称 | +| `project_dir` | Input + 目录选择 | 项目目录绝对路径 | +| `minecraft_properties_dir` | Input | 默认 `./properties` | +| `project_properties_path` | Input | 默认 `./gradle.properties` | +| `mod_version_field` | Input | 默认 `mod_version` | +| `filename_format` | Input | 含 `${version}` `${mc_version}` | +| `source_filename_format` | Input | 含 `${version}` `${mc_version}` | + +**Modrinth 字段** + +| 字段 | 控件 | 说明 | +|------|------|------| +| `project_id` | Input | 项目 ID(8 位 base62)。输入后使用 Ant Design 防抖 hook 自动查询项目名称并显示在输入框下方以供确认 | +| `version_name` | Input | 模板字符串,含占位符。默认 `"ModName v${version} for Minecraft ${mc_version_range}"`。使用防抖自动显示匹配到的版本数量 | +| `version` | Input | 模板字符串,含占位符 | +| `loaders` | Select(多选) | 选项从 `GET /tag/loader` 获取 | +| `environment` | Select | 运行环境,必填。选项使用 Ant Design OptGroup 分组,具体分组参考 `docs/modrinth/upload.md` 中的 `environment` 枚举值章节 | +| `dependencies` | 动态列表 | 每项含 `dependency_type`(Select:required/optional/incompatible/embedded)和 `project_id`(Input) | + +**CurseForge 字段** + +| 字段 | 控件 | 说明 | +|------|------|------| +| `project_id` | InputNumber | 项目 ID(数字)。输入后使用 Ant Design 防抖 hook 自动查询项目名称并显示在输入框下方以供确认 | +| `version_name` | Input | 模板字符串,含占位符。默认 `"${filename_format}"`。使用防抖自动显示匹配到的版本数量 | +| `environment` | Select(多选) | 选项从 Game Version Types API(Environment 类型)获取 | +| `loaders` | Select(多选) | 选项从 Game Version Types API(Modloader 类型)获取 | +| `relations` | 动态列表 | 每项含 `slug`(Input)和 `type`(Select:embeddedLibrary/incompatible/optionalDependency/requiredDependency/tool) | + +> **防抖策略**:`project_id` 和 `version_name` 的防抖延迟需设置较长(建议 800ms~1s),避免触发两个平台 API 的速率限制。 + +底部:保存按钮,写入 `configs/{name}.json`。 + diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..ec3f4b8 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,92 @@ +# Mod Releaser + +一个基于 Web 界面的本地工具,用于通过 API 向 Modrinth 和 CurseForge 一键发布 mod 新版本。 + +## 技术栈 + +- **框架**:Next.js(前后端不分离,WebUI 作为本地界面) +- **UI 库**:Ant Design +- **平台 API**:Modrinth API、CurseForge API + +## 项目结构 + +``` +/ +├── secrets.json # API 密钥(curseforge / modrinth) +├── configs/ +│ └── {config_name}.json # 各 mod 的配置 +├── public/ # 静态资源(图标、图片等) +├── src/ +│ ├── app/ # Next.js App Router(页面、布局) +│ ├── components/ # 可复用 UI 组件 +│ ├── lib/ # 后端逻辑(与前端直接调用,不创建 API Route) +│ └── types/ # TypeScript 类型定义 +├── docs/ # 本文档 +├── package.json +└── ... +``` + +## 开发规范 + +### 前后端通信 + +使用 Next.js Server Actions 或直接在 Server Component 中调用后端函数,**不创建 API Route**。前端通过 `"use server"` 指令的函数或 Server Component 的 `async` 逻辑直接调用 `src/lib/` 中的后端代码,避免前后端分离的冗余 API 层。 + +### `src/` 目录说明 + +| 目录 | 职责 | +|------|------| +| `app/` | Next.js App Router 页面与布局 | +| `components/` | 可复用的 Ant Design 组件 | +| `lib/` | 纯后端逻辑(读取配置、调用 API、模板解析等),可供前端直接引用 | +| `types/` | TypeScript 类型/接口定义 | + +### 代码质量 + +- 每个文件行数不宜过多,单一职责,低耦合高内聚 +- 尽可能减少技术债,写出可维护性高的代码 +- 组件拆分粒度合理,逻辑与 UI 分离 + +## 文档目录 + +- [frontend.md](./frontend.md) — 前端页面与组件(WebUI) +- [modrinth/](./modrinth/) — Modrinth API 参考 +- [curseforge/](./curseforge/) — CurseForge API 参考 +- [workflow.md](./workflow.md) — 整体发布流程、状态管理、错误处理 + +## 参考文档 + +编写代码时,如需查阅框架或组件库文档,通过 fetch 工具按以下体系获取: + +### Modrinth API + +参考 `docs/modrinth/` 目录(忽略 `openapi.yml`): + +| 文件 | 说明 | +|------|------| +| `upload.md` | 新建版本 API(`POST /version`) | +| `get.md` | 获取版本信息 API(列出项目版本、获取单个版本等) | +| `get-meta.md` | 元数据查询 API(加载器列表、游戏版本列表等) | + +### CurseForge API + +参考 `docs/curseforge/` 目录: + +| 文件 | 说明 | +|------|------| +| `upload.md` | 上传文件 API(含 Game Version Types / Game Versions 查询) | +| `get.md` | 获取文件列表 API(`GET /mods/{modId}/files`) | + +### 前端框架与组件库 + +#### Next.js + +- `nextjs.org/docs/llms.txt` — 目录索引(带描述) +- 每页 `.md` — 逐页获取正文 + +#### Ant Design + +- `ant.design/llms.txt` — 目录索引(纯链接) +- `ant.design/design.md` — 设计语言上下文 +- 每页 `.md` — 逐页获取正文 +- 每组件 `semantic.md` — 单个组件语义结构 diff --git a/docs/modrinth/get-meta.md b/docs/modrinth/get-meta.md new file mode 100644 index 0000000..03f5cd7 --- /dev/null +++ b/docs/modrinth/get-meta.md @@ -0,0 +1,284 @@ +# Modrinth 元数据查询 API 使用文档 + +本文档描述如何获取 Modrinth 平台支持的**所有加载器(loaders)** 和 **Minecraft 游戏版本(game versions)**,以及项目类型、分类等元数据。这些接口均为**公开接口,无需认证**。 + +--- + +## 基本信息 + +| 项目 | 内容 | +|------|------| +| **生产环境** | `https://api.modrinth.com/v2` | +| **认证** | 无需认证(全部公开) | +| **Rate Limit** | 每 IP 每分钟 300 请求 | + +--- + +## 接口一:获取所有加载器 + +获取 Modrinth 支持的全部模组加载器列表。 + +| 项目 | 内容 | +|------|------| +| **端点** | `GET /tag/loader` | +| **认证** | 否 | +| **成功响应码** | `200` | +| **响应体** | `LoaderTag[]` — 加载器对象数组 | + +### 响应字段(LoaderTag) + +| 字段 | 类型 | 说明 | 示例 | +|------|------|------|------| +| `name` | `string` | 加载器名称 | `"fabric"` | +| `icon` | `string` | 加载器的 SVG 图标 | `"..."` | +| `supported_project_types` | `string[]` | 该加载器适用的项目类型 | `["mod", "modpack"]` | + +### 请求示例 + +```bash +curl "https://api.modrinth.com/v2/tag/loader" +``` + +```js +const response = await fetch("https://api.modrinth.com/v2/tag/loader"); +const loaders = await response.json(); + +for (const loader of loaders) { + console.log(`${loader.name} — 适用于: ${loader.supported_project_types.join(", ")}`); +} +``` + +### 响应示例 + +```json +[ + { + "icon": "...", + "name": "fabric", + "supported_project_types": ["mod", "modpack"] + }, + { + "icon": "...", + "name": "forge", + "supported_project_types": ["mod", "modpack"] + }, + { + "icon": "...", + "name": "quilt", + "supported_project_types": ["mod", "modpack"] + }, + { + "icon": "...", + "name": "neoforge", + "supported_project_types": ["mod", "modpack"] + }, + { + "icon": "...", + "name": "minecraft", + "supported_project_types": ["resourcepack", "shader"] + } +] +``` + +> **注意:** `"minecraft"` 加载器用于资源包(resourcepacks)、光影等不需要模组加载器的项目类型。 + +--- + +## 接口二:获取所有游戏版本 + +获取 Modrinth 支持的全部 Minecraft 版本列表,包含版本类型和发布日期。 + +| 项目 | 内容 | +|------|------| +| **端点** | `GET /tag/game_version` | +| **认证** | 否 | +| **成功响应码** | `200` | +| **响应体** | `GameVersionTag[]` — 游戏版本对象数组 | + +### 响应字段(GameVersionTag) + +| 字段 | 类型 | 说明 | 示例 | +|------|------|------|------| +| `version` | `string` | 游戏版本号 | `"1.18.1"` | +| `version_type` | `string` | 版本类型:`release`、`snapshot`、`alpha`、`beta` | `"release"` | +| `date` | `string` | 版本发布日期(ISO-8601) | `"2021-12-10T00:00:00Z"` | +| `major` | `boolean` | 是否为主要版本,用于 Featured Versions 标记 | `true` | + +### 请求示例 + +```bash +curl "https://api.modrinth.com/v2/tag/game_version" +``` + +```js +const response = await fetch("https://api.modrinth.com/v2/tag/game_version"); +const versions = await response.json(); + +// 仅列出正式发布版 +for (const v of versions.filter(v => v.version_type === "release")) { + console.log(`${v.version} (${v.version_type})${v.major ? " [主要版本]" : ""}`); +} +``` + +### 响应示例 + +```json +[ + { + "version": "1.21.4", + "version_type": "release", + "date": "2025-04-04T00:00:00Z", + "major": true + }, + { + "version": "1.21.3", + "version_type": "release", + "date": "2024-11-02T00:00:00Z", + "major": false + }, + { + "version": "1.20.1", + "version_type": "release", + "date": "2023-06-12T00:00:00Z", + "major": true + }, + { + "version": "24w45a", + "version_type": "snapshot", + "date": "2024-11-06T00:00:00Z", + "major": false + } +] +``` + +--- + +## 辅助接口:项目类型 + +获取 Modrinth 支持的项目类型,用于了解加载器适用哪些类型的项目。 + +| 项目 | 内容 | +|------|------| +| **端点** | `GET /tag/project_type` | +| **响应体** | `string[]` | + +```bash +curl "https://api.modrinth.com/v2/tag/project_type" +``` + +```json +["mod", "modpack", "resourcepack", "shader"] +``` + +--- + +## 辅助接口:分类标签 + +获取 Modrinth 的项目分类列表(含图标和适用项目类型)。 + +| 项目 | 内容 | +|------|------| +| **端点** | `GET /tag/category` | +| **响应体** | `CategoryTag[]` | + +```bash +curl "https://api.modrinth.com/v2/tag/category" +``` + +响应字段: + +| 字段 | 类型 | 说明 | +|------|------|------| +| `icon` | `string` | 分类的 SVG 图标 | +| `name` | `string` | 分类名称 | +| `project_type` | `string` | 该分类适用的项目类型 | +| `header` | `string` | 该分类所属的标题分组 | + +--- + +## 常见使用场景 + +### 场景 1:构建上传表单的下拉选项 + +在发布模组的 UI 中,动态获取可选加载器和游戏版本: + +```js +const [loaders, gameVersions] = await Promise.all([ + fetch("https://api.modrinth.com/v2/tag/loader").then(r => r.json()), + fetch("https://api.modrinth.com/v2/tag/game_version").then(r => r.json()) +]); + +// 加载器下拉 +const loaderNames = loaders.map(l => l.name); +// → ["fabric", "forge", "quilt", "neoforge", "minecraft", ...] + +// 仅正式版游戏版本 +const releaseVersions = gameVersions + .filter(v => v.version_type === "release") + .map(v => v.version); +// → ["1.21.4", "1.21.3", "1.20.4", ...] +``` + +### 场景 2:验证用户输入是否合法 + +在上传版本前,校验 `loaders` 和 `game_versions` 中填的值是否被 Modrinth 接受: + +```js +const validLoaders = new Set(loaders.map(l => l.name)); +const validVersions = new Set(gameVersions.map(v => v.version)); + +const invalidLoaders = userInputLoaders.filter(l => !validLoaders.has(l)); +const invalidVersions = userInputVersions.filter(v => !validVersions.has(v)); + +if (invalidLoaders.length > 0 || invalidVersions.length > 0) { + console.error("不合法的值:", { invalidLoaders, invalidVersions }); +} +``` + +### 场景 3:获取某个加载器支持的 project_type + +用于判断某个加载器是否适用于 mod、modpack 等类型: + +```js +const loaders = await fetch("https://api.modrinth.com/v2/tag/loader").then(r => r.json()); + +const fabricInfo = loaders.find(l => l.name === "fabric"); +console.log(fabricInfo.supported_project_types); // → ["mod", "modpack"] + +// 判断 fabric 是否支持 mod 类型 +const supportsMod = fabricInfo.supported_project_types.includes("mod"); // → true +``` + +--- + +## 缓存建议 + +这些元数据接口的返回值在短期内不会频繁变化,强烈建议在客户端做缓存: + +- **加载器列表**:以天为单位缓存(加载器新增频率极低)。 +- **游戏版本列表**:以小时或天为单位缓存(仅在 Minecraft 新版本发布时需要刷新)。 + +```js +// 简单的内存缓存示例 +let cachedLoaders = null; +let cacheTime = 0; +const CACHE_TTL = 86400000; // 24 小时 + +async function getLoaders() { + if (cachedLoaders && Date.now() - cacheTime < CACHE_TTL) { + return cachedLoaders; + } + cachedLoaders = await fetch("https://api.modrinth.com/v2/tag/loader").then(r => r.json()); + cacheTime = Date.now(); + return cachedLoaders; +} +``` + +--- + +## 参考 + +- [Modrinth API 文档](https://docs.modrinth.com) +- [OpenAPI 规范文件](openapi.yml) +- [获取版本信息 API](get.md) +- [新建版本 API](upload.md) diff --git a/docs/modrinth/get.md b/docs/modrinth/get.md new file mode 100644 index 0000000..8cf2b86 --- /dev/null +++ b/docs/modrinth/get.md @@ -0,0 +1,402 @@ +# Modrinth 获取版本信息 API 使用文档 + +本文档描述如何通过 Modrinth API 获取项目的版本列表以及每个版本的详细信息,包括版本名(`name`)、版本号(`version_number`)、文件名(`filename`)、下载地址等。 + +--- + +## 基本信息 + +所有版本读取接口均为 **公开接口,无需认证**。但如果项目包含 `draft`(草稿)状态的版本,获取它们可能需要认证。 + +| 项目 | 内容 | +|------|------| +| **生产环境** | `https://api.modrinth.com/v2` | +| **测试环境** | `https://staging-api.modrinth.com/v2` | +| **认证** | 公开接口无需认证;获取草稿版本需要 PAT | +| **Rate Limit** | 每 IP 每分钟 300 请求 | + +--- + +## 接口一:列出项目的所有版本 + +获取指定项目下的全部版本列表。这是获取版本信息最常用的入口。 + +| 项目 | 内容 | +|------|------| +| **端点** | `GET /project/{id\|slug}/version` | +| **认证** | 否(公开) | +| **成功响应码** | `200` | +| **响应体** | `Version[]` — Version 对象数组 | + +### 路径参数 + +| 参数 | 说明 | 示例 | +|------|------|------| +| `id\|slug` | 项目的 8 位 base62 ID 或 slug(别名) | `AABBCCDD` 或 `my-mod` | + +### 查询参数(可选,用于过滤) + +| 参数 | 类型 | 说明 | 示例 | +|------|------|------|------| +| `loaders` | `string` | 按加载器过滤,JSON 数组字符串 | `'["fabric"]'` | +| `game_versions` | `string` | 按 Minecraft 版本过滤,JSON 数组字符串 | `'["1.20.4"]'` | +| `featured` | `boolean` | 按是否精选过滤 | `true` | +| `include_changelog` | `boolean` | 是否在响应中包含 `changelog` 字段。**强烈建议设为 `false`** 以减小响应体,除非确实需要更新日志内容 | `false` | + +### 请求示例 + +**获取某项目的全部版本(不含 changelog):** + +```bash +curl "https://api.modrinth.com/v2/project/AABBCCDD/version?include_changelog=false" +``` + +**按加载器和游戏版本过滤:** + +```bash +curl "https://api.modrinth.com/v2/project/AABBCCDD/version?loaders=%5B%22fabric%22%5D&game_versions=%5B%221.20.4%22%5D" +``` + +> 注意:`loaders` 和 `game_versions` 的值需要是 JSON 数组字符串(如 `["fabric"]`),在 URL 中需要做 URL 编码。`[...]` 编码后为 `%5B...%5D`。 + +**Node.js (fetch) 示例:** + +```js +const projectId = "AABBCCDD"; + +const url = new URL(`https://api.modrinth.com/v2/project/${projectId}/version`); +url.searchParams.set("include_changelog", "false"); + +const response = await fetch(url); +const versions = await response.json(); + +for (const v of versions) { + console.log(`版本名: ${v.name}`); + console.log(`版本号: ${v.version_number}`); + for (const f of v.files) { + console.log(` 文件: ${f.filename} (${f.size} bytes) ${f.primary ? "[主文件]" : ""}`); + } +} +``` + +--- + +## 接口二:获取单个版本 + +根据版本 ID 获取某个版本的完整信息。 + +| 项目 | 内容 | +|------|------| +| **端点** | `GET /version/{id}` | +| **认证** | 否(公开) | +| **成功响应码** | `200` | +| **响应体** | `Version` 对象 | + +### 路径参数 + +| 参数 | 说明 | 示例 | +|------|------|------| +| `id` | 版本 ID(8 位 base62 字符串) | `IIJJKKLL` | + +### 请求示例 + +```bash +curl "https://api.modrinth.com/v2/version/IIJJKKLL" +``` + +```js +const versionId = "IIJJKKLL"; +const response = await fetch(`https://api.modrinth.com/v2/version/${versionId}`); +const version = await response.json(); +console.log(version.name, version.version_number); +``` + +--- + +## 接口三:通过项目 + 版本号获取版本 + +如果你知道项目 ID 和版本号(而非版本 ID),可以用此接口直接获取。 + +| 项目 | 内容 | +|------|------| +| **端点** | `GET /project/{id\|slug}/version/{id\|number}` | +| **认证** | 否(公开) | +| **成功响应码** | `200` | +| **响应体** | `Version` 对象 | + +### 路径参数 + +| 参数 | 说明 | 示例 | +|------|------|------| +| `id\|slug` | 项目 ID 或 slug | `AABBCCDD` | +| `id\|number` | 版本 ID 或版本号字符串 | `1.0.0` 或 `IIJJKKLL` | + +> **注意:** 如果版本号匹配到多个版本,只会返回 **最旧的** 那个。建议使用版本 ID 获得精确结果。 + +### 请求示例 + +```bash +curl "https://api.modrinth.com/v2/project/AABBCCDD/version/1.0.0" +``` + +--- + +## 接口四:批量获取多个版本 + +一次请求获取多个指定版本的信息。 + +| 项目 | 内容 | +|------|------| +| **端点** | `GET /versions` | +| **认证** | 否(公开) | +| **成功响应码** | `200` | +| **响应体** | `Version[]` — Version 对象数组 | + +### 查询参数 + +| 参数 | 类型 | 必填 | 说明 | 示例 | +|------|------|------|------|------| +| `ids` | `string` | ✅ | JSON 数组字符串,包含版本 ID 列表 | `'["IIJJKKLL", "MMNNOOPP"]'` | + +### 请求示例 + +```bash +curl "https://api.modrinth.com/v2/versions?ids=%5B%22IIJJKKLL%22%2C%22MMNNOOPP%22%5D" +``` + +```js +const versionIds = ["IIJJKKLL", "MMNNOOPP"]; +const url = new URL("https://api.modrinth.com/v2/versions"); +url.searchParams.set("ids", JSON.stringify(versionIds)); +const response = await fetch(url); +const versions = await response.json(); +``` + +--- + +## 响应格式 + +### Version 对象结构 + +```json +{ + "id": "IIJJKKLL", + "project_id": "AABBCCDD", + "author_id": "EEFFGGHH", + "name": "Version 1.0.0", + "version_number": "1.0.0", + "changelog": "## 更新内容\n\n- 修复了若干 Bug", + "date_published": "2025-01-01T00:00:00Z", + "downloads": 1024, + "version_type": "release", + "status": "listed", + "requested_status": null, + "game_versions": ["1.20.1", "1.20.4"], + "loaders": ["fabric"], + "featured": false, + "dependencies": [ + { + "dependency_type": "required", + "project_id": "P7dR8mSH", + "version_id": null, + "file_name": null + } + ], + "files": [ + { + "hashes": { + "sha512": "93ecf5fe...", + "sha1": "c84dd4b3..." + }, + "url": "https://cdn.modrinth.com/data/AABBCCDD/versions/1.0.0/my-mod-1.0.0.jar", + "filename": "my-mod-1.0.0.jar", + "primary": true, + "size": 1097270, + "file_type": null + }, + { + "hashes": { + "sha512": "ab12cd34...", + "sha1": "ef56gh78..." + }, + "url": "https://cdn.modrinth.com/data/AABBCCDD/versions/1.0.0/my-mod-1.0.0-sources.jar", + "filename": "my-mod-1.0.0-sources.jar", + "primary": false, + "size": 543210, + "file_type": "sources-jar" + } + ] +} +``` + +### 关键字段说明 + +#### Version 顶层字段 + +| 字段 | 类型 | 说明 | +|------|------|------| +| `id` | `string` | 版本唯一 ID(8 位 base62) | +| `project_id` | `string` | 所属项目 ID | +| `author_id` | `string` | 发布者用户 ID | +| `name` | `string` | **版本名称**(如 `"Version 1.0.0"`) | +| `version_number` | `string` | **版本号**(如 `"1.0.0"`,建议遵循语义化版本) | +| `changelog` | `string \| null` | 更新日志文本(若使用 `include_changelog=false` 则为 `null`) | +| `date_published` | `string` | 发布时间(ISO-8601 格式) | +| `downloads` | `integer` | 下载次数 | +| `version_type` | `string` | 发布渠道:`release`、`beta`、`alpha` | +| `status` | `string` | 版本状态:`listed`、`archived`、`draft`、`unlisted`、`scheduled` | +| `game_versions` | `string[]` | 支持的 Minecraft 版本列表 | +| `loaders` | `string[]` | 支持的加载器列表 | +| `featured` | `boolean` | 是否为精选版本 | +| `dependencies` | `object[]` | 依赖列表(结构见下方) | +| `files` | `object[]` | 该版本的所有文件列表 | + +#### dependencies 数组项 + +| 字段 | 类型 | 说明 | +|------|------|------| +| `dependency_type` | `string` | `required`、`optional`、`incompatible`、`embedded` | +| `project_id` | `string \| null` | 依赖的项目 ID | +| `version_id` | `string \| null` | 依赖的具体版本 ID | +| `file_name` | `string \| null` | 依赖文件名 | + +#### files 数组项(最常需要关注的字段) + +| 字段 | 类型 | 说明 | +|------|------|------| +| `filename` | `string` | **文件名**(如 `"my-mod-1.0.0.jar"`) | +| `url` | `string` | 文件直链下载地址 | +| `size` | `integer` | 文件大小(字节) | +| `primary` | `boolean` | 是否为主文件。每个版本最多一个主文件;若没有一个标记为 `true`,则推断第一个文件为主文件 | +| `hashes.sha1` | `string` | 文件的 SHA-1 哈希 | +| `hashes.sha512` | `string` | 文件的 SHA-512 哈希 | +| `file_type` | `string \| null` | 文件类型:`sources-jar`、`dev-jar`、`javadoc-jar`、`required-resource-pack`、`optional-resource-pack`、`unknown`、`signature`。普通主 jar 为 `null` | + +--- + +## 常见使用场景 + +### 场景 1:获取项目的最新版本 + +从版本列表中取第一个(通常是时间最新的),再拿到文件信息: + +```js +const projectSlug = "my-mod"; +const response = await fetch( + `https://api.modrinth.com/v2/project/${projectSlug}/version?include_changelog=false` +); +const versions = await response.json(); + +const latest = versions[0]; +console.log(`版本: ${latest.name} (${latest.version_number})`); +console.log(`主文件: ${latest.files.find(f => f.primary).filename}`); +``` + +### 场景 2:列出某版本的所有文件及其类型 + +```js +const versionId = "IIJJKKLL"; +const response = await fetch(`https://api.modrinth.com/v2/version/${versionId}`); +const version = await response.json(); + +for (const file of version.files) { + const typeLabel = file.file_type ?? "主 jar"; + console.log(`[${typeLabel}] ${file.filename} — ${file.url}`); +} +``` + +输出示例: + +``` +[主 jar] my-mod-1.0.0.jar — https://cdn.modrinth.com/.../my-mod-1.0.0.jar +[sources-jar] my-mod-1.0.0-sources.jar — https://cdn.modrinth.com/.../my-mod-1.0.0-sources.jar +``` + +### 场景 3:下载版本的主文件(通过直链) + +```js +const version = await fetch("https://api.modrinth.com/v2/version/IIJJKKLL").then(r => r.json()); +const primaryFile = version.files.find(f => f.primary) ?? version.files[0]; + +const fileBuffer = await fetch(primaryFile.url).then(r => r.arrayBuffer()); +require("fs").writeFileSync(primaryFile.filename, Buffer.from(fileBuffer)); +``` + +### 场景 4:仅获取特定加载器 + 游戏版本的版本 + +```js +const projectId = "AABBCCDD"; +const url = new URL(`https://api.modrinth.com/v2/project/${projectId}/version`); +url.searchParams.set("loaders", JSON.stringify(["fabric"])); +url.searchParams.set("game_versions", JSON.stringify(["1.20.4"])); +url.searchParams.set("include_changelog", "false"); + +const versions = await fetch(url).then(r => r.json()); +for (const v of versions) { + console.log(`${v.name} (${v.version_number}) — ${v.files.find(f => f.primary)?.filename}`); +} +``` + +--- + +## 错误码 + +| 状态码 | 说明 | +|--------|------| +| `200` | 成功 | +| `404` | 项目不存在、版本不存在,或无权访问(如草稿版本) | + +--- + +## 注意事项与最佳实践 + +### 1. 始终使用 `include_changelog=false` + +除非你真的需要每个版本的 changelog 文本,否则务必传入 `include_changelog=false`。changelog 可能非常长,会显著增大响应体、降低请求速度。Modrinth 官方也强烈建议这样做。 + +### 2. 主文件的判断规则 + +`files` 数组中若有一个 `primary: true`,那就是主文件。如果一个都没有,则**推断第一个文件为主文件**。不要假定 `files[0]` 一定是主 jar——sources jar 也可能排在首位。 + +### 3. 版本列表的排序 + +`GET /project/{id|slug}/version` 返回的列表**按发布日期降序排列**(最新的在前)。若需确定顺序,请以 `date_published` 字段为准。 + +### 4. 别忘了 User-Agent + +即使是公开 GET 请求,Modrinth 也要求设置 `User-Agent` 请求头: + +``` +User-Agent: your_username/your_project/1.0.0 (contact@example.com) +``` + +### 5. 通过 slug 访问更易读 + +项目标识符支持 slug(如 `my-mod`)而非只能用 ID(如 `AABBCCDD`),对于调试和可读性更有帮助。但 slug 可能被项目所有者修改,长期存储建议使用 ID。 + +### 6. 版本号可能不唯一 + +`GET /project/{id|slug}/version/{id\|number}` 在使用版本号查找时,若遇到重名只返回最旧的那个。精确获取请使用版本 ID。 + +### 7. 分页 + +`GET /project/{id|slug}/version` **不支持分页参数**,会一次性返回该项目所有版本。对于版本数量极多的项目,响应体可能较大,此时推荐结合 `loaders` 和 `game_versions` 过滤。 + +--- + +## 可用加载器与游戏版本查询 + +如果你不确定 `loaders` 或 `game_versions` 过滤参数应填什么值,可以通过标签接口查询所有合法值: + +```bash +curl "https://api.modrinth.com/v2/tag/loader" +curl "https://api.modrinth.com/v2/tag/game_version" +``` + +--- + +## 参考 + +- [Modrinth API 文档](https://docs.modrinth.com) +- [OpenAPI 规范文件](openapi.yml) +- [新建版本 API 文档](upload.md) diff --git a/docs/modrinth/openapi.yml b/docs/modrinth/openapi.yml new file mode 100644 index 0000000..b49357c --- /dev/null +++ b/docs/modrinth/openapi.yml @@ -0,0 +1,3984 @@ +openapi: '3.0.0' + +info: + version: v2.7.0/366f528 + title: Labrinth + termsOfService: https://modrinth.com/legal/terms + contact: + name: Modrinth Support + url: https://support.modrinth.com + email: support@modrinth.com + description: | + This documentation doesn't provide a way to test our API. In order to facilitate testing, we recommend the following tools: + + - [cURL](https://curl.se/) (recommended, command-line) + - [ReqBIN](https://reqbin.com/) (recommended, online) + - [Postman](https://www.postman.com/downloads/) + - [Insomnia](https://insomnia.rest/) + - Your web browser, if you don't need to send headers or a request body + + Once you have a working client, you can test that it works by making a `GET` request to `https://staging-api.modrinth.com/`: + + ```json + { + "about": "Welcome traveler!", + "documentation": "https://docs.modrinth.com", + "name": "modrinth-labrinth", + "version": "2.7.0" + } + ``` + + If you got a response similar to the one above, you can use the Modrinth API! + When you want to go live using the production API, use `api.modrinth.com` instead of `staging-api.modrinth.com`. + + ## Authentication + This API has two options for authentication: personal access tokens and [OAuth2](https://en.wikipedia.org/wiki/OAuth). + All tokens are tied to a Modrinth user and use the `Authorization` header of the request. + + Example: + ``` + Authorization: mrp_RNtLRSPmGj2pd1v1ubi52nX7TJJM9sznrmwhAuj511oe4t1jAqAQ3D6Wc8Ic + ``` + + You do not need a token for most requests. Generally speaking, only the following types of requests require a token: + - those which create data (such as version creation) + - those which modify data (such as editing a project) + - those which access private data (such as draft projects, notifications, emails, and payout data) + + Each request requiring authentication has a certain scope. For example, to view the email of the user being requested, the token must have the `USER_READ_EMAIL` scope. + You can find the list of available scopes [on GitHub](https://github.com/modrinth/labrinth/blob/master/src/models/pats.rs#L15). Making a request with an invalid scope will return a 401 error. + + Please note that certain scopes and requests cannot be completed with a personal access token or using OAuth. + For example, deleting a user account can only be done through Modrinth's frontend. + + A detailed guide on OAuth has been published in [Modrinth's technical documentation](https://docs.modrinth.com/guide/oauth). + + ### Personal access tokens + Personal access tokens (PATs) can be generated in from [the user settings](https://modrinth.com/settings/account). + + ### GitHub tokens + For backwards compatibility purposes, some types of GitHub tokens also work for authenticating a user with Modrinth's API, granting all scopes. + **We urge any application still using GitHub tokens to start using personal access tokens for security and reliability purposes.** + GitHub tokens will cease to function to authenticate with Modrinth's API as soon as version 3 of the API is made generally available. + + ## Cross-Origin Resource Sharing + This API features Cross-Origin Resource Sharing (CORS) implemented in compliance with the [W3C spec](https://www.w3.org/TR/cors/). + This allows for cross-domain communication from the browser. + All responses have a wildcard same-origin which makes them completely public and accessible to everyone, including any code on any site. + + ## Identifiers + The majority of items you can interact with in the API have a unique eight-digit base62 ID. + Projects, versions, users, threads, teams, and reports all use this same way of identifying themselves. + Version files use the sha1 or sha512 file hashes as identifiers. + + Each project and user has a friendlier way of identifying them; slugs and usernames, respectively. + While unique IDs are constant, slugs and usernames can change at any moment. + If you want to store something in the long term, it is recommended to use the unique ID. + + ## Ratelimits + The API has a ratelimit defined per IP. Limits and remaining amounts are given in the response headers. + - `X-Ratelimit-Limit`: the maximum number of requests that can be made in a minute + - `X-Ratelimit-Remaining`: the number of requests remaining in the current ratelimit window + - `X-Ratelimit-Reset`: the time in seconds until the ratelimit window resets + + Ratelimits are the same no matter whether you use a token or not. + The ratelimit is currently 300 requests per minute. If you have a use case requiring a higher limit, please [contact us](mailto:support@modrinth.com). + + ## User Agents + To access the Modrinth API, you **must** use provide a uniquely-identifying `User-Agent` header. + Providing a user agent that only identifies your HTTP client library (such as "okhttp/4.9.3") increases the likelihood that we will block your traffic. + It is recommended, but not required, to include contact information in your user agent. + This allows us to contact you if we would like a change in your application's behavior without having to block your traffic. + - Bad: `User-Agent: okhttp/4.9.3` + - Good: `User-Agent: project_name` + - Better: `User-Agent: github_username/project_name/1.56.0` + - Best: `User-Agent: github_username/project_name/1.56.0 (launcher.com)` or `User-Agent: github_username/project_name/1.56.0 (contact@launcher.com)` + + ## Versioning + Modrinth follows a simple pattern for its API versioning. + In the event of a breaking API change, the API version in the URL path is bumped, and migration steps will be published below. + + When an API is no longer the current one, it will immediately be considered deprecated. + No more support will be provided for API versions older than the current one. + It will be kept for some time, but this amount of time is not certain. + + We will exercise various tactics to get people to update their implementation of our API. + One example is by adding something like `STOP USING THIS API` to various data returned by the API. + + Once an API version is completely deprecated, it will permanently return a 410 error. + Please ensure your application handles these 410 errors. + + ### Migrations + Inside the following spoiler, you will be able to find all changes between versions of the Modrinth API, accompanied by tips and a guide to migrate applications to newer versions. + + Here, you can also find changes for [Minotaur](https://github.com/modrinth/minotaur), Modrinth's official Gradle plugin. Major versions of Minotaur directly correspond to major versions of the Modrinth API. + +
API v1 to API v2 + + These bullet points cover most changes in the v2 API, but please note that fields containing `mod` in most contexts have been shifted to `project`. For example, in the search route, the field `mod_id` was renamed to `project_id`. + + - The search route has been moved from `/api/v1/mod` to `/v2/search` + - New project fields: `project_type` (may be `mod` or `modpack`), `moderation_message` (which has a `message` and `body`), `gallery` + - New search facet: `project_type` + - Alphabetical sort removed (it didn't work and is not possible due to limits in MeiliSearch) + - New search fields: `project_type`, `gallery` + - The gallery field is an array of URLs to images that are part of the project's gallery + - The gallery is a new feature which allows the user to upload images showcasing their mod to the CDN which will be displayed on their mod page + - Internal change: Any project file uploaded to Modrinth is now validated to make sure it's a valid Minecraft mod, Modpack, etc. + - For example, a Forge 1.17 mod with a JAR not containing a mods.toml will not be allowed to be uploaded to Modrinth + - In project creation, projects may not upload a mod with no versions to review, however they can be saved as a draft + - Similarly, for version creation, a version may not be uploaded without any files + - Donation URLs have been enabled + - New project status: `archived`. Projects with this status do not appear in search + - Tags (such as categories, loaders) now have icons (SVGs) and specific project types attached + - Dependencies have been wiped and replaced with a new system + - Notifications now have a `type` field, such as `project_update` + + Along with this, project subroutes (such as `/v2/project/{id}/version`) now allow the slug to be used as the ID. This is also the case with user routes. + +
Minotaur v1 to Minotaur v2 + + Minotaur 2.x introduced a few breaking changes to how your buildscript is formatted. + + First, instead of registering your own `publishModrinth` task, Minotaur now automatically creates a `modrinth` task. As such, you can replace the `task publishModrinth(type: TaskModrinthUpload) {` line with just `modrinth {`. + + To declare supported Minecraft versions and mod loaders, the `gameVersions` and `loaders` arrays must now be used. The syntax for these are pretty self-explanatory. + + Instead of using `releaseType`, you must now use `versionType`. This was actually changed in v1.2.0, but very few buildscripts have moved on from v1.1.0. + + Dependencies have been changed to a special DSL. Create a `dependencies` block within the `modrinth` block, and then use `scope.type("project/version")`. For example, `required.project("fabric-api")` adds a required project dependency on Fabric API. + + You may now use the slug anywhere that a project ID was previously required. + +
+ +# The above snippet about User Agents was adapted from https://crates.io/policies, copyright (c) 2014 The Rust Project Developers under MIT license + +servers: + - url: https://api.modrinth.com/v2 + description: Production server + - url: https://staging-api.modrinth.com/v2 + description: Staging server + +components: + parameters: + ProjectIdentifier: + name: id|slug + in: path + required: true + description: The ID or slug of the project + schema: + type: string + example: [AABBCCDD, my_project] + MultipleProjectIdentifier: + in: query + name: ids + description: The IDs and/or slugs of the projects + schema: + type: string + example: '["AABBCCDD", "EEFFGGHH"]' + required: true + UserIdentifier: + name: id|username + in: path + required: true + description: The ID or username of the user + schema: + type: string + example: [EEFFGGHH, my_user] + VersionIdentifier: + name: id + in: path + required: true + description: The ID of the version + schema: + type: string + example: [IIJJKKLL] + TeamIdentifier: + name: id + in: path + required: true + description: The ID of the team + schema: + type: string + example: [MMNNOOPP] + ReportIdentifier: + name: id + in: path + required: true + description: The ID of the report + schema: + type: string + example: [RRSSTTUU] + ThreadIdentifier: + name: id + in: path + required: true + description: The ID of the thread + schema: + type: string + example: [QQRRSSTT] + NotificationIdentifier: + name: id + in: path + required: true + description: The ID of the notification + schema: + type: string + example: [NNOOPPQQ] + AlgorithmIdentifier: + name: algorithm + in: query + required: true + description: The algorithm of the hash + schema: + type: string + enum: [sha1, sha512] + example: sha512 + default: sha1 + MultipleHashQueryIdentifier: + name: multiple + in: query + required: false + description: Whether to return multiple results when looking for this hash + schema: + type: boolean + default: false + FileHashIdentifier: + name: hash + in: path + required: true + description: The hash of the file, considering its byte content, and encoded in hexadecimal + schema: + type: string + example: 619e250c133106bacc3e3b560839bd4b324dfda8 + requestBodies: + Image: + content: + image/png: + schema: + type: string + format: binary + image/jpeg: + schema: + type: string + format: binary + image/bmp: + schema: + type: string + format: binary + image/gif: + schema: + type: string + format: binary + image/webp: + schema: + type: string + format: binary + image/svg: + schema: + type: string + format: binary + image/svgz: + schema: + type: string + format: binary + image/rgb: + schema: + type: string + format: binary + schemas: + # Version + BaseVersion: + type: object + properties: + name: + type: string + description: The name of this version + example: 'Version 1.0.0' + version_number: + type: string + description: 'The version number. Ideally will follow semantic versioning' + example: '1.0.0' + changelog: + type: string + description: 'The changelog for this version' + example: 'List of changes in this version: ...' + nullable: true + dependencies: + type: array + items: + $ref: '#/components/schemas/VersionDependency' + description: A list of specific versions of projects that this version depends on + game_versions: + type: array + items: + type: string + description: A list of versions of Minecraft that this version supports + example: ['1.16.5', '1.17.1'] + version_type: + type: string + description: The release channel for this version + enum: [release, beta, alpha] + example: release + loaders: + type: array + items: + type: string + description: The mod loaders that this version supports. In case of resource packs, use "minecraft" + example: ['fabric', 'forge', 'minecraft'] + featured: + type: boolean + description: Whether the version is featured or not + example: true + status: + type: string + enum: [listed, archived, draft, unlisted, scheduled, unknown] + example: listed + requested_status: + type: string + enum: [listed, archived, draft, unlisted] + nullable: true + VersionDependency: + type: object + properties: + version_id: + type: string + description: The ID of the version that this version depends on + example: IIJJKKLL + nullable: true + project_id: + type: string + description: The ID of the project that this version depends on + example: QQRRSSTT + nullable: true + file_name: + type: string + description: The file name of the dependency, mostly used for showing external dependencies on modpacks + example: sodium-fabric-mc1.19-0.4.2+build.16.jar + nullable: true + dependency_type: + type: string + enum: [required, optional, incompatible, embedded] + description: The type of dependency that this version has + example: required + required: + - dependency_type + + # https://github.com/modrinth/labrinth/blob/master/src/routes/versions.rs#L169-L190 + EditableVersion: + allOf: + - $ref: '#/components/schemas/BaseVersion' + - type: object + properties: + primary_file: + type: array + items: + type: string + example: [sha1, aaaabbbbccccddddeeeeffffgggghhhhiiiijjjj] + description: The hash format and the hash of the new primary file + file_types: + type: array + items: + $ref: '#/components/schemas/EditableFileType' + description: A list of file_types to edit + EditableFileType: + type: object + properties: + algorithm: + type: string + description: The hash algorithm of the hash specified in the hash field + example: sha1 + hash: + type: string + description: The hash of the file you're editing + example: aaaabbbbccccddddeeeeffffgggghhhhiiiijjjj + file_type: + allOf: + - $ref: '#/components/schemas/FileTypeEnum' + - nullable: true + description: The hash algorithm of the file you're editing + required: + - algorithm + - hash + - file_type + # https://github.com/modrinth/code/blob/main/apps/labrinth/src/models/v3/projects.rs#L981-990 + FileTypeEnum: + type: string + enum: + - required-resource-pack + - optional-resource-pack + - sources-jar + - dev-jar + - javadoc-jar + - unknown + - signature + example: required-resource-pack + # https://github.com/modrinth/code/blob/main/apps/labrinth/src/routes/v2/version_creation.rs#L32-L76 + CreatableVersion: + allOf: + - $ref: '#/components/schemas/BaseVersion' + - type: object + properties: + project_id: + type: string + description: The ID of the project this version is for + example: AABBCCDD + file_parts: + type: array + items: + type: string + description: An array of the multipart field names of each file that goes with this version + primary_file: + type: string + description: The multipart field name of the primary file + environment: + type: string + enum: + - client_and_server + - client_only + - client_only_server_optional + - singleplayer_only + - server_only + - server_only_client_optional + - dedicated_server_only + - client_or_server + - client_or_server_prefers_both + - unknown + description: The environment that this version is for. + file_types: + type: object + additionalProperties: + $ref: '#/components/schemas/FileTypeEnum' + description: A map of file parts to their associated file type, a file type is used for additional files such as sources jars. + required: + - file_parts + - project_id + - name + - version_number + - game_versions + - version_type + - loaders + - featured + - dependencies + CreateVersionBody: + type: object + properties: + data: + $ref: '#/components/schemas/CreatableVersion' + required: [data] + Version: + allOf: + - $ref: '#/components/schemas/BaseVersion' + - type: object + properties: + id: + type: string + description: The ID of the version, encoded as a base62 string + example: IIJJKKLL + project_id: + type: string + description: The ID of the project this version is for + example: AABBCCDD + author_id: + type: string + description: The ID of the author who published this version + example: EEFFGGHH + date_published: + type: string + format: ISO-8601 + downloads: + type: integer + description: The number of times this version has been downloaded + changelog_url: + type: string + description: A link to the changelog for this version. Always null, only kept for legacy compatibility. + deprecated: true + example: null + nullable: true + files: + type: array + items: + $ref: '#/components/schemas/VersionFile' + description: A list of files available for download for this version + required: + - id + - project_id + - author_id + - date_published + - downloads + - files + - name + - version_number + - game_versions + - version_type + - loaders + - featured + VersionFile: + type: object + properties: + hashes: + $ref: '#/components/schemas/VersionFileHashes' + url: + type: string + example: 'https://cdn.modrinth.com/data/AABBCCDD/versions/1.0.0/my_file.jar' + description: A direct link to the file + filename: + type: string + example: 'my_file.jar' + description: The name of the file + primary: + type: boolean + example: false + description: Whether this file is the primary one for its version. Only a maximum of one file per version will have this set to true. If there are not any primary files, it can be inferred that the first file is the primary one. + size: + type: integer + example: 1097270 + description: The size of the file in bytes + file_type: + allOf: + - $ref: '#/components/schemas/FileTypeEnum' + - nullable: true + description: The type of the additional file, used mainly for adding resource packs to datapacks + required: + - hashes + - url + - filename + - primary + - size + VersionFileHashes: + type: object + properties: + sha512: + type: string + example: 93ecf5fe02914fb53d94aa3d28c1fb562e23985f8e4d48b9038422798618761fe208a31ca9b723667a4e05de0d91a3f86bcd8d018f6a686c39550e21b198d96f + sha1: + type: string + example: c84dd4b3580c02b79958a0590afd5783d80ef504 + description: A map of hashes of the file. The key is the hashing algorithm and the value is the string version of the hash. + GetLatestVersionFromHashBody: + type: object + properties: + loaders: + type: array + items: + type: string + example: [fabric] + game_versions: + type: array + items: + type: string + example: ['1.18', 1.18.1] + required: + - loaders + - game_versions + HashVersionMap: + description: 'A map from hashes to versions' + type: object + additionalProperties: + $ref: '#/components/schemas/Version' + HashList: + description: 'A list of hashes and the algorithm used to create them' + type: object + properties: + hashes: + type: array + items: + type: string + example: + [ + ea0f38408102e4d2efd53c2cc11b88b711996b48d8922f76ea6abf731219c5bd1efe39ddf9cce77c54d49a62ff10fb685c00d2e4c524ab99d20f6296677ab2c4, + 925a5c4899affa4098d997dfa4a4cb52c636d539e94bc489d1fa034218cb96819a70eb8b01647a39316a59fcfe223c1a8c05ed2e2ae5f4c1e75fa48f6af1c960, + ] + algorithm: + type: string + enum: [sha1, sha512] + example: sha512 + required: + - hashes + - algorithm + GetLatestVersionsFromHashesBody: + allOf: + - $ref: '#/components/schemas/HashList' + - type: object + properties: + loaders: + type: array + items: + type: string + example: [fabric] + game_versions: + type: array + items: + type: string + example: ['1.18', 1.18.1] + required: + - loaders + - game_versions + # Project + # Fields that can be used in everything. Search, direct project lookup, project editing, you name it. + BaseProject: + type: object + properties: + slug: + type: string + description: "The slug of a project, used for vanity URLs. Regex: ```^[\\w!@$()`.+,\"\\-']{3,64}$```" + example: my_project + title: + type: string + description: The title or name of the project + example: My Project + description: + type: string + description: A short description of the project + example: A short description + categories: + type: array + items: + type: string + example: [technology, adventure, fabric] + description: A list of the categories that the project has + client_side: + type: string + enum: [required, optional, unsupported, unknown] + description: The client side support of the project + example: required + server_side: + type: string + enum: [required, optional, unsupported, unknown] + description: The server side support of the project + example: optional + # Fields added to search results and direct project lookups that cannot be edited. + ServerRenderedProject: + allOf: + - $ref: '#/components/schemas/BaseProject' + - type: object + properties: + project_type: + type: string + enum: [mod, modpack, resourcepack, shader] + description: The project type of the project + example: mod + downloads: + type: integer + description: The total number of downloads of the project + icon_url: + type: string + example: https://cdn.modrinth.com/data/AABBCCDD/b46513nd83hb4792a9a0e1fn28fgi6090c1842639.png + description: The URL of the project's icon + nullable: true + color: + type: integer + example: 8703084 + description: The RGB color of the project, automatically generated from the project icon + nullable: true + thread_id: + type: string + example: TTUUVVWW + description: The ID of the moderation thread associated with this project + monetization_status: + type: string + enum: [monetized, demonetized, force-demonetized] + required: + - project_type + - downloads + # The actual result in search. + ProjectResult: + allOf: + - $ref: '#/components/schemas/ServerRenderedProject' + - type: object + properties: + project_id: + type: string + description: The ID of the project + example: AABBCCDD + author: + type: string + description: The username of the project's author + example: my_user + display_categories: + type: array + items: + type: string + description: A list of the categories that the project has which are not secondary + example: ['technology', 'fabric'] + versions: + type: array + items: + type: string + description: A list of the minecraft versions supported by the project + example: ['1.8', '1.8.9'] + follows: + type: integer + description: The total number of users following the project + date_created: + type: string + format: ISO-8601 + description: The date the project was added to search + date_modified: + type: string + format: ISO-8601 + description: The date the project was last modified + latest_version: + type: string + description: The latest version of minecraft that this project supports + example: 1.8.9 + license: + type: string + description: The SPDX license ID of a project + example: MIT + gallery: + type: array + description: All gallery images attached to the project + example: + [ + 'https://cdn.modrinth.com/data/AABBCCDD/images/009b7d8d6e8bf04968a29421117c59b3efe2351a.png', + 'https://cdn.modrinth.com/data/AABBCCDD/images/c21776867afb6046fdc3c21dbcf5cc50ae27a236.png', + ] + items: + type: string + featured_gallery: + type: string + description: The featured gallery image of the project + nullable: true + required: + - slug + - title + - description + - client_side + - server_side + - project_id + - author + - versions + - follows + - date_created + - date_modified + - license + # Fields that appear everywhere EXCEPT search. + NonSearchProject: + allOf: + - $ref: '#/components/schemas/BaseProject' + - type: object + properties: + body: + type: string + description: A long form description of the project + example: A long body describing my project in detail + status: + type: string + enum: + [ + approved, + archived, + rejected, + draft, + unlisted, + processing, + withheld, + scheduled, + private, + unknown, + ] + description: The status of the project + example: approved + requested_status: + type: string + enum: [approved, archived, unlisted, private, draft] + description: The requested status when submitting for review or scheduling the project for release + nullable: true + additional_categories: + type: array + items: + type: string + description: A list of categories which are searchable but non-primary + example: [technology, adventure, fabric] + issues_url: + type: string + description: An optional link to where to submit bugs or issues with the project + example: https://github.com/my_user/my_project/issues + nullable: true + source_url: + type: string + description: An optional link to the source code of the project + example: https://github.com/my_user/my_project + nullable: true + wiki_url: + type: string + description: An optional link to the project's wiki page or other relevant information + example: https://github.com/my_user/my_project/wiki + nullable: true + discord_url: + type: string + description: An optional invite link to the project's discord + example: https://discord.gg/AaBbCcDd + nullable: true + donation_urls: + type: array + items: + $ref: '#/components/schemas/ProjectDonationURL' + description: A list of donation links for the project + ProjectDonationURL: + type: object + properties: + id: + type: string + description: The ID of the donation platform + example: patreon + platform: + type: string + description: The donation platform this link is to + example: Patreon + url: + type: string + description: The URL of the donation platform and user + example: https://www.patreon.com/my_user + # Fields available only when editing or creating a project + ModifiableProject: + allOf: + - $ref: '#/components/schemas/NonSearchProject' + - type: object + properties: + license_id: + type: string + description: The SPDX license ID of a project + example: LGPL-3.0-or-later + license_url: + type: string + description: The URL to this license + nullable: true + # Fields that can be edited through a PATCH request. https://github.com/modrinth/labrinth/blob/master/src/routes/projects.rs#L195-L269 + EditableProject: + allOf: + - $ref: '#/components/schemas/ModifiableProject' + - type: object + properties: + moderation_message: + type: string + description: The title of the moderators' message for the project + nullable: true + moderation_message_body: + type: string + description: The body of the moderators' message for the project + nullable: true + # Fields only available for project creation. https://github.com/modrinth/labrinth/blob/master/src/routes/project_creation.rs#L129-L197 + CreatableProject: + allOf: + - $ref: '#/components/schemas/ModifiableProject' + - type: object + properties: + project_type: + type: string + enum: [mod, modpack] + example: modpack + initial_versions: + type: array + items: + $ref: '#/components/schemas/EditableVersion' + description: A list of initial versions to upload with the created project. Deprecated - please upload version files after initial upload. + deprecated: true + is_draft: + type: boolean + description: Whether the project should be saved as a draft instead of being sent to moderation for review. Deprecated - please always mark this as true. + example: true + deprecated: true + gallery_items: + type: array + description: Gallery images to be uploaded with the created project. Deprecated - please upload gallery images after initial upload. + deprecated: true + items: + $ref: '#/components/schemas/CreatableProjectGalleryItem' + required: + - project_type + - slug + - title + - description + - body + - categories + - client_side + - server_side + - license_id + CreatableProjectGalleryItem: + type: object + nullable: true + properties: + item: + type: string + description: The name of the multipart item where the gallery media is located + featured: + type: boolean + description: Whether the image is featured in the gallery + example: true + title: + type: string + description: The title of the gallery image + example: My awesome screenshot! + nullable: true + description: + type: string + description: The description of the gallery image + example: This awesome screenshot shows all of the blocks in my mod! + nullable: true + ordering: + type: integer + description: The order of the gallery image. Gallery images are sorted by this field and then alphabetically by title. + example: 0 + Project: + allOf: + - $ref: '#/components/schemas/NonSearchProject' + - $ref: '#/components/schemas/ServerRenderedProject' + - type: object + properties: + id: + type: string + example: AABBCCDD + description: The ID of the project, encoded as a base62 string + team: + type: string + example: MMNNOOPP + description: The ID of the team that has ownership of this project + body_url: + type: string + deprecated: true + default: null + description: The link to the long description of the project. Always null, only kept for legacy compatibility. + example: null + nullable: true + moderator_message: + $ref: '#/components/schemas/ModeratorMessage' + published: + type: string + format: ISO-8601 + description: The date the project was published + updated: + type: string + format: ISO-8601 + description: The date the project was last updated + approved: + type: string + format: ISO-8601 + description: The date the project's status was set to an approved status + nullable: true + queued: + type: string + format: ISO-8601 + description: The date the project's status was submitted to moderators for review + nullable: true + followers: + type: integer + description: The total number of users following the project + license: + $ref: '#/components/schemas/ProjectLicense' + versions: + type: array + items: + type: string + example: [IIJJKKLL, QQRRSSTT] + description: A list of the version IDs of the project (will never be empty unless `draft` status) + game_versions: + type: array + items: + type: string + example: ['1.19', '1.19.1', '1.19.2', '1.19.3'] + description: A list of all of the game versions supported by the project + loaders: + type: array + items: + type: string + example: ['forge', 'fabric', 'quilt'] + description: A list of all of the loaders supported by the project + gallery: + type: array + items: + $ref: '#/components/schemas/GalleryImage' + description: A list of images that have been uploaded to the project's gallery + required: + - id + - team + - published + - updated + - followers + - title + - description + - categories + - client_side + - server_side + - slug + - body + - status + ModeratorMessage: + deprecated: true + type: object + properties: + message: + type: string + description: The message that a moderator has left for the project + body: + type: string + description: The longer body of the message that a moderator has left for the project + nullable: true + nullable: true + example: null + description: A message that a moderator sent regarding the project + ProjectLicense: + type: object + properties: + id: + type: string + description: The SPDX license ID of a project + example: LGPL-3.0-or-later + name: + type: string + description: The long name of a license + example: GNU Lesser General Public License v3 or later + url: + type: string + description: The URL to this license + nullable: true + description: The license of the project + GalleryImage: + type: object + nullable: true + properties: + url: + type: string + description: The URL of the gallery image + example: https://cdn.modrinth.com/data/AABBCCDD/images/009b7d8d6e8bf04968a29421117c59b3efe2351a.png + featured: + type: boolean + description: Whether the image is featured in the gallery + example: true + title: + type: string + description: The title of the gallery image + example: My awesome screenshot! + nullable: true + description: + type: string + description: The description of the gallery image + example: This awesome screenshot shows all of the blocks in my mod! + nullable: true + created: + type: string + format: ISO-8601 + description: The date and time the gallery image was created + ordering: + type: integer + description: The order of the gallery image. Gallery images are sorted by this field and then alphabetically by title. + example: 0 + required: + - url + - featured + - created + ProjectDependencyList: + type: object + properties: + projects: + type: array + items: + $ref: '#/components/schemas/Project' + description: Projects that the project depends upon + versions: + type: array + items: + $ref: '#/components/schemas/Version' + description: Versions that the project depends upon + PatchProjectsBody: + type: object + properties: + categories: + description: Set all of the categories to the categories specified here + type: array + items: + type: string + add_categories: + description: Add all of the categories specified here + type: array + items: + type: string + remove_categories: + description: Remove all of the categories specified here + type: array + items: + type: string + additional_categories: + description: Set all of the additional categories to the categories specified here + type: array + items: + type: string + add_additional_categories: + description: Add all of the additional categories specified here + type: array + items: + type: string + remove_additional_categories: + description: Remove all of the additional categories specified here + type: array + items: + type: string + donation_urls: + description: Set all of the donation links to the donation links specified here + type: array + items: + $ref: '#/components/schemas/ProjectDonationURL' + add_donation_urls: + description: Add all of the donation links specified here + type: array + items: + $ref: '#/components/schemas/ProjectDonationURL' + remove_donation_urls: + description: Remove all of the donation links specified here + type: array + items: + $ref: '#/components/schemas/ProjectDonationURL' + issues_url: + type: string + description: An optional link to where to submit bugs or issues with the projects + example: https://github.com/my_user/my_project/issues + nullable: true + source_url: + type: string + description: An optional link to the source code of the projects + example: https://github.com/my_user/my_project + nullable: true + wiki_url: + type: string + description: An optional link to the projects' wiki page or other relevant information + example: https://github.com/my_user/my_project/wiki + nullable: true + discord_url: + type: string + description: An optional invite link to the projects' discord + example: https://discord.gg/AaBbCcDd + nullable: true + CreateProjectBody: + type: object + properties: + data: + $ref: '#/components/schemas/CreatableProject' + icon: + type: string + format: binary + enum: ['*.png', '*.jpg', '*.jpeg', '*.bmp', '*.gif', '*.webp', '*.svg', '*.svgz', '*.rgb'] + description: Project icon file + required: [data] + ProjectIdentifier: + type: object + properties: + id: + type: string + example: AABBCCDD + Schedule: + type: object + properties: + time: + type: string + format: ISO-8601 + example: '2023-02-05T19:39:55.551839Z' + requested_status: + type: string + enum: [approved, archived, unlisted, private, draft] + description: The requested status when scheduling the project for release + required: + - time + - requested_status + # Search + SearchResults: + type: object + properties: + hits: + type: array + items: + $ref: '#/components/schemas/ProjectResult' + description: The list of results + offset: + type: integer + description: The number of results that were skipped by the query + example: 0 + limit: + type: integer + description: The number of results that were returned by the query + example: 10 + total_hits: + type: integer + description: The total number of results that match the query + example: 10 + required: + - hits + - offset + - limit + - total_hits + # User + UserIdentifier: + properties: + user_id: + type: string + example: EEFFGGHH + required: + - user_id + EditableUser: + type: object + properties: + username: + type: string + description: The user's username + example: my_user + name: + type: string + example: My User + description: The user's display name + nullable: true + email: + type: string + format: email + description: The user's email (only displayed if requesting your own account). Requires `USER_READ_EMAIL` PAT scope. + nullable: true + bio: + type: string + example: My short biography + description: A description of the user + payout_data: + $ref: '#/components/schemas/UserPayoutData' + required: + - username + UserPayoutData: + type: object + description: Various data relating to the user's payouts status (you can only see your own) + nullable: true + properties: + balance: + type: integer + description: The payout balance available for the user to withdraw (note, you cannot modify this in a PATCH request) + example: 10.11223344556677889900 + payout_wallet: + type: string + enum: [paypal, venmo] + description: The wallet that the user has selected + example: paypal + payout_wallet_type: + type: string + enum: [email, phone, user_handle] + description: The type of the user's wallet + example: email + payout_address: + type: string + description: The user's payout address + example: support@modrinth.com + User: + allOf: + - $ref: '#/components/schemas/EditableUser' + - type: object + properties: + id: + type: string + example: EEFFGGHH + description: The user's ID + avatar_url: + type: string + example: https://avatars.githubusercontent.com/u/11223344?v=1 + description: The user's avatar url + created: + type: string + format: ISO-8601 + description: The time at which the user was created + role: + type: string + enum: [admin, moderator, developer] + description: The user's role + example: developer + badges: + type: integer + format: bitfield + example: 63 + description: | + Any badges applicable to this user. These are currently unused and undisplayed, and as such are subject to change + + In order from first to seventh bit, the current bits are: + - (unused) + - EARLY_MODPACK_ADOPTER + - EARLY_RESPACK_ADOPTER + - EARLY_PLUGIN_ADOPTER + - ALPHA_TESTER + - CONTRIBUTOR + - TRANSLATOR + auth_providers: + type: array + items: + type: string + example: [github, gitlab, steam, microsoft, google, discord] + description: A list of authentication providers you have signed up for (only displayed if requesting your own account) + nullable: true + email_verified: + type: boolean + description: Whether your email is verified (only displayed if requesting your own account) + nullable: true + has_password: + type: boolean + description: Whether you have a password associated with your account (only displayed if requesting your own account) + nullable: true + has_totp: + type: boolean + description: Whether you have TOTP two-factor authentication connected to your account (only displayed if requesting your own account) + nullable: true + github_id: + deprecated: true + type: integer + description: Deprecated - this is no longer public for security reasons and is always null + example: null + nullable: true + required: + - id + - avatar_url + - created + - role + UserPayoutHistory: + type: object + properties: + all_time: + type: string + description: The all-time balance accrued by this user in USD + example: 10.11223344556677889900 + last_month: + type: string + description: The amount in USD made by the user in the previous 30 days + example: 2.22446688002244668800 + payouts: + type: array + description: A history of all of the user's past transactions + items: + $ref: '#/components/schemas/UserPayoutHistoryEntry' + UserPayoutHistoryEntry: + type: object + properties: + created: + type: string + format: ISO-8601 + description: The date of this transaction + amount: + type: integer + description: The amount of this transaction in USD + example: 10.00 + status: + type: string + description: The status of this transaction + example: success + # Notifications + Notification: + type: object + properties: + id: + type: string + description: The id of the notification + example: UUVVWWXX + user_id: + type: string + description: The id of the user who received the notification + example: EEFFGGHH + type: + type: string + enum: [project_update, team_invite, status_change, moderator_message] + description: The type of notification + example: project_update + nullable: true + title: + type: string + description: The title of the notification + example: '**My Project** has been updated!' + text: + type: string + description: The body text of the notification + example: 'The project, My Project, has released a new version: 1.0.0' + link: + type: string + description: A link to the related project or version + example: mod/AABBCCDD/version/IIJJKKLL + read: + type: boolean + example: false + description: Whether the notification has been read or not + created: + type: string + format: ISO-8601 + description: The time at which the notification was created + actions: + type: array + items: + $ref: '#/components/schemas/NotificationAction' + description: A list of actions that can be performed + required: + - id + - user_id + - title + - text + - link + - read + - created + - actions + NotificationAction: + type: object + description: An action that can be performed on a notification + properties: + title: + type: string + description: The friendly name for this action + example: Accept + action_route: + type: array + items: + type: string + description: The HTTP code and path to request in order to perform this action. + example: [POST, 'team/{id}/join'] + # Reports + CreatableReport: + type: object + properties: + report_type: + type: string + description: The type of the report being sent + example: copyright + item_id: + type: string + description: The ID of the item (project, version, or user) being reported + example: EEFFGGHH + item_type: + type: string + enum: [project, user, version] + description: The type of the item being reported + example: project + body: + type: string + description: The extended explanation of the report + example: This is a reupload of my mod, AABBCCDD! + required: + - report_type + - item_id + - item_type + - body + Report: + type: object + allOf: + - $ref: '#/components/schemas/CreatableReport' + - type: object + properties: + id: + type: string + description: The ID of the report + example: VVWWXXYY + reporter: + type: string + description: The ID of the user who reported the item + example: UUVVWWXX + created: + type: string + format: ISO-8601 + description: The time at which the report was created + closed: + type: boolean + description: Whether the report is resolved + thread_id: + type: string + example: TTUUVVWW + description: The ID of the moderation thread associated with this report + required: + - reporter + - created + - closed + - thread_id + # Threads + Thread: + type: object + properties: + id: + type: string + example: WWXXYYZZ + description: The ID of the thread + type: + type: string + enum: [project, report, direct_message] + project_id: + type: string + nullable: true + description: The ID of the associated project if a project thread + report_id: + type: string + nullable: true + description: The ID of the associated report if a report thread + messages: + type: array + items: + $ref: '#/components/schemas/ThreadMessage' + members: + type: array + items: + $ref: '#/components/schemas/User' + required: + - id + - type + - messages + - members + ThreadMessage: + type: object + properties: + id: + type: string + description: The ID of the message itself + example: MMNNOOPP + author_id: + type: string + description: The ID of the author + example: QQRRSSTT + nullable: true + body: + $ref: '#/components/schemas/ThreadMessageBody' + created: + type: string + format: ISO-8601 + description: The time at which the message was created + required: + - id + - body + - created + ThreadMessageBody: + type: object + description: The contents of the message. **Fields will vary depending on message type.** + properties: + type: + type: string + enum: [status_change, text, thread_closure, deleted] + description: The type of message + example: status_change + body: + type: string + description: The actual message text. **Only present for `text` message type** + example: This is the text of the message. + private: + type: boolean + description: Whether the message is only visible to moderators. **Only present for `text` message type** + example: false + replying_to: + type: string + description: The ID of the message being replied to by this message. **Only present for `text` message type** + nullable: true + example: SSTTUUVV + old_status: + type: string + enum: + [ + approved, + archived, + rejected, + draft, + unlisted, + processing, + withheld, + scheduled, + private, + unknown, + ] + description: The old status of the project. **Only present for `status_change` message type** + example: processing + new_status: + type: string + enum: + [ + approved, + archived, + rejected, + draft, + unlisted, + processing, + withheld, + scheduled, + private, + unknown, + ] + description: The new status of the project. **Only present for `status_change` message type** + example: approved + required: + - type + # Team + TeamMember: + type: object + properties: + team_id: + type: string + example: MMNNOOPP + description: The ID of the team this team member is a member of + user: + $ref: '#/components/schemas/User' + role: + type: string + example: Member + description: The user's role on the team + permissions: + type: integer + format: bitfield + example: 127 + description: | + The user's permissions in bitfield format (requires authorization to view) + + In order from first to tenth bit, the bits are: + - UPLOAD_VERSION + - DELETE_VERSION + - EDIT_DETAILS + - EDIT_BODY + - MANAGE_INVITES + - REMOVE_MEMBER + - EDIT_MEMBER + - DELETE_PROJECT + - VIEW_ANALYTICS + - VIEW_PAYOUTS + accepted: + type: boolean + example: true + description: Whether or not the user has accepted to be on the team (requires authorization to view) + payouts_split: + type: integer + example: 100 + description: The split of payouts going to this user. The proportion of payouts they get is their split divided by the sum of the splits of all members. + ordering: + type: integer + example: 0 + description: The order of the team member. + required: + - team_id + - user + - role + - accepted + # Tags + CategoryTag: + type: object + properties: + icon: + type: string + description: The SVG icon of a category + example: + name: + type: string + description: The name of the category + example: 'adventure' + project_type: + type: string + description: The project type this category is applicable to + example: mod + header: + type: string + description: The header under which the category should go + example: 'resolutions' + required: + - icon + - name + - project_type + - header + LoaderTag: + type: object + properties: + icon: + type: string + description: The SVG icon of a loader + example: + name: + type: string + description: The name of the loader + example: fabric + supported_project_types: + type: array + items: + type: string + description: The project type + description: The project types that this loader is applicable to + example: [mod, modpack] + required: + - icon + - name + - supported_project_types + GameVersionTag: + type: object + properties: + version: + type: string + description: The name/number of the game version + example: 1.18.1 + version_type: + type: string + enum: [release, snapshot, alpha, beta] + description: The type of the game version + example: release + date: + type: string + format: ISO-8601 + description: The date of the game version release + major: + type: boolean + description: Whether or not this is a major version, used for Featured Versions + example: true + required: + - version + - version_type + - date + - major + DonationPlatformTag: + type: object + properties: + short: + type: string + description: The short identifier of the donation platform + example: bmac + name: + type: string + description: The full name of the donation platform + example: Buy Me a Coffee + required: + - short + - name + ModifyTeamMemberBody: + properties: + role: + type: string + example: Contributor + permissions: + type: integer + format: bitfield + example: 127 + description: | + The user's permissions in bitfield format + + In order from first to tenth bit, the bits are: + - UPLOAD_VERSION + - DELETE_VERSION + - EDIT_DETAILS + - EDIT_BODY + - MANAGE_INVITES + - REMOVE_MEMBER + - EDIT_MEMBER + - DELETE_PROJECT + - VIEW_ANALYTICS + - VIEW_PAYOUTS + payouts_split: + type: integer + example: 100 + description: The split of payouts going to this user. The proportion of payouts they get is their split divided by the sum of the splits of all members. + ordering: + type: integer + example: 0 + description: The order of the team member. + LicenseTag: + type: object + description: A short overview of a license + properties: + short: + type: string + description: The short identifier of the license + example: lgpl-3 + name: + type: string + description: The full name of the license + example: GNU Lesser General Public License v3 + required: + - short + - name + License: + type: object + description: A full license + properties: + title: + type: string + example: GNU Lesser General Public License v3.0 or later + body: + type: string + example: Insert the entire text of the LGPL-3.0 here... + # Errors + InvalidInputError: + type: object + properties: + error: + type: string + description: The name of the error + example: 'invalid_input' + description: + type: string + description: The contents of the error + example: 'Error while parsing multipart payload' + required: + - error + - description + AuthError: + type: object + properties: + error: + type: string + description: The name of the error + example: 'unauthorized' + description: + type: string + description: The contents of the error + example: 'Authentication Error: Invalid Authentication Credentials' + required: + - error + - description + # Other + Statistics: + type: object + properties: + projects: + type: integer + description: Number of projects on Modrinth + versions: + type: integer + description: Number of versions on Modrinth + files: + type: integer + description: Number of version files on Modrinth + authors: + type: integer + description: Number of authors (users with projects) on Modrinth + ForgeUpdates: + type: object + description: Mod version information that can be consumed by Forge's update checker + properties: + homepage: + type: string + description: A link to the mod page + example: https://modrinth.com + promos: + $ref: '#/components/schemas/ForgeUpdateCheckerPromos' + ForgeUpdateCheckerPromos: + type: object + description: A list of the recommended and latest versions for each Minecraft release + properties: + '{version}-recommended': + type: string + description: The mod version that is recommended for `{version}`. Excludes versions with the `alpha` and `beta` version types. + '{version}-latest': + type: string + description: The latest mod version for `{version}`. Shows versions with the `alpha` and `beta` version types. + securitySchemes: + TokenAuth: + type: apiKey + in: header + name: Authorization + +tags: + - name: projects + x-displayName: Projects + description: Projects are what Modrinth is centered around, be it mods, modpacks, resource packs, etc. + - name: versions + x-displayName: Versions + description: Versions contain download links to files with additional metadata. + - name: version-files + x-displayName: Version Files + description: Versions can contain multiple files, and these routes help manage those files. + - name: users + x-displayName: Users + description: Users can create projects, join teams, access notifications, manage settings, and follow projects. Admins and moderators have more advanced permissions such as reviewing new projects. + - name: notifications + x-displayName: Notifications + description: Notifications are sent to users for various reasons, including for project updates, team invites, and moderation purposes. + - name: threads + x-displayName: Threads + description: Threads are a way of communicating between users and moderators, for the purposes of project reviews and reports. + - name: teams + x-displayName: Teams + description: Through teams, user permissions limit how team members can modify projects. + - name: tags + x-displayName: Tags + description: Tags are common and reusable lists of metadata types such as categories or versions. Some can be applied to projects and/or versions. + - name: misc + x-displayName: Miscellaneous + - name: project_model + x-displayName: Project Model + description: | + + - name: project_result_model + x-displayName: Search Result Model + description: | + + - name: version_model + x-displayName: Version Model + description: | + + - name: user_model + x-displayName: User Model + description: | + + - name: team_member_model + x-displayName: Team Member Model + description: | + + +x-tagGroups: + - name: Routes + tags: + - projects + - versions + - version-files + - users + - notifications + - threads + - teams + - tags + - misc + - name: Models + tags: + - project_model + - project_result_model + - version_model + - user_model + - team_member_model + +paths: + # Project + /search: + get: + summary: Search projects + operationId: searchProjects + parameters: + - in: query + name: query + schema: + type: string + example: gravestones + description: The query to search for + - in: query + name: facets + schema: + type: string + example: '[["categories:forge"],["versions:1.17.1"],["project_type:mod"],["license:mit"]]' + description: | + Facets are an essential concept for understanding how to filter out results. + + These are the most commonly used facet types: + - `project_type` + - `categories` (loaders are lumped in with categories in search) + - `versions` + - `client_side` + - `server_side` + - `open_source` + + Several others are also available for use, though these should not be used outside very specific use cases. + - `title` + - `author` + - `follows` + - `project_id` + - `license` + - `downloads` + - `color` + - `created_timestamp` (uses Unix timestamp) + - `modified_timestamp` (uses Unix timestamp) + - `date_created` (uses ISO-8601 timestamp) + - `date_modified` (uses ISO-8601 timestamp) + + In order to then use these facets, you need a value to filter by, as well as an operation to perform on this value. + The most common operation is `:` (same as `=`), though you can also use `!=`, `>=`, `>`, `<=`, and `<`. + Join together the type, operation, and value, and you've got your string. + ``` + {type} {operation} {value} + ``` + + Examples: + ``` + categories = adventure + versions != 1.20.1 + downloads <= 100 + ``` + + You then join these strings together in arrays to signal `AND` and `OR` operators. + + ##### OR + All elements in a single array are considered to be joined by OR statements. + For example, the search `[["versions:1.16.5", "versions:1.17.1"]]` translates to `Projects that support 1.16.5 OR 1.17.1`. + + ##### AND + Separate arrays are considered to be joined by AND statements. + For example, the search `[["versions:1.16.5"], ["project_type:modpack"]]` translates to `Projects that support 1.16.5 AND are modpacks`. + - in: query + name: index + schema: + type: string + enum: + - relevance + - downloads + - follows + - newest + - updated + default: relevance + example: downloads + description: The sorting method used for sorting search results + - in: query + name: offset + schema: + type: integer + default: 0 + example: 20 + description: The offset into the search. Skips this number of results + - in: query + name: limit + schema: + type: integer + default: 10 + example: 20 + minimum: 0 + maximum: 100 + description: The number of results returned by the search + tags: + - projects + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + $ref: '#/components/schemas/SearchResults' + '400': + description: Request was invalid, see given error + content: + application/json: + schema: + $ref: '#/components/schemas/InvalidInputError' + /project/{id|slug}: + parameters: + - $ref: '#/components/parameters/ProjectIdentifier' + get: + summary: Get a project + operationId: getProject + tags: + - projects + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + $ref: '#/components/schemas/Project' + '404': + description: The requested item(s) were not found or no authorization to access the requested item(s) + patch: + summary: Modify a project + operationId: modifyProject + tags: + - projects + security: + - TokenAuth: ['PROJECT_WRITE'] + requestBody: + description: 'Modified project fields' + content: + application/json: + schema: + $ref: '#/components/schemas/EditableProject' + responses: + '204': + description: Expected response to a valid request + '401': + description: Incorrect token scopes or no authorization to access the requested item(s) + content: + application/json: + schema: + $ref: '#/components/schemas/AuthError' + '404': + description: The requested item(s) were not found or no authorization to access the requested item(s) + delete: + summary: Delete a project + operationId: deleteProject + tags: + - projects + security: + - TokenAuth: ['PROJECT_DELETE'] + responses: + '204': + description: Expected response to a valid request + '400': + description: Request was invalid, see given error + content: + application/json: + schema: + $ref: '#/components/schemas/InvalidInputError' + '401': + description: Incorrect token scopes or no authorization to access the requested item(s) + content: + application/json: + schema: + $ref: '#/components/schemas/AuthError' + /projects: + parameters: + - $ref: '#/components/parameters/MultipleProjectIdentifier' + get: + summary: Get multiple projects + operationId: getProjects + tags: + - projects + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Project' + patch: + summary: Bulk-edit multiple projects + operationId: patchProjects + tags: + - projects + security: + - TokenAuth: ['PROJECT_WRITE'] + requestBody: + description: Fields to edit on all projects specified + content: + application/json: + schema: + $ref: '#/components/schemas/PatchProjectsBody' + responses: + '204': + description: Expected response to a valid request + '400': + description: Request was invalid, see given error + content: + application/json: + schema: + $ref: '#/components/schemas/InvalidInputError' + '401': + description: Incorrect token scopes or no authorization to access the requested item(s) + content: + application/json: + schema: + $ref: '#/components/schemas/AuthError' + /projects_random: + get: + summary: Get a list of random projects + operationId: randomProjects + parameters: + - in: query + name: count + required: true + schema: + type: integer + example: 70 + minimum: 0 + maximum: 100 + description: The number of random projects to return + tags: + - projects + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Project' + '400': + description: Request was invalid, see given error + content: + application/json: + schema: + $ref: '#/components/schemas/InvalidInputError' + /project: + post: + summary: Create a project + operationId: createProject + tags: + - projects + security: + - TokenAuth: ['PROJECT_CREATE'] + requestBody: + description: 'New project' + content: + multipart/form-data: + schema: + $ref: '#/components/schemas/CreateProjectBody' + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + $ref: '#/components/schemas/Project' + '400': + description: Request was invalid, see given error + content: + application/json: + schema: + $ref: '#/components/schemas/InvalidInputError' + '401': + description: Incorrect token scopes or no authorization to access the requested item(s) + content: + application/json: + schema: + $ref: '#/components/schemas/AuthError' + /project/{id|slug}/icon: + parameters: + - $ref: '#/components/parameters/ProjectIdentifier' + patch: + summary: Change project's icon + description: The new icon may be up to 256KiB in size. + operationId: changeProjectIcon + tags: + - projects + parameters: + - description: Image extension + in: query + name: ext + required: true + schema: + type: string + enum: [png, jpg, jpeg, bmp, gif, webp, svg, svgz, rgb] + requestBody: + $ref: '#/components/requestBodies/Image' + security: + - TokenAuth: ['PROJECT_WRITE'] + responses: + '204': + description: Expected response to a valid request + '400': + description: Request was invalid, see given error + content: + application/json: + schema: + $ref: '#/components/schemas/InvalidInputError' + delete: + summary: Delete project's icon + operationId: deleteProjectIcon + tags: + - projects + security: + - TokenAuth: ['PROJECT_WRITE'] + responses: + '204': + description: Expected response to a valid request + '400': + description: Request was invalid, see given error + content: + application/json: + schema: + $ref: '#/components/schemas/InvalidInputError' + '401': + description: Incorrect token scopes or no authorization to access the requested item(s) + content: + application/json: + schema: + $ref: '#/components/schemas/AuthError' + /project/{id|slug}/check: + parameters: + - $ref: '#/components/parameters/ProjectIdentifier' + get: + summary: Check project slug/ID validity + operationId: checkProjectValidity + tags: + - projects + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + $ref: '#/components/schemas/ProjectIdentifier' + '404': + description: The requested item(s) were not found or no authorization to access the requested item(s) + /project/{id|slug}/gallery: + parameters: + - $ref: '#/components/parameters/ProjectIdentifier' + post: + summary: Add a gallery image + description: Modrinth allows you to upload files of up to 5MiB to a project's gallery. + operationId: addGalleryImage + tags: + - projects + security: + - TokenAuth: ['PROJECT_WRITE'] + parameters: + - description: Image extension + in: query + name: ext + required: true + schema: + type: string + enum: [png, jpg, jpeg, bmp, gif, webp, svg, svgz, rgb] + - description: Whether an image is featured + in: query + name: featured + required: true + schema: + type: boolean + - description: Title of the image + in: query + name: title + schema: + type: string + - description: Description of the image + in: query + name: description + schema: + type: string + - description: Ordering of the image + in: query + name: ordering + schema: + type: integer + requestBody: + $ref: '#/components/requestBodies/Image' + responses: + '204': + description: Expected response to a valid request + '400': + description: Request was invalid, see given error + content: + application/json: + schema: + $ref: '#/components/schemas/InvalidInputError' + '401': + description: Incorrect token scopes or no authorization to access the requested item(s) + content: + application/json: + schema: + $ref: '#/components/schemas/AuthError' + '404': + description: The requested item(s) were not found or no authorization to access the requested item(s) + patch: + summary: Modify a gallery image + operationId: modifyGalleryImage + tags: + - projects + security: + - TokenAuth: ['PROJECT_WRITE'] + parameters: + - description: URL link of the image to modify + in: query + name: url + required: true + schema: + type: string + format: uri + - description: Whether the image is featured + in: query + name: featured + schema: + type: boolean + - description: New title of the image + in: query + name: title + schema: + type: string + - description: New description of the image + in: query + name: description + schema: + type: string + - description: New ordering of the image + in: query + name: ordering + schema: + type: integer + responses: + '204': + description: Expected response to a valid request + '401': + description: Incorrect token scopes or no authorization to access the requested item(s) + content: + application/json: + schema: + $ref: '#/components/schemas/AuthError' + '404': + description: The requested item(s) were not found or no authorization to access the requested item(s) + delete: + summary: Delete a gallery image + operationId: deleteGalleryImage + tags: + - projects + security: + - TokenAuth: ['PROJECT_WRITE'] + parameters: + - description: URL link of the image to delete + in: query + name: url + required: true + schema: + type: string + format: uri + responses: + '204': + description: Expected response to a valid request + '400': + description: Request was invalid, see given error + content: + application/json: + schema: + $ref: '#/components/schemas/InvalidInputError' + '401': + description: Incorrect token scopes or no authorization to access the requested item(s) + content: + application/json: + schema: + $ref: '#/components/schemas/AuthError' + /project/{id|slug}/dependencies: + parameters: + - $ref: '#/components/parameters/ProjectIdentifier' + get: + summary: Get all of a project's dependencies + operationId: getDependencies + tags: + - projects + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + $ref: '#/components/schemas/ProjectDependencyList' + '404': + description: The requested item(s) were not found or no authorization to access the requested item(s) + /project/{id|slug}/follow: + parameters: + - $ref: '#/components/parameters/ProjectIdentifier' + post: + summary: Follow a project + operationId: followProject + tags: + - projects + security: + - TokenAuth: ['USER_WRITE'] + responses: + '204': + description: Expected response to a valid request + '400': + description: Request was invalid, see given error + content: + application/json: + schema: + $ref: '#/components/schemas/InvalidInputError' + '401': + description: Incorrect token scopes or no authorization to access the requested item(s) + content: + application/json: + schema: + $ref: '#/components/schemas/AuthError' + delete: + summary: Unfollow a project + operationId: unfollowProject + tags: + - projects + security: + - TokenAuth: ['USER_WRITE'] + responses: + '204': + description: Expected response to a valid request + '400': + description: Request was invalid, see given error + content: + application/json: + schema: + $ref: '#/components/schemas/InvalidInputError' + '401': + description: Incorrect token scopes or no authorization to access the requested item(s) + content: + application/json: + schema: + $ref: '#/components/schemas/AuthError' + /project/{id|slug}/schedule: + parameters: + - $ref: '#/components/parameters/ProjectIdentifier' + post: + summary: Schedule a project + operationId: scheduleProject + tags: + - projects + security: + - TokenAuth: ['PROJECT_WRITE'] + requestBody: + description: Information about date and requested status + content: + application/json: + schema: + $ref: '#/components/schemas/Schedule' + responses: + '204': + description: Expected response to a valid request + '400': + description: Request was invalid, see given error + content: + application/json: + schema: + $ref: '#/components/schemas/InvalidInputError' + '401': + description: Incorrect token scopes or no authorization to access the requested item(s) + content: + application/json: + schema: + $ref: '#/components/schemas/AuthError' + # Version + /project/{id|slug}/version: + parameters: + - $ref: '#/components/parameters/ProjectIdentifier' + get: + summary: List project's versions + operationId: getProjectVersions + tags: + - versions + parameters: + - in: query + name: loaders + required: false + description: 'The types of loaders to filter for' + schema: + type: string + example: '["fabric"]' + - in: query + name: game_versions + required: false + description: 'The game versions to filter for' + schema: + type: string + example: '["1.18.1"]' + - in: query + name: featured + required: false + description: 'Allows to filter for featured or non-featured versions only' + schema: + type: boolean + - in: query + name: include_changelog + required: false + description: 'Allows you to toggle the inclusion of the changelog field in the response. It is highly recommended to use include_changelog=false in most cases unless you specifically need the changelog for all versions.' + schema: + type: boolean + default: true + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Version' + '404': + description: The requested item(s) were not found or no authorization to access the requested item(s) + /version/{id}: + parameters: + - $ref: '#/components/parameters/VersionIdentifier' + get: + summary: Get a version + operationId: getVersion + tags: + - versions + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + $ref: '#/components/schemas/Version' + '404': + description: The requested item(s) were not found or no authorization to access the requested item(s) + patch: + summary: Modify a version + operationId: modifyVersion + tags: + - versions + security: + - TokenAuth: ['VERSION_WRITE'] + requestBody: + description: 'Modified version fields' + content: + application/json: + schema: + $ref: '#/components/schemas/EditableVersion' + responses: + '204': + description: Expected response to a valid request + '401': + description: Incorrect token scopes or no authorization to access the requested item(s) + content: + application/json: + schema: + $ref: '#/components/schemas/AuthError' + '404': + description: The requested item(s) were not found or no authorization to access the requested item(s) + delete: + summary: Delete a version + operationId: deleteVersion + tags: + - versions + security: + - TokenAuth: ['VERSION_DELETE'] + responses: + '204': + description: Expected response to a valid request + '401': + description: Incorrect token scopes or no authorization to access the requested item(s) + content: + application/json: + schema: + $ref: '#/components/schemas/AuthError' + '404': + description: The requested item(s) were not found or no authorization to access the requested item(s) + /project/{id|slug}/version/{id|number}: + parameters: + - $ref: '#/components/parameters/ProjectIdentifier' + - name: id|number + in: path + required: true + description: The version ID or version number + schema: + type: string + example: [IIJJKKLL] + get: + summary: Get a version given a version number or ID + description: Please note that, if the version number provided matches multiple versions, only the **oldest matching version** will be returned. + operationId: getVersionFromIdOrNumber + tags: + - versions + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + $ref: '#/components/schemas/Version' + '404': + description: The requested item(s) were not found or no authorization to access the requested item(s) + /version: + post: + summary: Create a version + description: | + This route creates a version on an existing project. There must be at least one file attached to each new version, unless the new version's status is `draft`. `.mrpack`, `.jar`, `.zip`, and `.litemod` files are accepted. + + The request is a [multipart request](https://www.ietf.org/rfc/rfc2388.txt) with at least two form fields: one is `data`, which includes a JSON body with the version metadata as shown below, and at least one field containing an upload file. + + You can name the file parts anything you would like, but you must list each of the parts' names in `file_parts`, and optionally, provide one to use as the primary file in `primary_file`. + operationId: createVersion + tags: + - versions + security: + - TokenAuth: ['VERSION_CREATE'] + requestBody: + description: 'New version' + content: + multipart/form-data: + schema: + $ref: '#/components/schemas/CreateVersionBody' + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + $ref: '#/components/schemas/Version' + '400': + description: Request was invalid, see given error + content: + application/json: + schema: + $ref: '#/components/schemas/InvalidInputError' + '401': + description: Incorrect token scopes or no authorization to access the requested item(s) + content: + application/json: + schema: + $ref: '#/components/schemas/AuthError' + /version/{id}/schedule: + parameters: + - $ref: '#/components/parameters/VersionIdentifier' + post: + summary: Schedule a version + operationId: scheduleVersion + tags: + - versions + security: + - TokenAuth: ['VERSION_WRITE'] + requestBody: + description: Information about date and requested status + content: + application/json: + schema: + $ref: '#/components/schemas/Schedule' + responses: + '204': + description: Expected response to a valid request + '400': + description: Request was invalid, see given error + content: + application/json: + schema: + $ref: '#/components/schemas/InvalidInputError' + '401': + description: Incorrect token scopes or no authorization to access the requested item(s) + content: + application/json: + schema: + $ref: '#/components/schemas/AuthError' + /versions: + parameters: + - in: query + name: ids + description: The IDs of the versions + schema: + type: string + example: '["AABBCCDD", "EEFFGGHH"]' + required: true + get: + summary: Get multiple versions + operationId: getVersions + tags: + - versions + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Version' + /version/{id}/file: + parameters: + - $ref: '#/components/parameters/VersionIdentifier' + post: + summary: Add files to version + description: Project files are attached. `.mrpack` and `.jar` files are accepted. + operationId: addFilesToVersion + tags: + - versions + security: + - TokenAuth: ['VERSION_WRITE'] + requestBody: + description: 'New version files' + content: + multipart/form-data: + schema: + type: object + properties: + data: + type: object + enum: + - {} + responses: + '204': + description: Expected response to a valid request + '401': + description: Incorrect token scopes or no authorization to access the requested item(s) + content: + application/json: + schema: + $ref: '#/components/schemas/AuthError' + '404': + description: The requested item(s) were not found or no authorization to access the requested item(s) + # Version file + /version_file/{hash}: + parameters: + - $ref: '#/components/parameters/FileHashIdentifier' + - $ref: '#/components/parameters/AlgorithmIdentifier' + get: + summary: Get version from hash + operationId: versionFromHash + tags: + - version-files + parameters: + - $ref: '#/components/parameters/MultipleHashQueryIdentifier' + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + $ref: '#/components/schemas/Version' + '404': + description: The requested item(s) were not found or no authorization to access the requested item(s) + delete: + summary: Delete a file from its hash + operationId: deleteFileFromHash + tags: + - version-files + security: + - TokenAuth: ['VERSION_WRITE'] + parameters: + - description: Version ID to delete the version from, if multiple files of the same hash exist + required: false + in: query + name: version_id + schema: + type: string + example: [IIJJKKLL] + responses: + '204': + description: Expected response to a valid request + '401': + description: Incorrect token scopes or no authorization to access the requested item(s) + content: + application/json: + schema: + $ref: '#/components/schemas/AuthError' + '404': + description: The requested item(s) were not found or no authorization to access the requested item(s) + /version_file/{hash}/update: + parameters: + - $ref: '#/components/parameters/FileHashIdentifier' + - $ref: '#/components/parameters/AlgorithmIdentifier' + post: + summary: Latest version of a project from a hash, loader(s), and game version(s) + operationId: getLatestVersionFromHash + tags: + - version-files + requestBody: + description: Parameters of the updated version requested + content: + application/json: + schema: + $ref: '#/components/schemas/GetLatestVersionFromHashBody' + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + $ref: '#/components/schemas/Version' + '400': + description: Request was invalid, see given error + '404': + description: The requested item(s) were not found or no authorization to access the requested item(s) + /version_files: + post: + summary: Get versions from hashes + description: This is the same as [`/version_file/{hash}`](#operation/versionFromHash) except it accepts multiple hashes. + operationId: versionsFromHashes + tags: + - version-files + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + $ref: '#/components/schemas/HashVersionMap' + '400': + description: Request was invalid, see given error + requestBody: + description: Hashes and algorithm of the versions requested + content: + application/json: + schema: + $ref: '#/components/schemas/HashList' + /version_files/update: + post: + summary: Latest versions of multiple project from hashes, loader(s), and game version(s) + description: This is the same as [`/version_file/{hash}/update`](#operation/getLatestVersionFromHash) except it accepts multiple hashes. + operationId: getLatestVersionsFromHashes + tags: + - version-files + requestBody: + description: Parameters of the updated version requested + content: + application/json: + schema: + $ref: '#/components/schemas/GetLatestVersionsFromHashesBody' + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + $ref: '#/components/schemas/HashVersionMap' + '400': + description: Request was invalid, see given error + # TODO check this out? https://github.com/modrinth/labrinth/blob/ec80c2b9dbf0bae98eb41714d3455b98095563b7/src/routes/v2/version_file.rs#L381 + #/version_files/project: + # post: + # summary: Get projects from hashes + # operationId: projectsFromHashes + # tags: + # - version-files + # responses: + # "200": + # description: Expected response to a valid request + # content: + # application/json: + # schema: + # type: object + # properties: + # your_hash_here: + # $ref: '#/components/schemas/Project' + # "400": + # description: Input is invalid + # requestBody: + # description: Hashes and algorithm of the projects requested + # content: + # application/json: + # schema: + # type: object + # properties: + # hashes: + # type: array + # items: + # type: string + # example: [ ea0f38408102e4d2efd53c2cc11b88b711996b48d8922f76ea6abf731219c5bd1efe39ddf9cce77c54d49a62ff10fb685c00d2e4c524ab99d20f6296677ab2c4, 925a5c4899affa4098d997dfa4a4cb52c636d539e94bc489d1fa034218cb96819a70eb8b01647a39316a59fcfe223c1a8c05ed2e2ae5f4c1e75fa48f6af1c960 ] + # algorithm: + # type: string + # enum: [ sha1, sha512 ] + # example: sha512 + # required: + # - hashes + # - algorithm + # User + /user/{id|username}: + parameters: + - $ref: '#/components/parameters/UserIdentifier' + get: + summary: Get a user + operationId: getUser + tags: + - users + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + $ref: '#/components/schemas/User' + '404': + description: The requested item(s) were not found or no authorization to access the requested item(s) + patch: + summary: Modify a user + operationId: modifyUser + tags: + - users + security: + - TokenAuth: ['USER_WRITE'] + requestBody: + description: 'Modified user fields' + content: + application/json: + schema: + $ref: '#/components/schemas/EditableUser' + responses: + '204': + description: Expected response to a valid request + '401': + description: Incorrect token scopes or no authorization to access the requested item(s) + content: + application/json: + schema: + $ref: '#/components/schemas/AuthError' + '404': + description: The requested item(s) were not found or no authorization to access the requested item(s) + /user: + get: + summary: Get user from authorization header + operationId: getUserFromAuth + tags: + - users + security: + - TokenAuth: ['USER_READ'] + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + $ref: '#/components/schemas/User' + '401': + description: Incorrect token scopes or no authorization to access the requested item(s) + content: + application/json: + schema: + $ref: '#/components/schemas/AuthError' + /users: + parameters: + - in: query + name: ids + description: The IDs of the users + schema: + type: string + example: '["AABBCCDD", "EEFFGGHH"]' + required: true + get: + summary: Get multiple users + operationId: getUsers + tags: + - users + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + /user/{id|username}/icon: + parameters: + - $ref: '#/components/parameters/UserIdentifier' + patch: + summary: Change user's avatar + description: The new avatar may be up to 2MiB in size. + operationId: changeUserIcon + tags: + - users + requestBody: + $ref: '#/components/requestBodies/Image' + security: + - TokenAuth: ['USER_WRITE'] + responses: + '204': + description: Expected response to a valid request + '400': + description: Request was invalid, see given error + content: + application/json: + schema: + $ref: '#/components/schemas/InvalidInputError' + '404': + description: The requested item(s) were not found or no authorization to access the requested item(s) + delete: + summary: Remove user's avatar + operationId: deleteUserIcon + tags: + - users + security: + - TokenAuth: ['USER_WRITE'] + responses: + '204': + description: Expected response to a valid request + '400': + description: Request was invalid, see given error + content: + application/json: + schema: + $ref: '#/components/schemas/InvalidInputError' + '404': + description: The requested item(s) were not found or no authorization to access the requested item(s) + /user/{id|username}/projects: + parameters: + - $ref: '#/components/parameters/UserIdentifier' + get: + summary: Get user's projects + operationId: getUserProjects + tags: + - users + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Project' + '404': + description: The requested item(s) were not found or no authorization to access the requested item(s) + /user/{id|username}/follows: + parameters: + - $ref: '#/components/parameters/UserIdentifier' + get: + summary: Get user's followed projects + operationId: getFollowedProjects + tags: + - users + security: + - TokenAuth: ['USER_READ'] + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Project' + '401': + description: Incorrect token scopes or no authorization to access the requested item(s) + content: + application/json: + schema: + $ref: '#/components/schemas/AuthError' + '404': + description: The requested item(s) were not found or no authorization to access the requested item(s) + /user/{id|username}/payouts: + parameters: + - $ref: '#/components/parameters/UserIdentifier' + get: + summary: Get user's payout history + operationId: getPayoutHistory + tags: + - users + security: + - TokenAuth: ['PAYOUTS_READ'] + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + $ref: '#/components/schemas/UserPayoutHistory' + '401': + description: Incorrect token scopes or no authorization to access the requested item(s) + content: + application/json: + schema: + $ref: '#/components/schemas/AuthError' + '404': + description: The requested item(s) were not found or no authorization to access the requested item(s) + post: + summary: Withdraw payout balance to PayPal or Venmo + operationId: withdrawPayout + description: 'Warning: certain amounts get withheld for fees. Please do not call this API endpoint without first acknowledging the warnings on the corresponding frontend page.' + tags: + - users + security: + - TokenAuth: ['PAYOUTS_WRITE'] + parameters: + - name: amount + in: query + description: Amount to withdraw + schema: + type: integer + required: true + responses: + '204': + description: Expected response to a valid request + '401': + description: Incorrect token scopes or no authorization to access the requested item(s) + content: + application/json: + schema: + $ref: '#/components/schemas/AuthError' + '404': + description: The requested item(s) were not found or no authorization to access the requested item(s) + # Notifications + /user/{id|username}/notifications: + parameters: + - $ref: '#/components/parameters/UserIdentifier' + get: + summary: Get user's notifications + operationId: getUserNotifications + tags: + - notifications + security: + - TokenAuth: ['NOTIFICATION_READ'] + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Notification' + '401': + description: Incorrect token scopes or no authorization to access the requested item(s) + content: + application/json: + schema: + $ref: '#/components/schemas/AuthError' + '404': + description: The requested item(s) were not found or no authorization to access the requested item(s) + /notification/{id}: + parameters: + - $ref: '#/components/parameters/NotificationIdentifier' + get: + summary: Get notification from ID + operationId: getNotification + tags: + - notifications + security: + - TokenAuth: ['NOTIFICATION_READ'] + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + $ref: '#/components/schemas/Notification' + '401': + description: Incorrect token scopes or no authorization to access the requested item(s) + content: + application/json: + schema: + $ref: '#/components/schemas/AuthError' + '404': + description: The requested item(s) were not found or no authorization to access the requested item(s) + patch: + summary: Mark notification as read + operationId: readNotification + tags: + - notifications + security: + - TokenAuth: ['NOTIFICATION_WRITE'] + responses: + '204': + description: Expected response to a valid request + '401': + description: Incorrect token scopes or no authorization to access the requested item(s) + content: + application/json: + schema: + $ref: '#/components/schemas/AuthError' + '404': + description: The requested item(s) were not found or no authorization to access the requested item(s) + delete: + summary: Delete notification + operationId: deleteNotification + tags: + - notifications + security: + - TokenAuth: ['NOTIFICATION_WRITE'] + responses: + '204': + description: Expected response to a valid request + '401': + description: Incorrect token scopes or no authorization to access the requested item(s) + content: + application/json: + schema: + $ref: '#/components/schemas/AuthError' + '404': + description: The requested item(s) were not found or no authorization to access the requested item(s) + /notifications: + parameters: + - in: query + name: ids + description: The IDs of the notifications + schema: + type: string + example: '["AABBCCDD", "EEFFGGHH"]' + required: true + get: + summary: Get multiple notifications + operationId: getNotifications + tags: + - notifications + security: + - TokenAuth: ['NOTIFICATION_READ'] + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Notification' + '401': + description: Incorrect token scopes or no authorization to access the requested item(s) + content: + application/json: + schema: + $ref: '#/components/schemas/AuthError' + '404': + description: The requested item(s) were not found or no authorization to access the requested item(s) + patch: + summary: Mark multiple notifications as read + operationId: readNotifications + tags: + - notifications + security: + - TokenAuth: ['NOTIFICATION_WRITE'] + responses: + '204': + description: Expected response to a valid request + '401': + description: Incorrect token scopes or no authorization to access the requested item(s) + content: + application/json: + schema: + $ref: '#/components/schemas/AuthError' + '404': + description: The requested item(s) were not found or no authorization to access the requested item(s) + delete: + summary: Delete multiple notifications + operationId: deleteNotifications + tags: + - notifications + security: + - TokenAuth: ['NOTIFICATION_WRITE'] + responses: + '204': + description: Expected response to a valid request + '401': + description: Incorrect token scopes or no authorization to access the requested item(s) + content: + application/json: + schema: + $ref: '#/components/schemas/AuthError' + '404': + description: The requested item(s) were not found or no authorization to access the requested item(s) + # Threads + /report: + post: + summary: Report a project, user, or version + description: Bring a project, user, or version to the attention of the moderators by reporting it. + operationId: submitReport + tags: + - threads + security: + - TokenAuth: ['REPORT_CREATE'] + requestBody: + description: The report to be sent + content: + application/json: + schema: + $ref: '#/components/schemas/CreatableReport' + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + $ref: '#/components/schemas/Report' + '400': + description: Request was invalid, see given error + content: + application/json: + schema: + $ref: '#/components/schemas/InvalidInputError' + '401': + description: Incorrect token scopes or no authorization to access the requested item(s) + content: + application/json: + schema: + $ref: '#/components/schemas/AuthError' + get: + summary: Get your open reports + operationId: getOpenReports + tags: + - threads + security: + - TokenAuth: ['REPORT_READ'] + parameters: + - in: query + name: count + schema: + type: integer + example: 100 + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Report' + '401': + description: Incorrect token scopes or no authorization to access the requested item(s) + content: + application/json: + schema: + $ref: '#/components/schemas/AuthError' + '404': + description: The requested item(s) were not found or no authorization to access the requested item(s) + /report/{id}: + parameters: + - $ref: '#/components/parameters/ReportIdentifier' + get: + summary: Get report from ID + operationId: getReport + tags: + - threads + security: + - TokenAuth: ['REPORT_READ'] + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + $ref: '#/components/schemas/Report' + '401': + description: Incorrect token scopes or no authorization to access the requested item(s) + content: + application/json: + schema: + $ref: '#/components/schemas/AuthError' + '404': + description: The requested item(s) were not found or no authorization to access the requested item(s) + patch: + summary: Modify a report + operationId: modifyReport + tags: + - threads + security: + - TokenAuth: ['REPORT_WRITE'] + requestBody: + description: What to modify about the report + content: + application/json: + schema: + type: object + properties: + body: + type: string + description: The contents of the report + example: This is the meat and potatoes of the report! + closed: + type: boolean + description: Whether the thread should be closed + responses: + '204': + description: Expected response to a valid request + '400': + description: Request was invalid, see given error + content: + application/json: + schema: + $ref: '#/components/schemas/InvalidInputError' + '401': + description: Incorrect token scopes or no authorization to access the requested item(s) + content: + application/json: + schema: + $ref: '#/components/schemas/AuthError' + '404': + description: The requested item(s) were not found or no authorization to access the requested item(s) + /reports: + parameters: + - in: query + name: ids + description: The IDs of the reports + schema: + type: string + example: '["AABBCCDD", "EEFFGGHH"]' + required: true + get: + summary: Get multiple reports + operationId: getReports + tags: + - threads + security: + - TokenAuth: ['REPORT_READ'] + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Report' + '401': + description: Incorrect token scopes or no authorization to access the requested item(s) + content: + application/json: + schema: + $ref: '#/components/schemas/AuthError' + '404': + description: The requested item(s) were not found or no authorization to access the requested item(s) + /thread/{id}: + parameters: + - $ref: '#/components/parameters/ThreadIdentifier' + get: + summary: Get a thread + operationId: getThread + tags: + - threads + security: + - TokenAuth: ['THREAD_READ'] + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + $ref: '#/components/schemas/Thread' + '404': + description: The requested item(s) were not found or no authorization to access the requested item(s) + post: + summary: Send a text message to a thread + operationId: sendThreadMessage + tags: + - threads + security: + - TokenAuth: ['THREAD_WRITE'] + requestBody: + description: The message to be sent. Note that you only need the fields applicable for the `text` type. + content: + application/json: + schema: + $ref: '#/components/schemas/ThreadMessageBody' + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + $ref: '#/components/schemas/Thread' + '400': + description: Request was invalid, see given error + content: + application/json: + schema: + $ref: '#/components/schemas/InvalidInputError' + '404': + description: The requested item(s) were not found or no authorization to access the requested item(s) + /threads: + parameters: + - in: query + name: ids + description: The IDs of the threads + schema: + type: string + example: '["AABBCCDD", "EEFFGGHH"]' + required: true + get: + summary: Get multiple threads + operationId: getThreads + tags: + - threads + security: + - TokenAuth: ['THREAD_READ'] + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Thread' + '404': + description: The requested item(s) were not found or no authorization to access the requested item(s) + /message/{id}: + parameters: + - name: id + in: path + required: true + description: The ID of the message + schema: + type: string + example: [IIJJKKLL] + delete: + summary: Delete a thread message + operationId: deleteThreadMessage + tags: + - threads + security: + - TokenAuth: ['THREAD_WRITE'] + responses: + '204': + description: Expected response to a valid request + '401': + description: Incorrect token scopes or no authorization to access the requested item(s) + content: + application/json: + schema: + $ref: '#/components/schemas/AuthError' + '404': + description: The requested item(s) were not found or no authorization to access the requested item(s) + # Teams + /project/{id|slug}/members: + parameters: + - $ref: '#/components/parameters/ProjectIdentifier' + get: + summary: Get a project's team members + operationId: getProjectTeamMembers + tags: + - teams + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/TeamMember' + description: An array of team members + '404': + description: The requested item(s) were not found or no authorization to access the requested item(s) + /team/{id}/members: + parameters: + - $ref: '#/components/parameters/TeamIdentifier' + get: + summary: Get a team's members + operationId: getTeamMembers + tags: + - teams + security: + - TokenAuth: ['PROJECT_READ'] + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/TeamMember' + description: An array of team members + post: + summary: Add a user to a team + operationId: addTeamMember + tags: + - teams + security: + - TokenAuth: ['PROJECT_WRITE'] + requestBody: + description: User to be added (must be the ID, usernames cannot be used here) + content: + application/json: + schema: + $ref: '#/components/schemas/UserIdentifier' + responses: + '204': + description: Expected response to a valid request + '401': + description: Incorrect token scopes or no authorization to access the requested item(s) + content: + application/json: + schema: + $ref: '#/components/schemas/AuthError' + '404': + description: The requested item(s) were not found or no authorization to access the requested item(s) + /teams: + parameters: + - in: query + name: ids + description: The IDs of the teams + schema: + type: string + example: '["AABBCCDD", "EEFFGGHH"]' + required: true + get: + summary: Get the members of multiple teams + operationId: getTeams + tags: + - teams + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + type: array + items: + type: array + items: + $ref: '#/components/schemas/TeamMember' + /team/{id}/join: + parameters: + - $ref: '#/components/parameters/TeamIdentifier' + post: + summary: Join a team + operationId: joinTeam + tags: + - teams + security: + - TokenAuth: ['PROJECT_WRITE'] + responses: + '204': + description: Expected response to a valid request + '401': + description: Incorrect token scopes or no authorization to access the requested item(s) + content: + application/json: + schema: + $ref: '#/components/schemas/AuthError' + '404': + description: The requested item(s) were not found or no authorization to access the requested item(s) + /team/{id}/members/{id|username}: + parameters: + - $ref: '#/components/parameters/TeamIdentifier' + - $ref: '#/components/parameters/UserIdentifier' + patch: + summary: Modify a team member's information + operationId: modifyTeamMember + tags: + - teams + security: + - TokenAuth: ['PROJECT_WRITE'] + requestBody: + description: Contents to be modified + content: + application/json: + schema: + $ref: '#/components/schemas/ModifyTeamMemberBody' + responses: + '204': + description: Expected response to a valid request + '401': + description: Incorrect token scopes or no authorization to access the requested item(s) + content: + application/json: + schema: + $ref: '#/components/schemas/AuthError' + '404': + description: The requested item(s) were not found or no authorization to access the requested item(s) + delete: + summary: Remove a member from a team + operationId: deleteTeamMember + tags: + - teams + security: + - TokenAuth: ['PROJECT_WRITE'] + responses: + '204': + description: Expected response to a valid request + '401': + description: Incorrect token scopes or no authorization to access the requested item(s) + content: + application/json: + schema: + $ref: '#/components/schemas/AuthError' + '404': + description: The requested item(s) were not found or no authorization to access the requested item(s) + /team/{id}/owner: + parameters: + - $ref: '#/components/parameters/TeamIdentifier' + patch: + summary: Transfer team's ownership to another user + operationId: transferTeamOwnership + tags: + - teams + security: + - TokenAuth: ['PROJECT_WRITE'] + requestBody: + description: New owner's ID + content: + application/json: + schema: + $ref: '#/components/schemas/UserIdentifier' + responses: + '204': + description: Expected response to a valid request + '401': + description: Incorrect token scopes or no authorization to access the requested item(s) + content: + application/json: + schema: + $ref: '#/components/schemas/AuthError' + '404': + description: The requested item(s) were not found or no authorization to access the requested item(s) + # Tags + /tag/category: + get: + summary: Get a list of categories + description: Gets an array of categories, their icons, and applicable project types + operationId: categoryList + tags: + - tags + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/CategoryTag' + /tag/loader: + get: + summary: Get a list of loaders + description: Gets an array of loaders, their icons, and supported project types + operationId: loaderList + tags: + - tags + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/LoaderTag' + /tag/game_version: + get: + summary: Get a list of game versions + description: Gets an array of game versions and information about them + operationId: versionList + tags: + - tags + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/GameVersionTag' + /tag/license: + get: + deprecated: true + summary: Get a list of licenses + description: Deprecated - simply use SPDX IDs. + operationId: licenseList + tags: + - tags + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/LicenseTag' + /tag/license/{id}: + parameters: + - name: id + in: path + required: true + description: The license ID to get the text of + schema: + type: string + example: [LGPL-3.0-or-later] + get: + summary: Get the text and title of a license + operationId: licenseText + tags: + - tags + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + $ref: '#/components/schemas/License' + '400': + description: Request was invalid, see given error + content: + application/json: + schema: + $ref: '#/components/schemas/InvalidInputError' + /tag/donation_platform: + get: + summary: Get a list of donation platforms + description: Gets an array of donation platforms and information about them + operationId: donationPlatformList + tags: + - tags + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/DonationPlatformTag' + /tag/report_type: + get: + summary: Get a list of report types + description: Gets an array of valid report types + operationId: reportTypeList + tags: + - tags + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + type: array + items: + type: string + example: [spam, copyright, inappropriate, malicious, name-squatting, other] + /tag/project_type: + get: + summary: Get a list of project types + description: Gets an array of valid project types + operationId: projectTypeList + tags: + - tags + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + type: array + items: + type: string + example: [mod, modpack, resourcepack, shader] + /tag/side_type: + get: + summary: Get a list of side types + description: Gets an array of valid side types + operationId: sideTypeList + tags: + - tags + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + type: array + items: + type: string + example: [required, optional, unsupported, unknown] + # Miscellaneous + /updates/{id|slug}/forge_updates.json: + parameters: + - $ref: '#/components/parameters/ProjectIdentifier' + - name: neoforge + in: query + description: Whether to include NeoForge versions. Can be `only` (NeoForge-only versions), `include` (both Forge and NeoForge versions), or omitted (Forge-only versions). + schema: + type: string + enum: [only, include] + example: include + servers: + - url: https://api.modrinth.com + description: Production server + - url: https://staging-api.modrinth.com + description: Staging server + get: + summary: Forge Updates JSON file + operationId: forgeUpdates + description: | + If you're a Forge mod developer, your Modrinth mods have an automatically generated `updates.json` using the + [Forge Update Checker](https://docs.minecraftforge.net/en/latest/misc/updatechecker/). + + The only setup is to insert the URL into the `[[mods]]` section of your `mods.toml` file as such: + + ```toml + [[mods]] + # the other stuff here - ID, version, display name, etc. + updateJSONURL = "https://api.modrinth.com/updates/{slug|ID}/forge_updates.json" + ``` + + Replace `{slug|id}` with the slug or ID of your project. + + Modrinth will handle the rest! When you update your mod, Forge will notify your users that their copy of your mod is out of date. + + Make sure that the version format you use for your Modrinth releases is the same as the version format you use in your `mods.toml`. + If you use a format such as `1.2.3-forge` or `1.2.3+1.19` with your Modrinth releases but your `mods.toml` only has `1.2.3`, + the update checker may not function properly. + + If you're using NeoForge, NeoForge versions will, by default, not appear in the default URL. + You will need to add `?neoforge=only` to show your NeoForge-only versions, or `?neoforge=include` for both. + + ```toml + [[mods]] + # the other stuff here - ID, version, display name, etc. + updateJSONURL = "https://api.modrinth.com/updates/{slug|ID}/forge_updates.json?neoforge=only" + ``` + tags: + - misc + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + $ref: '#/components/schemas/ForgeUpdates' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/InvalidInputError' + /statistics: + get: + summary: Various statistics about this Modrinth instance + operationId: statistics + tags: + - misc + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + $ref: '#/components/schemas/Statistics' diff --git a/docs/modrinth/upload.md b/docs/modrinth/upload.md new file mode 100644 index 0000000..3b500c5 --- /dev/null +++ b/docs/modrinth/upload.md @@ -0,0 +1,407 @@ +# Modrinth 新建版本 API 使用文档 + +本文档详细描述如何通过 Modrinth API 创建一个新版本,包括上传模组主文件(`.jar`)和一个 `-sources.jar` 附属文件。 + +--- + +## 基本信息 + +| 项目 | 内容 | +|------|------| +| **端点** | `POST /version` | +| **生产环境** | `https://api.modrinth.com/v2/version` | +| **测试环境** | `https://staging-api.modrinth.com/v2/version` | +| **Content-Type** | `multipart/form-data` | +| **认证** | 必需 — Personal Access Token (PAT) | +| **所需 Scope** | `VERSION_CREATE` | +| **成功响应码** | `200` | +| **成功响应体** | [Version](#响应格式) 对象 | + +--- + +## 认证 + +所有创建数据的请求都需要认证。你需要一个 **Personal Access Token**(PAT),在 [Modrinth 用户设置](https://modrinth.com/settings/account) 中生成。 + +创建版本需要 PAT 具有 `VERSION_CREATE` 权限范围。 + +将 Token 放在 `Authorization` 请求头中: + +``` +Authorization: mrp_RNtLRSPmGj2pd1v1ubi52nX7TJJM9sznrmwhAuj511oe4t1jAqAQ3D6Wc8Ic +``` + +> **注意:** 需要在 `upload.md` 中提供有效的 Token。 + +同时,Modrinth **要求**提供一个可唯一标识的 `User-Agent` 请求头: + +``` +User-Agent: github_username/project_name/1.0.0 (contact@example.com) +``` + +--- + +## 请求格式 + +请求是一个 **multipart/form-data** 请求,必须包含至少两个表单字段: + +- **`data`**:一个 JSON 字符串,包含版本元数据(见下方 [data JSON 结构](#data-json-结构))。 +- **一个或多个文件字段**:包含要上传的 `.jar`、`.mrpack`、`.zip` 或 `.litemod` 文件。文件字段名可自定义。 + +### 重要概念区分:multipart 字段名 vs 文件名 + +在 multipart 请求中,每个文件字段都有**两个不同的名字**: + +``` +-F "main-file=@./build/libs/my-mod-1.0.0.jar" + ^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^ + multipart 字段名 (field name) 文件名 (filename) +``` + +- **multipart 字段名**(如 `main-file`):在请求体中标识这一部分数据的 key,由你自定义。 +- **文件名**(如 `my-mod-1.0.0.jar`):被上传文件的原始名称。 + +> **关键规则:`file_parts`、`primary_file`、`file_types` 的 key 全部使用 multipart 字段名,而不是文件名。** + +### `file_parts` — 声明有哪些文件 + +在 `data` JSON 的 `file_parts` 数组中列出所有承载文件的 **multipart 字段名**,并在 `primary_file` 中指定哪个是主文件。 + +```json +{ + "file_parts": ["main-file", "sources-file", "dev-file"], + "primary_file": "main-file" +} +``` + +### `file_types` — 标记附属文件类型 + +`file_types` 是一个 **对象(map)**,**key 是 multipart 字段名**(与 `file_parts` 中的值一致),value 是该文件的类型枚举值。用于标记 `-sources.jar`、`-dev.jar` 等附属文件。 + +可用的文件类型(`FileTypeEnum`): + +| 值 | 含义 | +|----|------| +| `required-resource-pack` | 必需资源包 | +| `optional-resource-pack` | 可选资源包 | +| `sources-jar` | 源代码 JAR | +| `dev-jar` | 开发版 JAR(包含未混淆的类) | +| `javadoc-jar` | Javadoc JAR | +| `unknown` | 未知类型 | +| `signature` | 签名文件 | + +**标记 sources.jar 的示例:** + +```json +{ + "file_types": { + "sources-file": "sources-jar" + } +} +``` + +> 上例中 `"sources-file"` 是 multipart 字段名,需要在 `file_parts` 中同步出现。 + +--- + +## data JSON 结构 + +`data` 字段的值是一个 JSON 字符串,对应 `CreatableVersion` schema,它扩展了 `BaseVersion`。 + +### 顶层结构(`CreateVersionBody`) + +```json +{ + "data": "{ ... CreatableVersion JSON ... }" +} +``` + +### CreatableVersion 字段一览 + +#### 必填字段 + +| 字段 | 类型 | 说明 | 示例 | +|------|------|------|------| +| `project_id` | `string` | 目标项目的 ID(8 位 base62) | `"AABBCCDD"` | +| `file_parts` | `string[]` | 所有承载文件的 **multipart 字段名** 列表 | `["main-file", "sources-file"]` | +| `name` | `string` | 版本名称 | `"Version 1.0.0"` | +| `version_number` | `string` | 版本号,建议遵循语义化版本 | `"1.0.0"` | +| `game_versions` | `string[]` | 支持的 Minecraft 版本列表 | `["1.20.1", "1.20.4"]` | +| `version_type` | `string` | 发布渠道:`release`、`beta`、`alpha` | `"release"` | +| `loaders` | `string[]` | 支持的模组加载器列表 | `["fabric", "forge"]` | +| `featured` | `boolean` | 是否为精选版本 | `false` | +| `dependencies` | `object[]` | 依赖列表(可为空数组 `[]`) | 见下方 [依赖结构](#依赖结构) | + +#### 可选字段 + +| 字段 | 类型 | 说明 | 示例 | +|------|------|------|------| +| `primary_file` | `string` | 主文件的 **multipart 字段名**。不提供时推断第一个文件为主文件 | `"main-file"` | +| `changelog` | `string \| null` | 更新日志(Markdown/纯文本) | `"修复了若干 Bug"` | +| `status` | `string` | 版本状态:`listed`、`archived`、`draft`、`unlisted`、`scheduled`、`unknown`。默认为 `listed` | `"listed"` | +| `requested_status` | `string \| null` | 请求的状态(用于审核流程):`listed`、`archived`、`draft`、`unlisted` | `"listed"` | +| `environment` | `string` | 目标环境,见下方枚举 | `"client_and_server"` | +| `file_types` | `object` | **multipart 字段名** → 文件类型的映射 | `{"sources-file": "sources-jar"}` | + +#### `environment` 枚举值 + +所有值来自 Modrinth 前端源码 [environments.ts](https://github.com/modrinth/code/blob/main/packages/ui/src/components/project/settings/environment/environments.ts)。以下按前端 UI 的层级结构排列,只有 **Server-side only** 和 **Client and server** 是带子选项的大类,其余为单选: + +| 大类(前端 UI) | 子项标题 | API 值 | 说明 | +|----------------|---------|--------|------| +| *(无分类,单选)* | Unknown environment | `unknown` | 未指定或无法确定环境 | +| Client-side only | — | `client_only` | 所有功能在客户端运行,兼容原版服务端 | +| **Server-side only** | Works in singleplayer too | `server_only` | 所有功能在服务端运行,兼容原版客户端;也支持单人模式的内置服务端 | +| | Dedicated server only | `dedicated_server_only` | 所有功能在服务端运行,兼容原版客户端;仅在专用服务器上工作 | +| **Client and server** | Required on both | `client_and_server` | 客户端和服务端都必须安装 | +| | Optional on client | `server_only_client_optional` | 主要为服务端功能,客户端安装可增强体验 | +| | Optional on server | `client_only_server_optional` | 主要为客户端功能,服务端安装可增强体验 | +| | Optional on both, works best when installed on both sides | `client_or_server_prefers_both` | 双方都装体验最佳 | +| | Optional on both, works the same if installed on either side | `client_or_server` | 单独安装任一侧效果相同 | +| Singleplayer only | — | `singleplayer_only` | 仅在单人模式或未连接多人服务器时可用 | + +### 依赖结构 + +`dependencies` 数组中每个依赖对象至少需要 `dependency_type` 字段: + +| 字段 | 类型 | 必填 | 说明 | 示例 | +|------|------|------|------|------| +| `dependency_type` | `string` | ✅ | 依赖类型:`required`、`optional`、`incompatible`、`embedded` | `"required"` | +| `project_id` | `string \| null` | ❌ | 依赖的项目 ID | `"P7dR8mSH"` (Fabric API) | +| `version_id` | `string \| null` | ❌ | 依赖的具体版本 ID;为 `null` 时匹配最新版 | `"IIJJKKLL"` | +| `file_name` | `string \| null` | ❌ | 依赖文件名,主要用于整合包中的外部依赖 | `"fabric-api-0.92.0.jar"` | + +> **注意:** `project_id` 和 `version_id` 二者至少提供一个,否则依赖关系无法解析。 + +**Fabric API 依赖示例:** + +```json +{ + "dependency_type": "required", + "project_id": "P7dR8mSH" +} +``` + +--- + +## 完整请求示例 + +以下是一个使用 cURL 上传一个主 jar 和一个 sources.jar 的完整请求。 + +### 准备 + +假设: +- 项目 ID 为 `AABBCCDD` +- 主文件为 `my-mod-1.0.0.jar` +- 源代码文件为 `my-mod-1.0.0-sources.jar` + +### cURL 命令 + +```bash +curl -X POST "https://api.modrinth.com/v2/version" \ + -H "Authorization: YOUR_PAT_TOKEN" \ + -H "User-Agent: your_username/your_project/1.0.0" \ + -F "data={ + \"project_id\": \"AABBCCDD\", + \"file_parts\": [\"main-file\", \"sources-file\"], + \"primary_file\": \"main-file\", + \"name\": \"Version 1.0.0\", + \"version_number\": \"1.0.0\", + \"changelog\": \"## 更新内容\n\n- 新增了某某功能\n- 修复了某某 Bug\", + \"dependencies\": [ + { + \"dependency_type\": \"required\", + \"project_id\": \"P7dR8mSH\" + } + ], + \"game_versions\": [\"1.20.1\", \"1.20.4\"], + \"version_type\": \"release\", + \"loaders\": [\"fabric\"], + \"featured\": false, + \"status\": \"listed\", + \"environment\": \"client_and_server\", + \"file_types\": { + \"sources-file\": \"sources-jar\" + } + }" \ + -F "main-file=@./build/libs/my-mod-1.0.0.jar" \ + -F "sources-file=@./build/libs/my-mod-1.0.0-sources.jar" +``` + +### Node.js (fetch) 示例 + +```js +const fs = require("fs"); + +const TOKEN = "YOUR_PAT_TOKEN"; +const PROJECT_ID = "AABBCCDD"; + +const form = new FormData(); + +const metadata = { + project_id: PROJECT_ID, + file_parts: ["main-file", "sources-file"], + primary_file: "main-file", + name: "Version 1.0.0", + version_number: "1.0.0", + changelog: "## 更新内容\n\n- 新增了某某功能\n- 修复了某某 Bug", + dependencies: [ + { dependency_type: "required", project_id: "P7dR8mSH" } + ], + game_versions: ["1.20.1", "1.20.4"], + version_type: "release", + loaders: ["fabric"], + featured: false, + status: "listed", + environment: "client_and_server", + file_types: { + "sources-file": "sources-jar" + } +}; + +form.append("data", JSON.stringify(metadata)); +form.append("main-file", new Blob([fs.readFileSync("./build/libs/my-mod-1.0.0.jar")]), "my-mod-1.0.0.jar"); +form.append("sources-file", new Blob([fs.readFileSync("./build/libs/my-mod-1.0.0-sources.jar")]), "my-mod-1.0.0-sources.jar"); + +const response = await fetch("https://api.modrinth.com/v2/version", { + method: "POST", + headers: { + "Authorization": TOKEN, + "User-Agent": "your_username/your_project/1.0.0" + }, + body: form +}); + +const result = await response.json(); +console.log(result); +``` + +--- + +## 响应格式 + +成功创建后返回一个 [Version](#version-对象) 对象,HTTP 状态码 `200`。 + +### Version 对象 + +```json +{ + "id": "IIJJKKLL", + "project_id": "AABBCCDD", + "author_id": "EEFFGGHH", + "name": "Version 1.0.0", + "version_number": "1.0.0", + "changelog": "## 更新内容\n\n- 新增了某某功能\n- 修复了某某 Bug", + "date_published": "2025-01-01T00:00:00Z", + "downloads": 0, + "version_type": "release", + "status": "listed", + "requested_status": null, + "game_versions": ["1.20.1", "1.20.4"], + "loaders": ["fabric"], + "featured": false, + "files": [ + { + "hashes": { + "sha512": "93ecf5fe...", + "sha1": "c84dd4b3..." + }, + "url": "https://cdn.modrinth.com/data/AABBCCDD/versions/1.0.0/my-mod-1.0.0.jar", + "filename": "my-mod-1.0.0.jar", + "primary": true, + "size": 1097270, + "file_type": null + }, + { + "hashes": { + "sha512": "ab12cd34...", + "sha1": "ef56gh78..." + }, + "url": "https://cdn.modrinth.com/data/AABBCCDD/versions/1.0.0/my-mod-1.0.0-sources.jar", + "filename": "my-mod-1.0.0-sources.jar", + "primary": false, + "size": 543210, + "file_type": "sources-jar" + } + ] +} +``` + +响应中 `files` 数组的每个文件对象包含: + +| 字段 | 说明 | +|------|------| +| `hashes` | 文件哈希,包含 `sha512` 和 `sha1` | +| `url` | 文件直链 | +| `filename` | 文件名 | +| `primary` | 是否为主文件 | +| `size` | 文件大小(字节) | +| `file_type` | 文件类型(对于 sources.jar 为 `"sources-jar"`) | + +--- + +## 错误码 + +| 状态码 | 错误类型 | 说明 | +|--------|----------|------| +| `400` | `InvalidInputError` | 请求参数无效,详见响应中的 `description`。常见原因:缺少必填字段、`game_versions` 或 `loaders` 值不合法、文件格式不被接受 | +| `401` | `AuthError` | Token 无效、未提供 Token,或 Token 不包含 `VERSION_CREATE` 权限范围 | +| `404` | — | 目标项目 `project_id` 不存在 | + +--- + +## 注意事项与最佳实践 + +### 1. 必须至少上传一个文件 + +除非版本 `status` 设置为 `draft`,否则每个新版本必须附带至少一个文件。接受的格式:`.mrpack`、`.jar`、`.zip`、`.litemod`。 + +### 2. `file_parts` / `file_types` / `primary_file` 全部使用 multipart 字段名 + +这三处字符串和实际表单字段名必须**严格匹配**,且它们指向的都是 **multipart 字段名**(如 `"main-file"`),不是文件名(如 `"my-mod-1.0.0.jar"`)。否则上传的文件不会被正确关联。 + +### 3. sources.jar 的正确做法 + +- 给 sources 文件随意命名 multipart 字段名(如 `"sources-file"`),将其列入 `file_parts`。 +- 在 `file_types` 中映射该 **multipart 字段名** → `"sources-jar"`。 +- **不要**将 sources 文件设为主文件(`primary_file` 应指向主 jar 的 multipart 字段名)。 + +### 4. `dependencies` 可以是空数组 + +如果该模组没有依赖(极少数情况),直接传 `[]` 即可。但 `dependencies` 字段本身是必填的。 + +### 5. 文件大小限制 + +Modrinth 对上传文件有大小限制。如果文件过大,可能收到 `400` 错误。建议保持在合理范围内(一般单个文件不超过 100MB)。 + +### 6. 版本号建议 + +`version_number` 理想上应遵循[语义化版本规范](https://semver.org/lang/zh-CN/),便于 Modrinth 进行版本比较和用户理解。 + +### 7. 草稿版本 + +如果你想先上传版本而不立即公开,可将 `status` 设为 `draft`。草稿版本可以不附带文件。后续可通过 PATCH `/version/{id}` 修改状态为 `listed` 来发布。 + +### 8. 已有版本追加文件 + +如果版本已创建但需要追加文件,应使用 `POST /version/{id}/file` 端点(需要 `VERSION_WRITE` 权限范围)。 + +### 9. 环境与加载器的校验 + +`loaders` 和 `game_versions` 必须使用 Modrinth 认可的值。你可以通过以下端口查询可用值: +- `GET /tag/loader` — 可用加载器 +- `GET /tag/game_version` — 可用 Minecraft 版本 + +### 10. Rate Limit + +Modrinth API 有频率限制:每 IP 每分钟最多 **300** 个请求。响应头中包含 `X-Ratelimit-Limit`、`X-Ratelimit-Remaining`、`X-Ratelimit-Reset`。 + +--- + +## 参考 + +- [Modrinth API 文档](https://docs.modrinth.com) +- [Modrinth 创建 PAT](https://modrinth.com/settings/account) +- [OpenAPI 规范文件](openapi.yml) +- [Modrinth Labrinth 源码](https://github.com/modrinth/labrinth) +- [语义化版本规范](https://semver.org) diff --git a/docs/workflow.md b/docs/workflow.md new file mode 100644 index 0000000..29939dd --- /dev/null +++ b/docs/workflow.md @@ -0,0 +1,132 @@ +# 整体框架 + +## 数据文件 + +### `secrets.json` + +存放 API 密钥,位于项目根目录。 + +```json +{ + "curseforge": "", + "modrinth": "" +} +``` + +### `configs/{config_name}.json` + +存放单个 mod 项目的配置,位于 `configs/` 目录下。文件名即为配置名,用户通过 WebUI 下拉选择。 + +```jsonc +{ + "name": "模组名称", + "modrinth": { + "project_id": "AABBCCDD", + "version_name": "ModName v${version} for Minecraft ${mc_version_range}", + "version": "${version}-mc${mc_version}", + "loaders": ["fabric"], + "environment": "client_and_server", + "dependencies": [ + { "dependency_type": "required", "project_id": "P7dR8mSH" } + ] + }, + "curseforge": { + "project_id": 123456, + "version_name": "${filename_format}", + "environment": ["Client", "Server"], + "loaders": ["fabric"], + "relations": { + "projects": [] + } + }, + "project_dir": "/path/to/project", + "minecraft_properties_dir": "./properties", // 目录内为 ${mc_version}.properties 文件,文件名即对应游戏版本 + "project_properties_path": "./gradle.properties", // 项目版本配置文件路径 + "mod_version_field": "mod_version", // 版本号在 project_properties_path 文件中的键名 + "filename_format": "modname-${version}-mc${mc_version}.jar", + "source_filename_format": "modname-${version}-mc${mc_version}-sources.jar" +} +``` + +| 字段 | 类型 | 说明 | +|------|------|------| +| `name` | string | 配置文件显示名称(模组名称) | +| `modrinth` | object | Modrinth 配置 | +| `modrinth.project_id` | string | Modrinth 项目 ID(8 位 base62) | +| `modrinth.version_name` | string | 版本名称模板,`${mc_version_range}` 为 `"1.19"` 或 `"1.20-1.20.2"` | +| `modrinth.version` | string | 版本号模板 | +| `modrinth.loaders` | string[] | 加载器列表 | +| `modrinth.environment` | string | 运行环境,必填。可选值:`client_and_server`、`client_only`、`server_only` 等 | +| `modrinth.dependencies` | object[] | 依赖列表,无依赖填 `[]` | +| `curseforge` | object | CurseForge 配置 | +| `curseforge.project_id` | number | CurseForge 项目 ID | +| `curseforge.version_name` | string | 版本名称模板,`${filename_format}` 引用 config 中 `filename_format` 字段 | +| `curseforge.environment` | string[] | 运行环境 | +| `curseforge.loaders` | string[] | 加载器列表 | +| `curseforge.relations` | object | 关联项目,格式 `{ projects: [...] }` | +| `project_dir` | string | 项目目录绝对路径 | +| `minecraft_properties_dir` | string | 相对项目目录的 MC 版本配置目录,默认 `./properties`。目录内为 `${mc_version}.properties` 文件,文件名即为可用游戏版本 | +| `project_properties_path` | string | 相对项目目录的项目版本配置文件路径,默认 `./gradle.properties` | +| `mod_version_field` | string | 版本号在 `project_properties_path` 文件中的键名,默认 `"mod_version"` | +| `filename_format` | string | 构建产物文件名模板,用于反向解析 `${version}` 和 `${mc_version}` | +| `source_filename_format` | string | 源码 jar 文件名模板,用于正向生成文件名 | + +> **递归解析规则**:所有含 `${...}` 的模板字符串,正向 format 或反向解析后,若结果仍包含 `${...}` 占位符,则需继续解析,直至结果中不再有占位符为止。`${...}` 中的变量首先从 config 同级字段取值,例如 `${filename_format}` 引用 config 中的 `filename_format` 字段。 + +## 运行时流程 + +### 1. 启动 + +- 启动 Next.js 开发服务器 +- 后端读取 `configs/` 目录,列出所有可用配置文件 +- 后端读取 `secrets.json`,加载两个平台的 API 密钥到内存 + +### 2. 选择配置 + +- 前端 ConfigSelector 下拉框展示所有配置(按 `name` 显示) +- 用户选择一个配置后,前端请求后端解析项目信息 + +### 3. 解析项目 + +后端收到选中配置后: + +1. 读取 `{project_dir}/{project_properties_path}`,提取 `mod_version_field` 对应的值 → `version` +2. 扫描 `{project_dir}/{minecraft_properties_dir}/` 目录,获取所有 `${mc_version}.properties` 文件名 → `mc_version[]` +3. 遍历每个 `mc_version`: + - 用 `version` 和 `mc_version` 填入 `filename_format` 模板,生成构建产物文件名 + - 用 `version` 和 `mc_version` 填入 `source_filename_format` 模板,生成源码文件名 + - 检查 `{project_dir}/build/libs/` 下是否存在对应文件 +4. 将所有匹配结果返回前端,由 VersionTable 展示(MC 版本 | 构建产物 | 源码) + +### 4. 版本确认与发布 + +#### 第一页:版本选择 + +1. 用户点击 `[一键发布]`,弹出 PublishModal +2. 前端请求两个平台的已有版本列表: + - Modrinth:`GET /project/{id}/version` + - CurseForge:通过对应 API 获取 +3. 后端比对已有版本与待发布版本(按 `version` 模板生成的值匹配) +4. 前端左右两栏展示(待发布版本与已有版本合并后,按 `mod_version` 降序、`mc_version` 降序排列): + - 顶部绿色行 = 待发布版本,复选框默认勾选 + - 下方灰色行 = 已有版本,复选框禁用 + - 已存在的待发布版本显示为已有版本颜色,复选框禁用 +5. 用户调整勾选后点击"下一步" + +#### 第二页:发布设置 + +- 用户填写 changelog(Markdown,Code / Preview 切换) +- 选择 version_type(默认 `release`) +- 点击"确认发布";可点击"上一步"返回修改 + +### 5. 执行发布 + +1. 前端显示两个进度条(仅显示有勾选任务的平台),格式:`{平台图标} 平台名: [======== ] 0 / N` +2. 单个平台内按 MC 版本顺序逐个提交,**不可并行**;两个平台之间可以并行执行 +3. 每个版本的发布流程: + - 解析模板:用 `version` 和 `mc_version` 填入各平台 `version_name`、`version` 模板(含递归解析) + - Modrinth:构造 multipart/form-data 请求,含 `data` JSON + 构建产物文件 + 源码文件 + - CurseForge:构造 multipart/form-data 请求,含 `metadata` JSON + 文件 + - 更新对应进度条 +4. **任一请求失败则取消当前平台的后续所有任务**,不影响另一个平台 +5. 全部完成或失败后,弹出结果 Modal 显示最终状态(成功数 / 失败数)