├── src └── main │ ├── resources │ └── version.txt │ └── kotlin │ └── com │ └── github │ └── p03w │ └── modifold │ ├── core │ ├── github_schema │ │ ├── AuthToken.kt │ │ └── DeviceCode.kt │ ├── CollectModrinthProjects.kt │ ├── VerifyArgs.kt │ ├── CollectCurseforgeProjects.kt │ ├── CreateModrinthProjects.kt │ ├── InformationToSave.kt │ ├── MatchExistingProjects.kt │ ├── ModrinthLogin.kt │ ├── SimilarProjectFinder.kt │ └── TransferProjectFiles.kt │ ├── Global.kt │ ├── NoModrinthFlow.kt │ └── Main.kt ├── .gitignore ├── gradle.properties ├── API-Common ├── build.gradle.kts └── src │ └── main │ └── kotlin │ └── com │ └── github │ └── p03w │ └── modifold │ └── api_core │ ├── Ratelimit.kt │ └── APIInterface.kt ├── .github └── FUNDING.yml ├── images └── curseforge_id.png ├── API-Curseforge ├── build.gradle.kts └── src │ └── main │ └── kotlin │ └── com.github.p03w.modifold.curseforge_api │ └── CurseforgeAPI.kt ├── Schema-Curseforge └── src │ └── main │ └── kotlin │ └── com │ └── github │ └── p03w │ └── modifold │ └── curseforge_schema │ ├── CurseforgeDescription.kt │ ├── CurseforgeCategory.kt │ ├── CurseforgeAuthor.kt │ ├── DataWrappers.kt │ ├── CurseforgeLinks.kt │ ├── CurseforgeAsset.kt │ ├── CurseforgeFile.kt │ └── CurseforgeProject.kt ├── Schema-Modrinth └── src │ └── main │ └── kotlin │ └── com │ └── github │ └── p03w │ └── modifold │ └── modrinth_schema │ ├── ModrinthShortLicense.kt │ ├── ModrinthLicense.kt │ ├── ModrinthDonationURL.kt │ ├── ModrinthCategory.kt │ ├── ModrinthLoader.kt │ ├── ModrinthUser.kt │ ├── ModrinthVersionUpload.kt │ └── ModrinthProject.kt ├── API-Conversion ├── build.gradle.kts └── src │ └── main │ └── kotlin │ └── com │ └── github │ └── p03w │ └── modifold │ └── conversion │ └── MapCategories.kt ├── settings.gradle.kts ├── CLITools └── src │ └── main │ └── kotlin │ └── com │ └── github │ └── p03w │ └── modifold │ └── cli │ ├── BadCancellationException.kt │ ├── Output.kt │ ├── Countdown.kt │ ├── UI.kt │ ├── Util.kt │ ├── Spinner.kt │ └── ModifoldArgs.kt ├── API-Modrinth ├── build.gradle.kts └── src │ └── main │ └── kotlin │ └── com │ └── github │ └── p03w │ └── modifold │ └── modrinth_api │ ├── ModrinthProjectCreate.kt │ └── ModrinthAPI.kt ├── README.md ├── gradlew.bat ├── gradlew └── LICENSE /src/main/resources/version.txt: -------------------------------------------------------------------------------- 1 | 2.2.4 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .gradle 3 | gradle 4 | build 5 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.parallel=true 2 | kotlin.code.style=official 3 | -------------------------------------------------------------------------------- /API-Common/build.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencies { 2 | implementation(project(":CLITools")) 3 | } -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: ["https://www.paypal.com/donate/?hosted_button_id=73KL89UWZDDQQ"] 2 | -------------------------------------------------------------------------------- /images/curseforge_id.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SilverAndro/Modifold/HEAD/images/curseforge_id.png -------------------------------------------------------------------------------- /src/main/kotlin/com/github/p03w/modifold/core/github_schema/AuthToken.kt: -------------------------------------------------------------------------------- 1 | package com.github.p03w.modifold.core.github_schema 2 | 3 | data class AuthToken(val access_token: String?) 4 | -------------------------------------------------------------------------------- /API-Curseforge/build.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencies { 2 | implementation(project(":API-Common")) 3 | implementation(project(":Schema-Curseforge")) 4 | implementation(project(":CLITools")) 5 | } -------------------------------------------------------------------------------- /Schema-Curseforge/src/main/kotlin/com/github/p03w/modifold/curseforge_schema/CurseforgeDescription.kt: -------------------------------------------------------------------------------- 1 | package com.github.p03w.modifold.curseforge_schema 2 | 3 | data class CurseforgeDescription(val data: String) 4 | -------------------------------------------------------------------------------- /Schema-Curseforge/src/main/kotlin/com/github/p03w/modifold/curseforge_schema/CurseforgeCategory.kt: -------------------------------------------------------------------------------- 1 | package com.github.p03w.modifold.curseforge_schema 2 | 3 | data class CurseforgeCategory(val id: Int, val name: String) 4 | -------------------------------------------------------------------------------- /Schema-Modrinth/src/main/kotlin/com/github/p03w/modifold/modrinth_schema/ModrinthShortLicense.kt: -------------------------------------------------------------------------------- 1 | package com.github.p03w.modifold.modrinth_schema 2 | 3 | data class ModrinthShortLicense(val short: String, val name: String) 4 | -------------------------------------------------------------------------------- /Schema-Modrinth/src/main/kotlin/com/github/p03w/modifold/modrinth_schema/ModrinthLicense.kt: -------------------------------------------------------------------------------- 1 | package com.github.p03w.modifold.modrinth_schema 2 | 3 | data class ModrinthLicense(val id: String, val name: String, val url: String) 4 | -------------------------------------------------------------------------------- /API-Conversion/build.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencies { 2 | implementation(project(":CLITools")) 3 | implementation(project(":API-Common")) 4 | implementation(project(":Schema-Curseforge")) 5 | implementation(project(":Schema-Modrinth")) 6 | } -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "Modifold" 2 | include("CLITools") 3 | include("API-Common") 4 | include("API-Curseforge") 5 | include("API-Modrinth") 6 | include("API-Conversion") 7 | include("Schema-Curseforge") 8 | include("Schema-Modrinth") 9 | -------------------------------------------------------------------------------- /Schema-Curseforge/src/main/kotlin/com/github/p03w/modifold/curseforge_schema/CurseforgeAuthor.kt: -------------------------------------------------------------------------------- 1 | package com.github.p03w.modifold.curseforge_schema 2 | 3 | data class CurseforgeAuthor( 4 | val id: Int, 5 | val name: String, 6 | val url: String, 7 | ) 8 | -------------------------------------------------------------------------------- /Schema-Curseforge/src/main/kotlin/com/github/p03w/modifold/curseforge_schema/DataWrappers.kt: -------------------------------------------------------------------------------- 1 | package com.github.p03w.modifold.curseforge_schema 2 | 3 | data class ProjectWrapper(val data: CurseforgeProject) 4 | data class FilesWrapper(val data: List) 5 | -------------------------------------------------------------------------------- /Schema-Modrinth/src/main/kotlin/com/github/p03w/modifold/modrinth_schema/ModrinthDonationURL.kt: -------------------------------------------------------------------------------- 1 | package com.github.p03w.modifold.modrinth_schema 2 | 3 | data class ModrinthDonationURL( 4 | val id: String, 5 | val platform: String, 6 | val url: String 7 | ) 8 | -------------------------------------------------------------------------------- /Schema-Modrinth/src/main/kotlin/com/github/p03w/modifold/modrinth_schema/ModrinthCategory.kt: -------------------------------------------------------------------------------- 1 | package com.github.p03w.modifold.modrinth_schema 2 | 3 | data class ModrinthCategory( 4 | val icon: String, 5 | val name: String, 6 | val project_type: String 7 | ) 8 | -------------------------------------------------------------------------------- /CLITools/src/main/kotlin/com/github/p03w/modifold/cli/BadCancellationException.kt: -------------------------------------------------------------------------------- 1 | package com.github.p03w.modifold.cli 2 | 3 | import kotlinx.coroutines.CancellationException 4 | 5 | // Used to cancel a job in a "bad" way 6 | class BadCancellationException : CancellationException() 7 | -------------------------------------------------------------------------------- /Schema-Modrinth/src/main/kotlin/com/github/p03w/modifold/modrinth_schema/ModrinthLoader.kt: -------------------------------------------------------------------------------- 1 | package com.github.p03w.modifold.modrinth_schema 2 | 3 | data class ModrinthLoader( 4 | val icon: String, 5 | val name: String, 6 | val supported_project_types: List 7 | ) 8 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/p03w/modifold/core/github_schema/DeviceCode.kt: -------------------------------------------------------------------------------- 1 | package com.github.p03w.modifold.core.github_schema 2 | 3 | data class DeviceCode( 4 | val device_code: String, 5 | val user_code: String, 6 | val verification_uri: String, 7 | val interval: Int 8 | ) 9 | -------------------------------------------------------------------------------- /API-Modrinth/build.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencies { 2 | implementation(project(":API-Common")) 3 | implementation(project(":API-Conversion")) 4 | implementation(project(":Schema-Curseforge")) 5 | implementation(project(":Schema-Modrinth")) 6 | implementation(project(":CLITools")) 7 | } 8 | -------------------------------------------------------------------------------- /Schema-Curseforge/src/main/kotlin/com/github/p03w/modifold/curseforge_schema/CurseforgeLinks.kt: -------------------------------------------------------------------------------- 1 | package com.github.p03w.modifold.curseforge_schema 2 | 3 | data class CurseforgeLinks( 4 | val websiteUrl: String?, 5 | val wikiUrl: String?, 6 | val issuesUrl: String?, 7 | val sourceUrl: String? 8 | ) 9 | -------------------------------------------------------------------------------- /Schema-Modrinth/src/main/kotlin/com/github/p03w/modifold/modrinth_schema/ModrinthUser.kt: -------------------------------------------------------------------------------- 1 | package com.github.p03w.modifold.modrinth_schema 2 | 3 | data class ModrinthUser( 4 | val id: String, 5 | val username: String, 6 | val name: String, 7 | val bio: String, 8 | val role: String 9 | ) 10 | -------------------------------------------------------------------------------- /Schema-Curseforge/src/main/kotlin/com/github/p03w/modifold/curseforge_schema/CurseforgeAsset.kt: -------------------------------------------------------------------------------- 1 | package com.github.p03w.modifold.curseforge_schema 2 | 3 | data class CurseforgeAsset( 4 | val id: Int, 5 | val title: String, 6 | val description: String, 7 | val url: String, 8 | ) { 9 | fun getExt() = url.split(".").last() 10 | } 11 | -------------------------------------------------------------------------------- /Schema-Curseforge/src/main/kotlin/com/github/p03w/modifold/curseforge_schema/CurseforgeFile.kt: -------------------------------------------------------------------------------- 1 | package com.github.p03w.modifold.curseforge_schema 2 | 3 | data class CurseforgeFile( 4 | val id: Int, 5 | val displayName: String, 6 | val fileName: String, 7 | val releaseType: Int, 8 | val downloadUrl: String, 9 | val gameVersions: List, 10 | val fileLength: Long, 11 | val fileDate: String 12 | ) 13 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/p03w/modifold/Global.kt: -------------------------------------------------------------------------------- 1 | package com.github.p03w.modifold 2 | 3 | object Global { 4 | const val helpMenuPrologue = 5 | "Modifold is a Kotlin CLI program for moving curseforge mods to modrinth almost completely autonomously thanks to " + 6 | "the incredible modrinth API work by the modrinth team, " + 7 | "as well as the curseforge API proxy developer, much thanks to both <3" 8 | } 9 | -------------------------------------------------------------------------------- /CLITools/src/main/kotlin/com/github/p03w/modifold/cli/Output.kt: -------------------------------------------------------------------------------- 1 | package com.github.p03w.modifold.cli 2 | 3 | import org.fusesource.jansi.Ansi 4 | import org.fusesource.jansi.Ansi.ansi 5 | 6 | fun String.highlight(): Ansi = ansi().bold().fgCyan().a(this).reset() 7 | fun String.debug(): Ansi = ansi().fgGreen().a(this).reset() 8 | fun String.warn(): Ansi = ansi().fgBrightYellow().a(this).reset() 9 | fun String.error(): Ansi = ansi().fgBrightRed().a(this).reset() 10 | fun String.bold(): Ansi = ansi().bold().a(this).reset() 11 | -------------------------------------------------------------------------------- /Schema-Modrinth/src/main/kotlin/com/github/p03w/modifold/modrinth_schema/ModrinthVersionUpload.kt: -------------------------------------------------------------------------------- 1 | package com.github.p03w.modifold.modrinth_schema 2 | 3 | data class ModrinthVersionUpload( 4 | val mod_id: String, 5 | val file_parts: List, 6 | val version_number: String, 7 | val version_title: String, 8 | val version_body: String, 9 | val game_versions: List, 10 | val release_channel: String, 11 | val loaders: List, 12 | val featured: Boolean = false, 13 | val dependencies: List = emptyList() 14 | ) 15 | -------------------------------------------------------------------------------- /CLITools/src/main/kotlin/com/github/p03w/modifold/cli/Countdown.kt: -------------------------------------------------------------------------------- 1 | package com.github.p03w.modifold.cli 2 | 3 | import java.time.Instant 4 | import kotlin.reflect.KProperty 5 | import kotlin.time.Duration 6 | 7 | class Countdown(private val delay: Duration) { 8 | private var clock = -1L 9 | 10 | operator fun getValue(thisRef: Any, kParameter: KProperty<*>): Boolean { 11 | if (Instant.now().toEpochMilli() >= clock) { 12 | clock = Instant.now().toEpochMilli() + delay.inWholeMilliseconds 13 | return true 14 | } 15 | return false 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Schema-Curseforge/src/main/kotlin/com/github/p03w/modifold/curseforge_schema/CurseforgeProject.kt: -------------------------------------------------------------------------------- 1 | package com.github.p03w.modifold.curseforge_schema 2 | 3 | data class CurseforgeProject( 4 | val id: Int, 5 | val name: String, 6 | val slug: String, 7 | val links: CurseforgeLinks, 8 | val authors: List, 9 | val summary: String, 10 | val categories: List, 11 | val logo: CurseforgeAsset, 12 | val screenshots: List, 13 | val latestFiles: List, 14 | val allowModDistribution: Boolean 15 | ) { 16 | fun display() = "$name ($id)" 17 | } 18 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/p03w/modifold/core/CollectModrinthProjects.kt: -------------------------------------------------------------------------------- 1 | package com.github.p03w.modifold.core 2 | 3 | import com.github.p03w.modifold.cli.log 4 | import com.github.p03w.modifold.cli.withSpinner 5 | import com.github.p03w.modifold.modrinth_schema.ModrinthProject 6 | import com.github.p03w.modifold.modrinth_schema.ModrinthUser 7 | import com.github.p03w.modifold.modrinth_api.ModrinthAPI 8 | 9 | fun collectModrinthProjects(modrinthUser: ModrinthUser): MutableList { 10 | log("Collecting existing modrinth projects") 11 | val modrinthProjects = mutableListOf() 12 | ModrinthAPI.getUserProjects(modrinthUser).forEach { project -> 13 | val projectData = withSpinner("Collecting project info for project ${project.display()}") { 14 | ModrinthAPI.getProjectInfo(project.id) 15 | } 16 | modrinthProjects.add(projectData) 17 | } 18 | 19 | return modrinthProjects 20 | } 21 | -------------------------------------------------------------------------------- /Schema-Modrinth/src/main/kotlin/com/github/p03w/modifold/modrinth_schema/ModrinthProject.kt: -------------------------------------------------------------------------------- 1 | package com.github.p03w.modifold.modrinth_schema 2 | 3 | data class ModrinthProject( 4 | val id: String, 5 | val slug: String, 6 | 7 | val title: String, 8 | val description: String, 9 | val categories: List, 10 | 11 | val client_side: String, 12 | val server_side: String, 13 | 14 | val body: String, 15 | 16 | val issues_url: String?, 17 | val source_url: String?, 18 | val wiki_url: String?, 19 | val discord_url: String?, 20 | val donation_urls: List?, 21 | 22 | val project_type: String, 23 | val downloads: Int, 24 | 25 | val icon_url: String?, 26 | 27 | val team: String, 28 | 29 | @Deprecated("Only for old projects") 30 | val body_url: String?, 31 | 32 | val license: ModrinthLicense, 33 | val versions: List, 34 | ) { 35 | fun display() = "$title ($id)" 36 | } 37 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/p03w/modifold/NoModrinthFlow.kt: -------------------------------------------------------------------------------- 1 | package com.github.p03w.modifold 2 | 3 | import com.github.p03w.modifold.cli.ModifoldArgs 4 | import com.github.p03w.modifold.cli.ModifoldArgsContainer.* 5 | import com.github.p03w.modifold.cli.log 6 | import com.github.p03w.modifold.cli.userUnderstandsUsageAlternative 7 | import com.github.p03w.modifold.core.* 8 | import kotlin.system.exitProcess 9 | 10 | fun noModrinthFlow() { 11 | if (!ModifoldArgs.args.donts.contains(DONT.VERIFY_END_USER)) { 12 | if (!userUnderstandsUsageAlternative()) { 13 | println("Quiting") 14 | exitProcess(1) 15 | } 16 | } 17 | 18 | val curseforgeProjects = collectCurseforgeProjects(ModifoldArgs.args.curseforgeIDs) 19 | 20 | if (curseforgeProjects.isEmpty()) { 21 | error("No curseforge projects") 22 | } 23 | 24 | log("Done getting projects") 25 | 26 | val toSave = getInfoToSaveLocally() 27 | val toTransfer = getFilesToTransfer() 28 | 29 | log("Beginning file \"transfer\"") 30 | backupProjectFiles(curseforgeProjects, toSave, toTransfer) 31 | 32 | log("Done!") 33 | } -------------------------------------------------------------------------------- /API-Common/src/main/kotlin/com/github/p03w/modifold/api_core/Ratelimit.kt: -------------------------------------------------------------------------------- 1 | package com.github.p03w.modifold.api_core 2 | 3 | import java.time.Instant 4 | import kotlin.time.Duration 5 | import kotlin.time.Duration.Companion.milliseconds 6 | import kotlin.time.Duration.Companion.seconds 7 | 8 | class Ratelimit( 9 | private val delay: Duration, 10 | private val ignoreRequestCounter: Boolean 11 | ) { 12 | private var clock = -1L 13 | var resetClock = Instant.now() 14 | var remainingRequests = 10 // Better get a ratelimit in 10 requests lol 15 | var secondsUntilReset = 1000.seconds 16 | 17 | fun makeRequest() { 18 | remainingRequests-- 19 | } 20 | 21 | val canSend: Boolean get() { 22 | if (resetClock.toEpochMilli().milliseconds + secondsUntilReset <= Instant.now().toEpochMilli().milliseconds) { 23 | remainingRequests = 10 24 | } 25 | if (Instant.now().toEpochMilli() >= clock) { 26 | clock = Instant.now().toEpochMilli() + delay.inWholeMilliseconds 27 | return remainingRequests > 0 || ignoreRequestCounter 28 | } 29 | return false 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /CLITools/src/main/kotlin/com/github/p03w/modifold/cli/UI.kt: -------------------------------------------------------------------------------- 1 | package com.github.p03w.modifold.cli 2 | 3 | import com.github.kinquirer.KInquirer 4 | import com.github.kinquirer.components.promptConfirm 5 | 6 | fun userUnderstandsUsage(): Boolean { 7 | println( 8 | """ 9 | ONLY USE THIS TOOL ON PROJECTS YOU OWN 10 | I built this for honest users who want to move off curseforge, I don't want to have to deal with people blaming me because someone stole their mods. 11 | Modrinth moderation also checks for ownership anyways, so you're unlikely to get anywhere 12 | """.trimIndent() 13 | ) 14 | return KInquirer.promptConfirm("I understand this tool should only be used on my own projects") 15 | } 16 | 17 | fun userUnderstandsUsageAlternative(): Boolean { 18 | println( 19 | """ 20 | ONLY USE THIS TOOL ON PROJECTS YOU OWN 21 | I built this for honest users who want to move off curseforge, and not for anyone else, as that has significant legal complications. 22 | """.trimIndent() 23 | ) 24 | return KInquirer.promptConfirm("I understand this tool should only be used on my own projects") 25 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/p03w/modifold/core/VerifyArgs.kt: -------------------------------------------------------------------------------- 1 | package com.github.p03w.modifold.core 2 | 3 | import com.github.p03w.modifold.cli.ModifoldArgs 4 | import com.github.p03w.modifold.cli.debug 5 | import com.github.p03w.modifold.cli.error 6 | import com.github.p03w.modifold.modrinth_api.ModrinthAPI 7 | 8 | fun verifyDefaultArgs() { 9 | debug("Getting supported modrinth licenses") 10 | val possibleLicenses = ModrinthAPI.getPossibleLicenses() 11 | if (!possibleLicenses.contains(ModifoldArgs.args.defaultLicense)) { 12 | error(buildString { 13 | appendLine("Unsupported default license \"${ModifoldArgs.args.defaultLicense}\"") 14 | appendLine("If you want to use a license not on the following list, you must set it yourself per project") 15 | appendLine("Available licenses:") 16 | possibleLicenses.forEach { 17 | appendLine("- $it") 18 | } 19 | }) 20 | } 21 | 22 | debug("Getting supported modrinth loaders") 23 | val possibleLoaders = ModrinthAPI.getPossibleLoaders().map { it.name } 24 | ModifoldArgs.args.defaultLoaders.forEach { loader -> 25 | if (!possibleLoaders.contains(loader)) { 26 | error(buildString { 27 | appendLine("Unsupported default loader \"$loader\"") 28 | appendLine("Available loaders:") 29 | possibleLoaders.forEach { 30 | appendLine("- $it") 31 | } 32 | }) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/p03w/modifold/core/CollectCurseforgeProjects.kt: -------------------------------------------------------------------------------- 1 | package com.github.p03w.modifold.core 2 | 3 | import com.github.p03w.modifold.cli.ModifoldArgs 4 | import com.github.p03w.modifold.cli.log 5 | import com.github.p03w.modifold.cli.warn 6 | import com.github.p03w.modifold.cli.withSpinner 7 | import com.github.p03w.modifold.curseforge_api.CurseforgeAPI 8 | import com.github.p03w.modifold.curseforge_schema.CurseforgeProject 9 | 10 | fun collectCurseforgeProjects(ids: List): MutableList { 11 | log("Collecting curseforge projects") 12 | val curseforgeProjects = mutableListOf() 13 | ids.forEach { id -> 14 | val projectData = withSpinner("Collecting curseforge project info for project $id") { 15 | CurseforgeAPI.getProjectData(id) 16 | } ?: run { 17 | warn("Could not get curseforge project info for project $id") 18 | return@forEach 19 | } 20 | 21 | if (projectData.categories.any { it.id == 4471 /* Hardcoded category ID for modpacks */ }) { 22 | warn("Skipping project id $id (${projectData.name}) because its a modpack") 23 | } else if (!projectData.authors.any { it.name.lowercase() == ModifoldArgs.args.curseforgeUsername.lowercase() }) { 24 | warn("Skipping project id $id (${projectData.name}) because none of its authors are ${ModifoldArgs.args.curseforgeUsername}") 25 | } else { 26 | curseforgeProjects.add(projectData) 27 | } 28 | } 29 | return curseforgeProjects 30 | } 31 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/p03w/modifold/core/CreateModrinthProjects.kt: -------------------------------------------------------------------------------- 1 | package com.github.p03w.modifold.core 2 | 3 | import com.github.p03w.modifold.cli.ModifoldArgs 4 | import com.github.p03w.modifold.cli.ModifoldArgsContainer 5 | import com.github.p03w.modifold.cli.log 6 | import com.github.p03w.modifold.cli.withSpinner 7 | import com.github.p03w.modifold.curseforge_api.CurseforgeAPI 8 | import com.github.p03w.modifold.curseforge_schema.CurseforgeProject 9 | import com.github.p03w.modifold.modrinth_api.ModrinthAPI 10 | import com.github.p03w.modifold.modrinth_api.ModrinthProjectCreate 11 | import com.github.p03w.modifold.modrinth_schema.ModrinthProject 12 | 13 | fun createModrinthProjects(curseforgeProjects: List): MutableMap { 14 | log("Creating modrinth projects from curseforge projects") 15 | val out = mutableMapOf() 16 | 17 | curseforgeProjects.forEach { project -> 18 | withSpinner("Making modrinth project for ${project.display()}") { 19 | val description = if (ModifoldArgs.args.donts.contains(ModifoldArgsContainer.DONT.MIGRATE_DESCRIPTION)) { 20 | null 21 | } else { 22 | CurseforgeAPI.getProjectDescription(project.id) 23 | } 24 | 25 | val mod = ModrinthAPI.makeProject( 26 | ModrinthProjectCreate.of( 27 | project, 28 | description 29 | ), project 30 | ) 31 | out[project] = mod 32 | } 33 | } 34 | 35 | return out 36 | } 37 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/p03w/modifold/core/InformationToSave.kt: -------------------------------------------------------------------------------- 1 | package com.github.p03w.modifold.core 2 | 3 | import com.github.kinquirer.KInquirer 4 | import com.github.kinquirer.components.CheckboxViewOptions 5 | import com.github.kinquirer.components.ListViewOptions 6 | import com.github.kinquirer.components.promptCheckbox 7 | import com.github.kinquirer.components.promptList 8 | import java.util.* 9 | 10 | fun getFilesToTransfer(): FileSet { 11 | val choice = KInquirer.promptList( 12 | "What set of files should be transferred?", 13 | FileSet.values().map { it.userFacing }, 14 | viewOptions = ListViewOptions(questionMarkPrefix = "") 15 | ) 16 | 17 | return FileSet.values().first { it.userFacing == choice } 18 | } 19 | 20 | enum class FileSet(val userFacing: String) { 21 | ALL("All files"), 22 | LATEST("Only the \"latest files\" (according to curseforge)") 23 | } 24 | 25 | fun getInfoToSaveLocally(): EnumSet { 26 | val set = EnumSet.noneOf(InformationToSave::class.java) 27 | 28 | val choices = KInquirer.promptCheckbox( 29 | "What data should be written to disk locally while transferring?", 30 | InformationToSave.values().map { it.userFacing }, 31 | hint = "Space to select option, enter to confirm", 32 | viewOptions = CheckboxViewOptions(questionMarkPrefix = "", unchecked = "[ ] ", checked = "[X] ") 33 | ) 34 | 35 | choices.forEach {choice -> 36 | set.add(InformationToSave.values().first { it.userFacing == choice }) 37 | } 38 | 39 | return set 40 | } 41 | 42 | enum class InformationToSave(val userFacing: String) { 43 | IMAGES("Curseforge images"), 44 | VERSIONS("Version files") 45 | } 46 | -------------------------------------------------------------------------------- /CLITools/src/main/kotlin/com/github/p03w/modifold/cli/Util.kt: -------------------------------------------------------------------------------- 1 | package com.github.p03w.modifold.cli 2 | 3 | import kotlinx.coroutines.cancel 4 | import org.fusesource.jansi.Ansi 5 | 6 | inline fun withSpinner(message: String, action: (Spinner) -> T): T { 7 | val spinner = Spinner(message) 8 | val result: T 9 | try { 10 | result = action(spinner) 11 | } catch (err: Exception) { 12 | spinner.fail() 13 | throw err 14 | } 15 | if (Spinner.spinnerActive) { 16 | spinner.done() 17 | } 18 | return result 19 | } 20 | 21 | private fun clearLine() { 22 | synchronized(Spinner.Companion) { 23 | print("\r${Ansi.ansi().eraseLine()}\r") 24 | System.out.flush() 25 | } 26 | } 27 | 28 | fun debug(text: String) { 29 | if (ModifoldArgs.args.debug) { 30 | if (Spinner.spinnerActive) { 31 | clearLine() 32 | } 33 | synchronized(Spinner.Companion) { 34 | println("DEBUG: ".debug().a(text)) 35 | } 36 | } 37 | } 38 | 39 | fun log(text: String) { 40 | if (Spinner.spinnerActive) { 41 | clearLine() 42 | } 43 | synchronized(Spinner.Companion) { 44 | println(text) 45 | } 46 | } 47 | 48 | fun await(text: String) { 49 | print(text) 50 | readln() 51 | } 52 | 53 | fun warn(text: String) { 54 | if (Spinner.spinnerActive) { 55 | clearLine() 56 | } 57 | synchronized(Spinner.Companion) { 58 | println("WARN: $text".warn()) 59 | } 60 | } 61 | 62 | fun error(text: String, err: Throwable? = null): Nothing { 63 | if (Spinner.spinnerActive) Spinner.scope.cancel(BadCancellationException()) 64 | synchronized(Spinner.Companion) { 65 | println() 66 | println("ERROR: $text".error().toString()) 67 | } 68 | if (err != null) { 69 | throw IllegalStateException("ERROR: $text").initCause(err) 70 | } else { 71 | throw IllegalStateException("ERROR: $text") 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /API-Curseforge/src/main/kotlin/com.github.p03w.modifold.curseforge_api/CurseforgeAPI.kt: -------------------------------------------------------------------------------- 1 | package com.github.p03w.modifold.curseforge_api 2 | 3 | import com.github.p03w.modifold.api_core.APIInterface 4 | import com.github.p03w.modifold.api_core.Ratelimit 5 | import com.github.p03w.modifold.cli.ModifoldArgs 6 | import com.github.p03w.modifold.curseforge_schema.* 7 | import java.io.InputStream 8 | import java.net.URL 9 | import kotlin.time.Duration.Companion.milliseconds 10 | 11 | object CurseforgeAPI : APIInterface() { 12 | override val ratelimit = Ratelimit(ModifoldArgs.args.curseforgeSpeed.milliseconds, true) 13 | const val root = "https://api.curse.tools/v1/cf" 14 | 15 | private val cache = mutableMapOf() 16 | 17 | fun getProjectData(id: Int): CurseforgeProject? { 18 | return cache.computeIfAbsent(id) { 19 | try { 20 | getWithoutAuth("$root/mods/$id").data 21 | } catch (ignored: Exception) { 22 | ignored.printStackTrace() 23 | null 24 | } 25 | } 26 | } 27 | 28 | fun getProjectFiles(id: Int, allFiles: Boolean, onFailed: () -> Unit = {}): List { 29 | return try { 30 | if (allFiles) { 31 | getWithoutAuth("$root/mods/$id/files").data 32 | } else { 33 | getProjectData(id)!!.latestFiles 34 | } 35 | } catch (err: Exception) { 36 | err.printStackTrace() 37 | onFailed() 38 | emptyList() 39 | } 40 | } 41 | 42 | fun getProjectDescription(id: Int): String? { 43 | return try { 44 | getWithoutAuth("$root/mods/$id/description").data 45 | } catch (err: Exception) { 46 | err.printStackTrace() 47 | null 48 | } 49 | } 50 | 51 | fun getFileStream(file: CurseforgeFile): InputStream { 52 | waitUntilCanSend() 53 | return URL(file.downloadUrl).openStream() 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/p03w/modifold/core/MatchExistingProjects.kt: -------------------------------------------------------------------------------- 1 | package com.github.p03w.modifold.core 2 | 3 | import com.github.kinquirer.KInquirer 4 | import com.github.kinquirer.components.ListViewOptions 5 | import com.github.kinquirer.components.promptList 6 | import com.github.p03w.modifold.cli.* 7 | import com.github.p03w.modifold.cli.ModifoldArgsContainer.DONT 8 | import com.github.p03w.modifold.curseforge_schema.CurseforgeProject 9 | import com.github.p03w.modifold.modrinth_schema.ModrinthUser 10 | 11 | fun matchExistingProjects(modrinthUser: ModrinthUser, curseforgeProjects: MutableList) { 12 | if (!ModifoldArgs.args.donts.contains(DONT.VERIFY_EXISTING)) { 13 | val modrinthProjects = collectModrinthProjects(modrinthUser) 14 | if (modrinthProjects.isNotEmpty()) { 15 | val finder = SimilarProjectFinder(modrinthProjects, curseforgeProjects) 16 | 17 | log("\nYou will now be given the closest match on modrinth for each curseforge project.\n") 18 | 19 | val existing = mutableListOf() 20 | curseforgeProjects.forEach { 21 | val similar = finder.findSimilar(it) 22 | if (similar != null) { 23 | log("Possible existing modrinth project found for curseforge project ${it.display()}:") 24 | log("${similar.display()}: ${similar.description}") 25 | val option = KInquirer.promptList( 26 | "Is this a match?", 27 | listOf("Yes", "No", "Ignore"), 28 | "Ignore makes modifold not consider this modrinth project again for matches", 29 | viewOptions = ListViewOptions(questionMarkPrefix = "") 30 | ) 31 | when (option) { 32 | "Yes" -> { 33 | existing.add(it); finder.ignoredIDs.add(similar.id) 34 | } 35 | "No" -> {} 36 | "Ignore" -> finder.ignoredIDs.add(similar.id) 37 | } 38 | println() 39 | } else { 40 | debug("Found no similar mods for ${it.name}") 41 | } 42 | } 43 | curseforgeProjects.removeAll { existing.contains(it) } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /CLITools/src/main/kotlin/com/github/p03w/modifold/cli/Spinner.kt: -------------------------------------------------------------------------------- 1 | package com.github.p03w.modifold.cli 2 | 3 | import kotlinx.coroutines.* 4 | import java.time.Instant 5 | import kotlin.time.Duration.Companion.milliseconds 6 | 7 | class Spinner(private val message: String) { 8 | private val delay by Countdown(70.milliseconds) 9 | private var spinner = "|" 10 | private val job: Job 11 | 12 | private val startInstant = Instant.now() 13 | 14 | init { 15 | spinnerActive = true 16 | job = scope.launch { 17 | try { 18 | while (true) { 19 | spin() 20 | delay(7) 21 | } 22 | } catch (cancel: CancellationException) { 23 | if (cancel !is BadCancellationException) { 24 | finish() 25 | } 26 | spinnerActive = false 27 | } 28 | } 29 | } 30 | 31 | fun done() { 32 | runBlocking { 33 | job.cancelAndJoin() 34 | } 35 | } 36 | 37 | fun fail() { 38 | runBlocking { 39 | job.cancel(BadCancellationException()) 40 | job.join() 41 | } 42 | val now = Instant.now() 43 | val change = now.toEpochMilli() - startInstant.toEpochMilli() 44 | synchronized(Companion) { 45 | println("\r$message [${"FAIL".error()}] (${change}ms)") 46 | } 47 | } 48 | 49 | private fun spin() { 50 | tickSpinner() 51 | val now = Instant.now() 52 | val change = now.toEpochMilli() - startInstant.toEpochMilli() 53 | synchronized(Companion) { 54 | print("\r$message [${spinner.highlight()}] (${change}ms)") 55 | } 56 | } 57 | 58 | 59 | private fun finish() { 60 | val now = Instant.now() 61 | val change = now.toEpochMilli() - startInstant.toEpochMilli() 62 | 63 | synchronized(Companion) { 64 | println("\r$message [${"DONE".highlight()}] (${change}ms)") 65 | } 66 | } 67 | 68 | private fun tickSpinner() { 69 | if (delay) { 70 | spinner = when (spinner) { 71 | "|" -> "/" 72 | "/" -> "-" 73 | "-" -> "\\" 74 | "\\" -> "|" 75 | else -> "|" 76 | } 77 | } 78 | } 79 | 80 | companion object { 81 | val scope = CoroutineScope(Dispatchers.Default) 82 | 83 | @Volatile 84 | var spinnerActive = false 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Modifold 2 | 3 | Modifold is a Kotlin CLI utility that allows you to transfer mods from curseforge to modrinth. 4 | **This project is not affiliated with curseforge or modrinth** 5 | 6 | ### Usage 7 | 8 | (Please make sure you are using java 9+, java 8 will not work) 9 | 10 | The first thing you need to know is how to get a curseforge project ID. On the sidebar in the "About Project" tab, you can 11 | find that ID. You will need the numerical ID of every curseforge project you want to transfer to modrinth. 12 | 13 | ![An image showing where the curseforge project ID is located on the project page](images/curseforge_id.png "Curseforge ID location") 14 | 15 | You will also need your curseforge username, this is used to verify the ownership of each mod being moved to modrinth, 16 | and will be discarded if it does not match. 17 | (You must be listed as an *author*, it's possible to be listed as a contributor but not an author) 18 | 19 | **Make sure 3rd party downloads are enabled on the project** 20 | 21 | There is a full `-h` menu, however the simplest invocation of modifold is 22 | simply `java -jar modifold.jar `. This will walk you through any necessary 23 | steps, with notices along the way, so please read carefully to avoid confusion. The tool will only move entire 24 | projects, *not* individual files. 25 | 26 | Some other common flags would be `-l LICENSE` to set the default license and `-d DISCORD` to set the discord link. 27 | License field will be verified by the modrinth team so make sure you update it, its `arr` by default. 28 | 29 | **Remember to update the projects once created!** Modifold creates the mods as drafts so that you can add sources, 30 | issues, delete if needed, ect. 31 | 32 | 33 | ### What next? 34 | 35 | If you want to automate publishing to modrinth I recommend the [minotaur](https://github.com/modrinth/minotaur) plugin (maintained by the modrinth team) or [mc-publish](https://github.com/Kir-Antipov/mc-publish). 36 | 37 | ### FAQ 38 | 39 | --- 40 | Q: Will you ever add individual file transfer support? 41 | 42 | A: No, that's out of scope for this, and im concerned it would make people rely on this tool instead of properly 43 | embracing modrinth. 44 | 45 | --- 46 | 47 | Q: Is this safe to use? 48 | 49 | A: Probably! It uses a proxy for the official 3rd party api, so it should be capable of accessing everything in a normal way. 50 | 51 | --- 52 | 53 | Q: Modpack support? 54 | 55 | A: Not really viable without much more work than I'm willing to put in, please see the amazing [packwiz](https://github.com/packwiz/packwiz) project for a tool that is mostly capable of that. 56 | -------------------------------------------------------------------------------- /API-Conversion/src/main/kotlin/com/github/p03w/modifold/conversion/MapCategories.kt: -------------------------------------------------------------------------------- 1 | package com.github.p03w.modifold.conversion 2 | 3 | import com.github.p03w.modifold.cli.ModifoldArgs 4 | import com.github.p03w.modifold.cli.ModifoldArgsContainer.DONT 5 | import com.github.p03w.modifold.cli.debug 6 | import com.github.p03w.modifold.cli.warn 7 | import com.github.p03w.modifold.curseforge_schema.CurseforgeCategory 8 | 9 | fun mapCategories(curseforgeCategories: List): List { 10 | val out = mutableSetOf() 11 | 12 | val mapping = mapOf( 13 | "World Gen" to "worldgen", 14 | 15 | "Armor, Tools, and Weapons" to "equipment", 16 | 17 | "Technology" to "technology", 18 | "Redstone" to "technology", 19 | 20 | "Adventure and RPG" to "adventure", 21 | 22 | "Magic" to "magic", 23 | 24 | "Miscellaneous" to "misc", 25 | "Twitch Integration" to "misc", 26 | 27 | "Storage" to "storage", 28 | 29 | "Server Utility" to "utility", 30 | "Utility & QoL" to "utility", 31 | "Map and Information" to "utility", 32 | 33 | "API and Library" to "library", 34 | 35 | "Cosmetic" to "decoration", 36 | 37 | "Food" to "food", 38 | 39 | "Combat / PvP" to "combat", 40 | 41 | "Hardcore" to "challenging", 42 | 43 | "Multiplayer" to "multiplayer", 44 | 45 | "Quests" to "quests", 46 | 47 | if (!ModifoldArgs.args.donts.contains(DONT.CURSE_MCREATOR)) "MCreator" to "cursed" else "MCreator" to "misc" 48 | ) 49 | 50 | curseforgeCategories.forEach { 51 | val name = it.name 52 | if (mapping.containsKey(name)) { 53 | out.add(mapping[name]!!) 54 | } else { 55 | debug("No category mapping found for \"$name\"") 56 | } 57 | } 58 | 59 | return out.toList() 60 | } 61 | 62 | fun checkForUnknownCategories(known: Set) { 63 | val expected = setOf( 64 | "technology", 65 | "adventure", 66 | "magic", 67 | "utility", 68 | "decoration", 69 | "library", 70 | "cursed", 71 | "worldgen", 72 | "storage", 73 | "food", 74 | "equipment", 75 | "misc", 76 | "optimization", 77 | "combat", 78 | "challenging", 79 | "multiplayer", 80 | "quests", 81 | "kitchen-sink", 82 | "lightweight" 83 | ) 84 | 85 | val missing = known subtract expected 86 | 87 | if (missing.isNotEmpty()) { 88 | warn("Unknown modrinth categories: $missing") 89 | warn("These cannot be converted to with this version of the tool, make sure to apply them yourself if relevant") 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/p03w/modifold/Main.kt: -------------------------------------------------------------------------------- 1 | package com.github.p03w.modifold 2 | 3 | import com.github.p03w.modifold.cli.* 4 | import com.github.p03w.modifold.cli.ModifoldArgsContainer.DONT 5 | import com.github.p03w.modifold.conversion.checkForUnknownCategories 6 | import com.github.p03w.modifold.core.* 7 | import com.github.p03w.modifold.modrinth_api.ModrinthAPI 8 | import com.xenomachina.argparser.* 9 | import org.fusesource.jansi.AnsiConsole 10 | import kotlin.system.exitProcess 11 | 12 | suspend fun main(args: Array) { 13 | AnsiConsole.systemInstall() 14 | 15 | try { 16 | ModifoldArgs.args = ArgParser(args, helpFormatter = DefaultHelpFormatter(prologue = Global.helpMenuPrologue)).parseInto(::ModifoldArgsContainer) 17 | } catch (err: SystemExitException) { 18 | log("See the help menu (-h or --help) for usage") 19 | mainBody { 20 | throw err 21 | } 22 | } 23 | 24 | if (ModifoldArgs.args.noModrinth) { 25 | noModrinthFlow() 26 | AnsiConsole.systemUninstall() 27 | return 28 | } 29 | 30 | if (!ModifoldArgs.args.donts.contains(DONT.MAP_CATEGORIES)) { 31 | checkForUnknownCategories(ModrinthAPI.getPossibleCategories().mapTo(mutableSetOf()) {it.name}) 32 | } 33 | verifyDefaultArgs() 34 | 35 | if (!ModifoldArgs.args.donts.contains(DONT.VERIFY_END_USER)) { 36 | if (!userUnderstandsUsage()) { 37 | println("Quiting") 38 | exitProcess(1) 39 | } 40 | } 41 | 42 | 43 | // Start actual flow with login 44 | ModrinthAPI.AuthToken = loginToModrinth() 45 | 46 | debug("Verifying and standardizing modrinth user") 47 | val modrinthUser = try { 48 | ModrinthAPI.getUser() 49 | } catch (err: Throwable) { 50 | error("Failed to login! Invalid access token?", err) 51 | } 52 | log("Modrinth login successful, user is ${modrinthUser.username} (${modrinthUser.id})") 53 | 54 | val curseforgeProjects = collectCurseforgeProjects(ModifoldArgs.args.curseforgeIDs) 55 | 56 | if (curseforgeProjects.isEmpty()) { 57 | error("No projects to transfer") 58 | } 59 | matchExistingProjects(modrinthUser, curseforgeProjects) 60 | if (curseforgeProjects.isEmpty()) { 61 | error("No projects to transfer") 62 | } 63 | 64 | log("Done matching projects") 65 | 66 | val toSave = getInfoToSaveLocally() 67 | val toTransfer = getFilesToTransfer() 68 | 69 | val projectMapping = createModrinthProjects(curseforgeProjects) 70 | 71 | log("Beginning file transfer") 72 | transferProjectFiles(projectMapping, toSave, toTransfer) 73 | 74 | log("Done! Don't forget to fix up the created mods and submit for approval!") 75 | 76 | AnsiConsole.systemUninstall() 77 | } 78 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if %ERRORLEVEL% equ 0 goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if %ERRORLEVEL% equ 0 goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | set EXIT_CODE=%ERRORLEVEL% 84 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 85 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 86 | exit /b %EXIT_CODE% 87 | 88 | :mainEnd 89 | if "%OS%"=="Windows_NT" endlocal 90 | 91 | :omega 92 | -------------------------------------------------------------------------------- /API-Modrinth/src/main/kotlin/com/github/p03w/modifold/modrinth_api/ModrinthProjectCreate.kt: -------------------------------------------------------------------------------- 1 | package com.github.p03w.modifold.modrinth_api 2 | 3 | import com.github.p03w.modifold.cli.ModifoldArgs 4 | import com.github.p03w.modifold.cli.ModifoldArgsContainer.DONT 5 | import com.github.p03w.modifold.conversion.mapCategories 6 | import com.github.p03w.modifold.curseforge_schema.CurseforgeProject 7 | import com.github.p03w.modifold.modrinth_schema.ModrinthDonationURL 8 | 9 | @Suppress("DataClassPrivateConstructor") 10 | data class ModrinthProjectCreate private constructor( 11 | val slug: String, 12 | val title: String, 13 | val description: String, 14 | val body: String, 15 | 16 | val categories: List, 17 | 18 | val client_side: String = "required", 19 | val server_side: String = "required", 20 | 21 | val issues_url: Any? = null, 22 | val source_url: Any? = null, 23 | val wiki_url: Any? = null, 24 | val discord_url: Any? = null, 25 | val donation_urls: List? = null, 26 | 27 | val license_id: String, 28 | val license_url: Any? = null, 29 | 30 | val project_type: String = "mod", 31 | val initial_versions: List = emptyList(), 32 | val is_draft: Boolean = true, 33 | val gallery_items: List? = null 34 | ) { 35 | companion object { 36 | fun of(curseforgeProject: CurseforgeProject, description: String?): ModrinthProjectCreate { 37 | val copyLinks = ModifoldArgs.args.donts.contains(DONT.COPY_LINKS).not() 38 | return ModrinthProjectCreate( 39 | title = curseforgeProject.name, 40 | slug = curseforgeProject.slug, 41 | description = curseforgeProject.summary, 42 | body = description ?: "Autogenerated project from modifold", 43 | license_id = ModifoldArgs.args.defaultLicense, 44 | categories = if (ModifoldArgs.args.donts.contains(DONT.MAP_CATEGORIES)) emptyList() else mapCategories( 45 | curseforgeProject.categories 46 | ), 47 | discord_url = ModifoldArgs.args.discordServer.takeIf { 48 | it.isNotBlank() && copyLinks 49 | }, 50 | donation_urls = ModifoldArgs.args.donationLinks.map { 51 | if ("paypal" in it) { 52 | ModrinthDonationURL("paypal", "Paypal", it) 53 | } else if ("patreon" in it) { 54 | ModrinthDonationURL("pateron", "Patreon", it) 55 | } else if ("buymeacoffee" in it) { 56 | ModrinthDonationURL("bmac", "Buy Me a Coffee", it) 57 | } else if ("github" in it) { 58 | ModrinthDonationURL("github", "GitHub Sponsors", it) 59 | } else if ("ko-fi" in it) { 60 | ModrinthDonationURL("ko-fi", "Ko-fi", it) 61 | } else { 62 | ModrinthDonationURL("other", "Other", it) 63 | } 64 | }, 65 | issues_url = curseforgeProject.links.issuesUrl?.takeIf { 66 | it.isNotBlank() && copyLinks 67 | }, 68 | source_url = curseforgeProject.links.sourceUrl?.takeIf { 69 | it.isNotBlank() && copyLinks 70 | }, 71 | wiki_url = curseforgeProject.links.wikiUrl?.takeIf { 72 | it.isNotBlank() && copyLinks 73 | } 74 | ) 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /API-Common/src/main/kotlin/com/github/p03w/modifold/api_core/APIInterface.kt: -------------------------------------------------------------------------------- 1 | package com.github.p03w.modifold.api_core 2 | 3 | import com.github.p03w.modifold.cli.debug 4 | import io.ktor.client.* 5 | import io.ktor.client.engine.cio.* 6 | import io.ktor.client.features.json.* 7 | import io.ktor.client.request.* 8 | import io.ktor.client.request.forms.* 9 | import io.ktor.client.statement.* 10 | import io.ktor.util.reflect.* 11 | import kotlinx.coroutines.runBlocking 12 | import java.time.Instant 13 | import kotlin.time.Duration.Companion.milliseconds 14 | import kotlin.time.Duration.Companion.seconds 15 | 16 | @Suppress("DEPRECATION") 17 | abstract class APIInterface { 18 | abstract val ratelimit: Ratelimit 19 | open val ratelimitRemainingHeader = "" 20 | open val ratelimitResetHeader = "" 21 | 22 | open val client = HttpClient(CIO) { 23 | install(JsonFeature) 24 | } 25 | open fun HttpRequestBuilder.attachAuth() {} 26 | 27 | fun waitUntilCanSend() { 28 | @Suppress("ControlFlowWithEmptyBody") 29 | while (!ratelimit.canSend) { 30 | 31 | } 32 | ratelimit.makeRequest() 33 | } 34 | 35 | @Deprecated("Internal only, please") 36 | suspend inline fun HttpResponse.extractRatelimit(): T { 37 | val ratelimitRemaining = response.headers[ratelimitRemainingHeader] 38 | val ratelimitReset = response.headers[ratelimitResetHeader] 39 | if (ratelimitRemaining != null) { 40 | debug("Got ratelimit remaining header of ${ratelimitRemaining.toInt()}") 41 | ratelimit.remainingRequests = ratelimitRemaining.toInt() 42 | } 43 | if (ratelimitReset != null) { 44 | debug("Got ratelimit reset header of ${ratelimitReset.toInt()}") 45 | // +200ms to make sure we don't undershoot 46 | ratelimit.secondsUntilReset = ratelimitReset.toInt().seconds + 200.milliseconds 47 | ratelimit.resetClock = Instant.now() 48 | } 49 | return call.receive(typeInfo()) as T 50 | } 51 | 52 | inline fun getWithoutAuth(url: String): T { 53 | return runBlocking { 54 | waitUntilCanSend() 55 | debug("GET | $url") 56 | return@runBlocking client.get(url).extractRatelimit() 57 | } 58 | } 59 | 60 | inline fun get(url: String): T { 61 | return runBlocking { 62 | waitUntilCanSend() 63 | debug("GET(AUTHED) | $url") 64 | return@runBlocking client.get(url) { attachAuth() }.extractRatelimit() 65 | } 66 | } 67 | 68 | inline fun post(url: String, crossinline action: HttpRequestBuilder.() -> Unit): T { 69 | return runBlocking { 70 | waitUntilCanSend() 71 | debug("POST(AUTHED) | $url") 72 | return@runBlocking client.post(url) { 73 | attachAuth() 74 | action() 75 | }.extractRatelimit() 76 | } 77 | } 78 | 79 | inline fun postForm(url: String, crossinline action: FormBuilder.() -> Unit): T { 80 | return runBlocking { 81 | waitUntilCanSend() 82 | debug("SUBMITFORM(AUTHED) | $url") 83 | return@runBlocking client.submitForm(url) { 84 | attachAuth() 85 | 86 | body = MultiPartFormDataContent( 87 | formData { 88 | action() 89 | } 90 | ) 91 | }.extractRatelimit() 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /CLITools/src/main/kotlin/com/github/p03w/modifold/cli/ModifoldArgs.kt: -------------------------------------------------------------------------------- 1 | package com.github.p03w.modifold.cli 2 | 3 | import com.xenomachina.argparser.ArgParser 4 | import com.xenomachina.argparser.default 5 | import java.util.* 6 | 7 | object ModifoldArgs { 8 | lateinit var args: ModifoldArgsContainer 9 | } 10 | 11 | class ModifoldArgsContainer(parser: ArgParser) { 12 | // 13 | // Optional Args 14 | // 15 | 16 | val debug by parser.flagging( 17 | "-v", "--verbose", 18 | help = "Enable debug/verbose mode" 19 | ) 20 | 21 | val modrinthToken by parser.storing( 22 | "--token", 23 | help = "Sets the modrinth access token manually, bypassing the web-auth flow" 24 | ).default(null) 25 | 26 | @Suppress("SpellCheckingInspection") 27 | val donts by parser.adding( 28 | "--dont", 29 | help = "Things to not do (can be repeated), " + 30 | "pass 0 to disable startup confirm, " + 31 | "1 to disable checking existing modrinth mods, " + 32 | "2 to change the mcreator->cursed mapping to mcreator->misc, " + 33 | "3 to disable category mapping entirely, " + 34 | "4 to disable copying links, " + 35 | "5 to disable migration of project bodies", 36 | argName = "DONT_INDEX" 37 | ) { DONT.values()[toInt()] } 38 | 39 | val curseforgeSpeed by parser.storing( 40 | "-s", "--speed", 41 | help = "Speed at which to make requests to curseforge, in minimum ms delay between requests. " + 42 | "Curseforge doesn't document their rate-limits so this is my solution. " + 43 | "Defaults to 2000ms (2 seconds)" 44 | ) { toInt() }.default(2000) 45 | 46 | val defaultLicense by parser.storing( 47 | "-l", "--license", 48 | help = "The default license for newly created projects, i.e mpl, lgpl-3, apache, cc0. Defaults to ARR" 49 | ) { lowercase(Locale.getDefault()) }.default("arr") 50 | 51 | val discordServer by parser.storing( 52 | "-d", "--discord", 53 | help = "The discord server link to add to each mod page" 54 | ).default("") 55 | 56 | val donationLinks by parser.adding( 57 | "--donation", 58 | help = "The sponsor/donation link to add to each mod page" 59 | ).default(listOf("")) 60 | 61 | val defaultLoaders by parser.adding( 62 | "-L", "--loader", 63 | help = "What loader to add to mods by default if no loader is specified, defaults to forge", 64 | argName = "DEFAULT_LOADER" 65 | ) { lowercase(Locale.getDefault()) }.default(listOf("forge")) 66 | 67 | val fileLimit by parser.storing( 68 | "-f", "--file-limit", 69 | help = "Limits how many files to transfer (recent first), -1 (default) to disable/transfer all" 70 | ) { toInt() }.default(-1) 71 | 72 | val noModrinth by parser.flagging( 73 | "--no-modrinth", 74 | help = "Disables all functionality related to modrinth" 75 | ) 76 | 77 | // 78 | // Required args 79 | // 80 | 81 | val curseforgeUsername by parser.positional( 82 | "CURSEFORGE_USERNAME", 83 | help = "The curseforge username so the program only moves projects you own" 84 | ) 85 | 86 | val curseforgeIDs by parser.positionalList( 87 | "CURSEFORGE_PROJECT_ID", 88 | help = "Adds a curseforge project to transfer by numerical ID", 89 | sizeRange = 1..Int.MAX_VALUE 90 | ) { toInt() } 91 | 92 | enum class DONT { 93 | VERIFY_END_USER, 94 | VERIFY_EXISTING, 95 | CURSE_MCREATOR, 96 | MAP_CATEGORIES, 97 | COPY_LINKS, 98 | MIGRATE_DESCRIPTION 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/p03w/modifold/core/ModrinthLogin.kt: -------------------------------------------------------------------------------- 1 | package com.github.p03w.modifold.core 2 | 3 | import com.github.kinquirer.KInquirer 4 | import com.github.kinquirer.components.ListViewOptions 5 | import com.github.kinquirer.components.promptInputPassword 6 | import com.github.kinquirer.components.promptList 7 | import com.github.p03w.modifold.cli.* 8 | import com.github.p03w.modifold.core.github_schema.AuthToken 9 | import com.github.p03w.modifold.core.github_schema.DeviceCode 10 | import io.ktor.client.* 11 | import io.ktor.client.engine.cio.* 12 | import io.ktor.client.features.json.* 13 | import io.ktor.client.request.* 14 | import io.ktor.http.* 15 | import kotlinx.coroutines.* 16 | import java.awt.Desktop 17 | import java.net.URI 18 | import kotlin.system.exitProcess 19 | import kotlin.time.Duration.Companion.milliseconds 20 | import kotlin.time.Duration.Companion.seconds 21 | import kotlin.time.ExperimentalTime 22 | 23 | suspend fun loginToModrinth(): String { 24 | if (ModifoldArgs.args.modrinthToken != null) { 25 | log("Token was passed through CLI, using that") 26 | return ModifoldArgs.args.modrinthToken!! 27 | } 28 | 29 | val option = KInquirer.promptList( 30 | "How do you want to login to modrinth?", 31 | listOf("Web Flow", "Manual Token Entry", "CLI Arg"), 32 | viewOptions = ListViewOptions(questionMarkPrefix = "") 33 | ) 34 | 35 | return when (option) { 36 | "Web Flow" -> doWebFlow() 37 | "Manual Token Entry" -> doManualEntry() 38 | "CLI Arg" -> showCLIExplainer() 39 | else -> throw IllegalStateException() 40 | } 41 | } 42 | 43 | fun showCLIExplainer(): Nothing { 44 | log("To pass the access token through CLI, pass --token ") 45 | exitProcess(0) 46 | } 47 | 48 | @OptIn(ExperimentalTime::class) 49 | suspend fun doWebFlow(): String { 50 | if (!Desktop.getDesktop().isSupported(Desktop.Action.BROWSE) && ModifoldArgs.args.modrinthToken == null) { 51 | error("Your system does not support starting a web browser! Cannot do web flow.") 52 | } 53 | 54 | debug("Starting github request client") 55 | val client = HttpClient(CIO) { 56 | install(JsonFeature) 57 | } 58 | 59 | debug("POSTing github for device code") 60 | val deviceCode: DeviceCode = client.post("https://github.com/login/device/code") { 61 | accept(ContentType("application", "json")) 62 | parameter("client_id", "7eacdcb00a21e6d6a847") 63 | } 64 | 65 | debug("Device code is ${deviceCode.device_code}") 66 | debug("User code is ${deviceCode.user_code}") 67 | 68 | await("Enter the code ${deviceCode.user_code.bold()} on ${deviceCode.verification_uri} (press enter to open)") 69 | withContext(Dispatchers.IO) { 70 | Desktop.getDesktop().browse(URI.create(deviceCode.verification_uri)) 71 | } 72 | 73 | log("Waiting for approval...") 74 | lateinit var authToken: String 75 | withContext(Dispatchers.IO) { 76 | launch { 77 | while (isActive) { 78 | delay(deviceCode.interval.seconds + 100.milliseconds) 79 | val response: AuthToken = client.post("https://github.com/login/oauth/access_token") { 80 | accept(ContentType("application", "json")) 81 | parameter("client_id", "7eacdcb00a21e6d6a847") 82 | parameter("device_code", deviceCode.device_code) 83 | parameter("grant_type", "urn:ietf:params:oauth:grant-type:device_code") 84 | } 85 | if (response.access_token != null) { 86 | log("Got access token!") 87 | authToken = response.access_token 88 | cancel() 89 | } 90 | } 91 | } 92 | } 93 | client.close() 94 | return authToken 95 | } 96 | 97 | fun doManualEntry(): String { 98 | return KInquirer.promptInputPassword( 99 | "Enter your modrinth access token", 100 | hint = "Go to account > Settings > Security > Copy token to clipboard" 101 | ) 102 | } 103 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/p03w/modifold/core/SimilarProjectFinder.kt: -------------------------------------------------------------------------------- 1 | package com.github.p03w.modifold.core 2 | 3 | import com.github.p03w.modifold.cli.withSpinner 4 | import com.github.p03w.modifold.curseforge_schema.CurseforgeProject 5 | import com.github.p03w.modifold.modrinth_schema.ModrinthProject 6 | import com.kennethlange.nlp.similarity.TextSimilarity 7 | import com.kennethlange.nlp.similarity.TokenizerImpl 8 | 9 | class SimilarProjectFinder( 10 | private val modrinthProjects: List, 11 | curseforgeProjects: List 12 | ) { 13 | private val ts = TextSimilarity(TokenizerImpl(IGNORED_WORDS)) 14 | 15 | val ignoredIDs: MutableSet = mutableSetOf() 16 | 17 | init { 18 | withSpinner("Pre-calculating project tf-idf weight") { 19 | modrinthProjects.forEach { 20 | ts.addDocument("$MODRINTH_PREFIX:${it.id}", it.slug + it.description + it.body) 21 | } 22 | curseforgeProjects.forEach { 23 | ts.addDocument("$CURSEFORGE_PREFIX:${it.id}", it.slug + it.name + it.summary) 24 | } 25 | 26 | ts.calculate() 27 | } 28 | } 29 | 30 | fun findSimilar(cfProject: CurseforgeProject): ModrinthProject? { 31 | val closest = ts.getSimilarDocuments("$CURSEFORGE_PREFIX:${cfProject.id}").firstOrNull { 32 | it.startsWith(MODRINTH_PREFIX) && !ignoredIDs.contains(it.split(":")[1]) 33 | } ?: return null 34 | 35 | val id = closest.split(":")[1] 36 | return modrinthProjects.first { it.id == id } 37 | } 38 | 39 | companion object { 40 | const val MODRINTH_PREFIX = "MODRINTH" 41 | const val CURSEFORGE_PREFIX = "CURSEFORGE" 42 | 43 | val IGNORED_WORDS = hashSetOf( 44 | // Default ones, have to copy here 45 | "a", 46 | "able", 47 | "about", 48 | "across", 49 | "after", 50 | "all", 51 | "almost", 52 | "also", 53 | "am", 54 | "among", 55 | "an", 56 | "and", 57 | "any", 58 | "are", 59 | "as", 60 | "at", 61 | "be", 62 | "because", 63 | "been", 64 | "but", 65 | "by", 66 | "can", 67 | "cannot", 68 | "could", 69 | "dear", 70 | "did", 71 | "do", 72 | "does", 73 | "either", 74 | "else", 75 | "ever", 76 | "every", 77 | "for", 78 | "from", 79 | "get", 80 | "got", 81 | "had", 82 | "has", 83 | "have", 84 | "he", 85 | "her", 86 | "hers", 87 | "him", 88 | "his", 89 | "how", 90 | "however", 91 | "i", 92 | "if", 93 | "in", 94 | "into", 95 | "is", 96 | "it", 97 | "its", 98 | "just", 99 | "least", 100 | "let", 101 | "like", 102 | "likely", 103 | "may", 104 | "me", 105 | "might", 106 | "most", 107 | "must", 108 | "my", 109 | "neither", 110 | "no", 111 | "nor", 112 | "not", 113 | "of", 114 | "off", 115 | "often", 116 | "on", 117 | "only", 118 | "or", 119 | "other", 120 | "our", 121 | "own", 122 | "rather", 123 | "said", 124 | "say", 125 | "says", 126 | "she", 127 | "should", 128 | "since", 129 | "so", 130 | "some", 131 | "than", 132 | "that", 133 | "the", 134 | "their", 135 | "them", 136 | "then", 137 | "there", 138 | "these", 139 | "they", 140 | "this", 141 | "tis", 142 | "to", 143 | "too", 144 | "twas", 145 | "us", 146 | "wants", 147 | "was", 148 | "we", 149 | "were", 150 | "what", 151 | "when", 152 | "where", 153 | "which", 154 | "while", 155 | "who", 156 | "whom", 157 | "why", 158 | "will", 159 | "with", 160 | "would", 161 | "yet", 162 | "you", 163 | "your", 164 | // Custom words 165 | "minecraft", 166 | "mod", 167 | "allow", 168 | "allows", 169 | "add", 170 | "adds", 171 | "make", 172 | "makes", 173 | "recipe", 174 | "recipes", 175 | "requires", 176 | "discord", 177 | "config", 178 | "configuration", 179 | "github", 180 | "curseforge", 181 | "modrinth", 182 | "wiki", 183 | "data", 184 | "features", 185 | "optional", 186 | "crafting", 187 | "crash", 188 | "bug", 189 | "report" 190 | ) 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/p03w/modifold/core/TransferProjectFiles.kt: -------------------------------------------------------------------------------- 1 | package com.github.p03w.modifold.core 2 | 3 | import com.github.p03w.modifold.cli.* 4 | import com.github.p03w.modifold.curseforge_api.CurseforgeAPI 5 | import com.github.p03w.modifold.curseforge_schema.CurseforgeProject 6 | import com.github.p03w.modifold.modrinth_api.ModrinthAPI 7 | import com.github.p03w.modifold.modrinth_schema.ModrinthProject 8 | import java.io.File 9 | import java.net.URL 10 | import java.nio.file.Files 11 | import java.nio.file.StandardCopyOption 12 | import java.time.Instant 13 | import java.util.* 14 | 15 | private fun File.make(): File { 16 | mkdirs() 17 | return this 18 | } 19 | 20 | fun transferProjectFiles( 21 | mapping: MutableMap, 22 | toSave: EnumSet, 23 | toTransfer: FileSet 24 | ) { 25 | mapping.keys.forEach { project -> 26 | val files = withSpinner("Collecting files for ${project.display()})") { 27 | CurseforgeAPI.getProjectFiles(project.id, toTransfer == FileSet.ALL) { 28 | error("Could not get curseforge files for project ${project.display()}") 29 | }.sortedBy { Instant.parse(it.fileDate) } 30 | }.let { 31 | if (ModifoldArgs.args.fileLimit > -1) { 32 | it.takeLast(ModifoldArgs.args.fileLimit) 33 | } else { 34 | it 35 | } 36 | } 37 | 38 | val modrinthProject = ModrinthAPI.getProjectInfo(mapping[project]!!.id) 39 | 40 | // Save logo 41 | if (toSave.contains(InformationToSave.IMAGES)) { 42 | debug("Saving ${project.logo.url} as project_icon.png") 43 | val localCopy = File("ModifoldSaved/${project.display()}/images/project_icon.png").make() 44 | Files.copy(URL(project.logo.url).openStream(), localCopy.toPath(), StandardCopyOption.REPLACE_EXISTING) 45 | } 46 | 47 | // Transfer screenshots 48 | project.screenshots.forEach nextAttach@{ 49 | withSpinner("Transferring ${it.title} to gallery") { spinner -> 50 | if (toSave.contains(InformationToSave.IMAGES)) { 51 | debug("Saving ${it.url} as ${it.title}.${it.getExt()}") 52 | val localCopy = File("ModifoldSaved/${project.display()}/images/${it.title}.${it.getExt()}").make() 53 | Files.copy(URL(it.url).openStream(), localCopy.toPath(), StandardCopyOption.REPLACE_EXISTING) 54 | } 55 | try { 56 | ModrinthAPI.addProjectImage(modrinthProject, it) 57 | } catch (err: Throwable) { 58 | log("Failed to add ${it.title} to gallery! ${err.localizedMessage}".error().toString()) 59 | spinner.fail() 60 | } 61 | } 62 | 63 | } 64 | 65 | files.forEach { file -> 66 | withSpinner("Transferring ${file.fileName}") { spinner -> 67 | val buffered = CurseforgeAPI.getFileStream(file).buffered() 68 | 69 | val stream = if (toSave.contains(InformationToSave.VERSIONS)) { 70 | debug("Saving version to disk") 71 | val localCopy = File("ModifoldSaved/${project.display()}/versions/${file.fileName}").make() 72 | Files.copy(buffered, localCopy.toPath(), StandardCopyOption.REPLACE_EXISTING) 73 | localCopy.inputStream().buffered() 74 | } else { 75 | buffered 76 | } 77 | 78 | try { 79 | ModrinthAPI.makeProjectVersion( 80 | modrinthProject, 81 | file, 82 | stream, 83 | project 84 | ) 85 | } catch (err: Throwable) { 86 | log("Failed to upload ${file.fileName}! ${err.localizedMessage}".error().toString()) 87 | spinner.fail() 88 | } 89 | } 90 | } 91 | println() 92 | } 93 | } 94 | 95 | fun backupProjectFiles( 96 | projects: List, 97 | toSave: EnumSet, 98 | toTransfer: FileSet 99 | ) { 100 | projects.forEach { project -> 101 | val files = withSpinner("Collecting files for ${project.display()})") { 102 | CurseforgeAPI.getProjectFiles(project.id, toTransfer == FileSet.ALL) { 103 | error("Could not get curseforge files for project ${project.display()}") 104 | }.sortedBy { Instant.parse(it.fileDate) } 105 | }.let { 106 | if (ModifoldArgs.args.fileLimit > -1) { 107 | it.takeLast(ModifoldArgs.args.fileLimit) 108 | } else { 109 | it 110 | } 111 | } 112 | 113 | // Save logo 114 | if (toSave.contains(InformationToSave.IMAGES)) { 115 | debug("Saving ${project.logo.url} as project_icon.png") 116 | val localCopy = File("ModifoldSaved/${project.display()}/images/project_icon.png").make() 117 | Files.copy(URL(project.logo.url).openStream(), localCopy.toPath(), StandardCopyOption.REPLACE_EXISTING) 118 | } 119 | 120 | // Transfer screenshots 121 | project.screenshots.forEach nextAttach@{ 122 | if (toSave.contains(InformationToSave.IMAGES)) { 123 | withSpinner("Saving gallery image ${it.title}") { _ -> 124 | debug("Saving ${it.url} as ${it.title}.${it.getExt()}") 125 | val localCopy = File("ModifoldSaved/${project.display()}/images/${it.title}.${it.getExt()}").make() 126 | Files.copy(URL(it.url).openStream(), localCopy.toPath(), StandardCopyOption.REPLACE_EXISTING) 127 | } 128 | } 129 | } 130 | 131 | files.forEach { file -> 132 | if (toSave.contains(InformationToSave.VERSIONS)) { 133 | withSpinner("Saving ${file.fileName}") { _ -> 134 | debug("Saving version ${file.fileName} to disk") 135 | val buffered = CurseforgeAPI.getFileStream(file).buffered() 136 | val localCopy = File("ModifoldSaved/${project.display()}/versions/${file.fileName}").make() 137 | Files.copy(buffered, localCopy.toPath(), StandardCopyOption.REPLACE_EXISTING) 138 | localCopy.inputStream().buffered() 139 | } 140 | } 141 | } 142 | println() 143 | } 144 | } -------------------------------------------------------------------------------- /API-Modrinth/src/main/kotlin/com/github/p03w/modifold/modrinth_api/ModrinthAPI.kt: -------------------------------------------------------------------------------- 1 | package com.github.p03w.modifold.modrinth_api 2 | 3 | import com.github.p03w.modifold.api_core.APIInterface 4 | import com.github.p03w.modifold.api_core.Ratelimit 5 | import com.github.p03w.modifold.cli.ModifoldArgs 6 | import com.github.p03w.modifold.cli.warn 7 | import com.github.p03w.modifold.curseforge_schema.CurseforgeAsset 8 | import com.github.p03w.modifold.curseforge_schema.CurseforgeFile 9 | import com.github.p03w.modifold.curseforge_schema.CurseforgeProject 10 | import com.github.p03w.modifold.modrinth_schema.* 11 | import com.google.gson.GsonBuilder 12 | import io.ktor.client.* 13 | import io.ktor.client.engine.cio.* 14 | import io.ktor.client.features.* 15 | import io.ktor.client.features.json.* 16 | import io.ktor.client.request.* 17 | import io.ktor.client.statement.* 18 | import io.ktor.http.* 19 | import io.ktor.utils.io.core.* 20 | import java.io.BufferedInputStream 21 | import java.net.URL 22 | import kotlin.time.Duration.Companion.milliseconds 23 | 24 | 25 | object ModrinthAPI : APIInterface() { 26 | override val ratelimit = Ratelimit(150.milliseconds, false) 27 | override val ratelimitRemainingHeader = "X-Ratelimit-Remaining" 28 | override val ratelimitResetHeader = "X-Ratelimit-Reset" 29 | lateinit var AuthToken: String 30 | 31 | fun getUserAgent(): String { 32 | return "SilverAndro/Modifold/${getFileVersion()} (Silver <3#0955)" 33 | } 34 | 35 | private fun getFileVersion(): String { 36 | return this.javaClass.getResourceAsStream("/version.txt") 37 | ?.bufferedReader()?.readText() ?: "Unknown Version" 38 | } 39 | 40 | override val client = HttpClient(CIO) { 41 | install(JsonFeature) 42 | install(UserAgent) { 43 | agent = getUserAgent() 44 | } 45 | } 46 | 47 | override fun HttpRequestBuilder.attachAuth() { 48 | headers { 49 | append(HttpHeaders.Authorization, AuthToken) 50 | } 51 | } 52 | 53 | const val root = "https://api.modrinth.com/v2" 54 | 55 | fun getPossibleLicenses(): List { 56 | val shortLicenses = getWithoutAuth>("$root/tag/license") 57 | return shortLicenses.map { it.short }.filterNot { it == "custom" } 58 | } 59 | 60 | fun getPossibleLoaders(): List { 61 | return getWithoutAuth("$root/tag/loader") 62 | } 63 | 64 | fun getPossibleCategories(): List { 65 | return getWithoutAuth("$root/tag/category") 66 | } 67 | 68 | fun getProjectInfo(id: String): ModrinthProject { 69 | return get("$root/project/$id") 70 | } 71 | 72 | fun getUser(): ModrinthUser { 73 | return get("$root/user") 74 | } 75 | 76 | fun getUserProjects(user: ModrinthUser): List { 77 | return get>("$root/user/${user.id}/projects").filter { it.project_type == "mod" } 78 | } 79 | 80 | fun makeProject(create: ModrinthProjectCreate, project: CurseforgeProject): ModrinthProject { 81 | return postForm("$root/project") { 82 | append("data", GsonBuilder().serializeNulls().disableHtmlEscaping().create().toJson(create)) 83 | 84 | appendInput("icon", headersOf(HttpHeaders.ContentDisposition, "filename=icon.png")) { 85 | buildPacket { 86 | writeFully( 87 | URL(project.logo.url).openStream().readAllBytes() 88 | ) 89 | } 90 | } 91 | } 92 | } 93 | 94 | private fun getLoaders(file: CurseforgeFile): List { 95 | return file.gameVersions.filterNot { 96 | val lowercase = it.lowercase() 97 | MC_SEMVER.matches(it) || 98 | lowercase.contains("java") || 99 | lowercase == "client" || 100 | lowercase == "server" 101 | }.map { it.lowercase() } 102 | } 103 | 104 | fun addProjectImage(project: ModrinthProject, attachment: CurseforgeAsset) { 105 | post("$root/project/${project.id}/gallery") { 106 | contentType(ContentType("image", attachment.getExt())) 107 | 108 | parameter("ext", attachment.getExt()) 109 | parameter("featured", false) 110 | parameter("title", attachment.title) 111 | parameter("description", attachment.description) 112 | 113 | body = URL(attachment.url).openStream().readAllBytes() 114 | } 115 | } 116 | 117 | fun makeProjectVersion(mod: ModrinthProject, file: CurseforgeFile, stream: BufferedInputStream, project: CurseforgeProject) { 118 | postForm("$root/version") { 119 | val upload = ModrinthVersionUpload( 120 | mod_id = mod.id, 121 | file_parts = listOf("${file.fileName}-0"), 122 | version_number = SEMVER.find(file.fileName.removeSuffix(".jar"))?.value 123 | ?: file.fileName.removeSuffix(".jar"), 124 | version_title = file.displayName, 125 | version_body = "Transferred automatically from https://www.curseforge.com/minecraft/mc-mods/${project.slug}/files/${file.id}", 126 | game_versions = getGameVersions(file), 127 | release_channel = when (file.releaseType) { 128 | 3 -> "alpha" 129 | 2 -> "beta" 130 | 1 -> "release" 131 | else -> throw IllegalArgumentException("Unknown release type ${file.releaseType} on file https://www.curseforge.com/minecraft/mc-mods/${project.slug}/files/${file.id}") 132 | }, 133 | loaders = getLoaders(file).takeUnless { it.isEmpty() } ?: run { 134 | warn("${file.fileName} has no specified loaders, using default loader(s) ${ModifoldArgs.args.defaultLoaders}") 135 | ModifoldArgs.args.defaultLoaders 136 | } 137 | ) 138 | append("data", GsonBuilder().serializeNulls().create().toJson(upload)) 139 | 140 | appendInput( 141 | "${file.fileName}-0", 142 | headersOf(HttpHeaders.ContentDisposition, "filename=${file.fileName}"), 143 | file.fileLength 144 | ) { 145 | buildPacket { 146 | var next = stream.read() 147 | while (next != -1) { 148 | writeByte(next.toByte()) 149 | next = stream.read() 150 | } 151 | } 152 | } 153 | } 154 | } 155 | 156 | private fun getGameVersions(file: CurseforgeFile): List { 157 | val out = mutableSetOf() 158 | file.gameVersions.forEach { 159 | if (MC_SEMVER.matches(it)) { 160 | if (SNAPSHOT_REGEX.containsMatchIn(it)) { 161 | warn( 162 | "Dropping snapshot version $it because curseforge" + 163 | " snapshots are not as precise as modrinth's and cannot" + 164 | "be accurately represented!" 165 | ) 166 | } else { 167 | out.add(it) 168 | } 169 | } 170 | } 171 | return out.toList() 172 | } 173 | 174 | @Suppress("SpellCheckingInspection") 175 | private val SEMVER = 176 | Regex("(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?") 177 | 178 | @Suppress("SpellCheckingInspection") 179 | val MC_SEMVER = Regex("(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:\\.(0|[1-9]\\d*))?(?:-[sS]napshots?)?") 180 | 181 | val SNAPSHOT_REGEX = Regex("-[sS]napshots?") 182 | } 183 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Stop when "xargs" is not available. 209 | if ! command -v xargs >/dev/null 2>&1 210 | then 211 | die "xargs is not available" 212 | fi 213 | 214 | # Use "xargs" to parse quoted args. 215 | # 216 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 217 | # 218 | # In Bash we could simply go: 219 | # 220 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 221 | # set -- "${ARGS[@]}" "$@" 222 | # 223 | # but POSIX shell has neither arrays nor command substitution, so instead we 224 | # post-process each arg (as a line of input to sed) to backslash-escape any 225 | # character that might be a shell metacharacter, then use eval to reverse 226 | # that process (while maintaining the separation between arguments), and wrap 227 | # the whole thing up as a single "set" statement. 228 | # 229 | # This will of course break if any of these variables contains a newline or 230 | # an unmatched quote. 231 | # 232 | 233 | eval "set -- $( 234 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 235 | xargs -n1 | 236 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 237 | tr '\n' ' ' 238 | )" '"$@"' 239 | 240 | exec "$JAVACMD" "$@" 241 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | --------------------------------------------------------------------------------