mirror of
				https://github.com/packwiz/packwiz.git
				synced 2025-10-31 10:34:32 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			357 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			357 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package modrinth
 | |
| 
 | |
| import (
 | |
| 	"encoding/json"
 | |
| 	"errors"
 | |
| 	"github.com/spf13/viper"
 | |
| 	"io/ioutil"
 | |
| 	"net/http"
 | |
| 	"net/url"
 | |
| 	"time"
 | |
| 
 | |
| 	"github.com/Masterminds/semver/v3"
 | |
| 	"github.com/comp500/packwiz/cmd"
 | |
| 	"github.com/comp500/packwiz/core"
 | |
| 	"github.com/spf13/cobra"
 | |
| )
 | |
| 
 | |
| const modrinthApiUrl = "https://api.modrinth.com/api/v1/"
 | |
| 
 | |
| var modrinthApiUrlParsed, _ = url.Parse(modrinthApiUrl)
 | |
| 
 | |
| var modrinthCmd = &cobra.Command{
 | |
| 	Use:     "modrinth",
 | |
| 	Aliases: []string{"mr"},
 | |
| 	Short:   "Manage modrinth-based mods",
 | |
| }
 | |
| 
 | |
| func init() {
 | |
| 	cmd.Add(modrinthCmd)
 | |
| 	core.Updaters["modrinth"] = mrUpdater{}
 | |
| }
 | |
| 
 | |
| type License struct {
 | |
| 	Id   string `json:"id"`   //The license id of a mod, retrieved from the licenses get route
 | |
| 	Name string `json:"name"` //The long for name of a license
 | |
| 	Url  string `json:"url"`  //The URL to this license
 | |
| }
 | |
| 
 | |
| type Mod struct {
 | |
| 	ID          string   `json:"id"`          //The ID of the mod, encoded as a base62 string
 | |
| 	Slug        string   `json:"slug"`        //The slug of a mod, used for vanity URLs
 | |
| 	Team        string   `json:"team"`        //The id of the team that has ownership of this mod
 | |
| 	Title       string   `json:"title"`       //The title or name of the mod
 | |
| 	Description string   `json:"description"` //A short description of the mod
 | |
| 	Body        string   `json:"body"`        //A long form description of the mod.
 | |
| 	BodyUrl     string   `json:"body_url"`    //DEPRECATED The link to the long description of the mod (Optional)
 | |
| 	Published   string   `json:"published"`   //The date at which the mod was first published
 | |
| 	Updated     string   `json:"updated"`     //The date at which the mod was updated
 | |
| 	Status      string   `json:"status"`      //The status of the mod - approved, rejected, draft, unlisted, processing, or unknown
 | |
| 	License     struct { //The license of the mod
 | |
| 		ID   string `json:"id"`
 | |
| 		Name string `json:"name"`
 | |
| 		URL  string `json:"url"`
 | |
| 	} `json:"license"`
 | |
| 	ClientSide string   `json:"client_side"` //The support range for the client mod - required, optional, unsupported, or unknown
 | |
| 	ServerSide string   `json:"server_side"` //The support range for the server mod - required, optional, unsupported, or unknown
 | |
| 	Downloads  int      `json:"downloads"`   //The total number of downloads the mod has
 | |
| 	Categories []string `json:"categories"`  //A list of the categories that the mod is in
 | |
| 	Versions   []string `json:"versions"`    //A list of ids for versions of the mod
 | |
| 	IconUrl    string   `json:"icon_url"`    //The URL of the icon of the mod (Optional)
 | |
| 	IssuesUrl  string   `json:"issues_url"`  //An optional link to where to submit bugs or issues with the mod (Optional)
 | |
| 	SourceUrl  string   `json:"source_url"`  //An optional link to the source code for the mod (Optional)
 | |
| 	WikiUrl    string   `json:"wiki_url"`    //An optional link to the mod's wiki page or other relevant information (Optional)
 | |
| 	DiscordUrl string   `json:"discord_url"` //An optional link to the mod's discord (Optional)
 | |
| }
 | |
| 
 | |
| type ModResult struct {
 | |
| 	ModID         string   `json:"mod_id"`         //The id of the mod; prefixed with local-
 | |
| 	ProjectType   string   `json:"project_id"`     //The project type of the mod
 | |
| 	Author        string   `json:"author"`         //The username of the author of the mod
 | |
| 	Title         string   `json:"title"`          //The name of the mod
 | |
| 	Description   string   `json:"description"`    //A short description of the mod
 | |
| 	Categories    []string `json:"categories"`     //A list of the categories the mod is in
 | |
| 	Versions      []string `json:"versions"`       //A list of the minecraft versions supported by the mod
 | |
| 	Downloads     int      `json:"downloads"`      //The total number of downloads for the mod
 | |
| 	PageUrl       string   `json:"page_url"`       //A link to the mod's main page;
 | |
| 	IconUrl       string   `json:"icon_url"`       //The url of the mod's icon
 | |
| 	AuthorUrl     string   `json:"author_url"`     //The url of the mod's author
 | |
| 	DateCreated   string   `json:"date_created"`   //The date that the mod was originally created
 | |
| 	DateModified  string   `json:"date_modified"`  //The date that the mod was last modified
 | |
| 	LatestVersion string   `json:"latest_version"` //The latest version of minecraft that this mod supports
 | |
| 	License       string   `json:"license"`        //The id of the license this mod follows
 | |
| 	ClientSide    string   `json:"client_side"`    //The side type id that this mod is on the client
 | |
| 	ServerSide    string   `json:"server_side"`    //The side type id that this mod is on the server
 | |
| 	Host          string   `json:"host"`           //The host that this mod is from, always modrinth
 | |
| }
 | |
| 
 | |
| type ModSearchResult struct {
 | |
| 	Hits      []ModResult `json:"hits"`       //The list of results
 | |
| 	Offset    int         `json:"offset"`     //The number of results that were skipped by the query
 | |
| 	Limit     int         `json:"limit"`      //The number of mods returned by the query
 | |
| 	TotalHits int         `json:"total_hits"` //The total number of mods that the query found
 | |
| }
 | |
| 
 | |
| type Version struct {
 | |
| 	ID            string        `json:"id"`             //The ID of the version, encoded as a base62 string
 | |
| 	ModID         string        `json:"mod_id"`         //The ID of the mod this version is for
 | |
| 	AuthorId      string        `json:"author_id"`      //The ID of the author who published this version
 | |
| 	Featured      bool          `json:"featured"`       //Whether the version is featured or not
 | |
| 	Name          string        `json:"name"`           //The name of this version
 | |
| 	VersionNumber string        `json:"version_number"` //The version number. Ideally will follow semantic versioning
 | |
| 	Changelog     string        `json:"changelog"`      //The changelog for this version of the mod. (Optional)
 | |
| 	DatePublished string        `json:"date_published"` //The date that this version was published
 | |
| 	Downloads     int           `json:"downloads"`      //The number of downloads this specific version has
 | |
| 	VersionType   string        `json:"version_type"`   //The type of the release - alpha, beta, or release
 | |
| 	Files         []VersionFile `json:"files"`          //A list of files available for download for this version
 | |
| 	//Dependencies  []string      `json:"dependencies"`   //A list of specific versions of mods that this version depends on
 | |
| 	GameVersions []string `json:"game_versions"` //A list of versions of Minecraft that this version of the mod supports
 | |
| 	Loaders      []string `json:"loaders"`       //The mod loaders that this version supports
 | |
| }
 | |
| 
 | |
| type VersionFile struct {
 | |
| 	Hashes   map[string]string //A map of hashes of the file. The key is the hashing algorithm and the value is the string version of the hash.
 | |
| 	Url      string            //A direct link to the file
 | |
| 	Filename string            //The name of the file
 | |
| 	Primary  bool              // Is the file the primary file?
 | |
| }
 | |
| 
 | |
| func getModIdsViaSearch(query string, versions []string) ([]ModResult, error) {
 | |
| 	baseUrl := *modrinthApiUrlParsed
 | |
| 	baseUrl.Path += "mod"
 | |
| 
 | |
| 	params := url.Values{}
 | |
| 	params.Add("limit", "5")
 | |
| 	params.Add("index", "relevance")
 | |
| 	facets := make([]string, 0)
 | |
| 	for _, v := range versions {
 | |
| 		facets = append(facets, "versions:"+v)
 | |
| 	}
 | |
| 	facetsEncoded, err := json.Marshal(facets)
 | |
| 	if err != nil {
 | |
| 		return []ModResult{}, err
 | |
| 	}
 | |
| 	params.Add("facets", "["+string(facetsEncoded)+"]")
 | |
| 	params.Add("query", query)
 | |
| 
 | |
| 	baseUrl.RawQuery = params.Encode()
 | |
| 
 | |
| 	resp, err := http.Get(baseUrl.String())
 | |
| 	if err != nil {
 | |
| 		return []ModResult{}, err
 | |
| 	}
 | |
| 
 | |
| 	defer resp.Body.Close()
 | |
| 	body, err := ioutil.ReadAll(resp.Body)
 | |
| 	if err != nil {
 | |
| 		return []ModResult{}, err
 | |
| 	}
 | |
| 
 | |
| 	var result ModSearchResult
 | |
| 	err = json.Unmarshal(body, &result)
 | |
| 	if err != nil {
 | |
| 		return []ModResult{}, err
 | |
| 	}
 | |
| 
 | |
| 	if result.TotalHits <= 0 {
 | |
| 		return []ModResult{}, errors.New("Couldn't find that mod. Is it available for this version?")
 | |
| 	}
 | |
| 
 | |
| 	return result.Hits, nil
 | |
| }
 | |
| 
 | |
| func getLatestVersion(modID string, pack core.Pack) (Version, error) {
 | |
| 	mcVersion, err := pack.GetMCVersion()
 | |
| 	if err != nil {
 | |
| 		return Version{}, err
 | |
| 	}
 | |
| 	gameVersions := append([]string{mcVersion}, viper.GetStringSlice("acceptable-game-versions")...)
 | |
| 	gameVersionsEncoded, err := json.Marshal(gameVersions)
 | |
| 	if err != nil {
 | |
| 		return Version{}, err
 | |
| 	}
 | |
| 
 | |
| 	loader := getLoader(pack)
 | |
| 
 | |
| 	baseUrl := *modrinthApiUrlParsed
 | |
| 	baseUrl.Path += "mod/"
 | |
| 	baseUrl.Path += modID
 | |
| 	baseUrl.Path += "/version"
 | |
| 
 | |
| 	params := url.Values{}
 | |
| 	params.Add("game_versions", string(gameVersionsEncoded))
 | |
| 	if loader != "any" {
 | |
| 		params.Add("loaders", "[\""+loader+"\"]")
 | |
| 	}
 | |
| 
 | |
| 	baseUrl.RawQuery = params.Encode()
 | |
| 
 | |
| 	resp, err := http.Get(baseUrl.String())
 | |
| 	if err != nil {
 | |
| 		return Version{}, err
 | |
| 	}
 | |
| 
 | |
| 	if resp.StatusCode == 404 {
 | |
| 		return Version{}, errors.New("couldn't find mod: " + modID)
 | |
| 	}
 | |
| 
 | |
| 	defer resp.Body.Close()
 | |
| 	body, err := ioutil.ReadAll(resp.Body)
 | |
| 	if err != nil {
 | |
| 		return Version{}, err
 | |
| 	}
 | |
| 
 | |
| 	var result []Version
 | |
| 	err = json.Unmarshal(body, &result)
 | |
| 	if err != nil {
 | |
| 		return Version{}, err
 | |
| 	}
 | |
| 
 | |
| 	if len(result) == 0 {
 | |
| 		return Version{}, errors.New("no valid versions found")
 | |
| 	}
 | |
| 
 | |
| 	latestValidVersion := result[0]
 | |
| 	for _, v := range result[1:] {
 | |
| 		currVersion, err1 := semver.NewVersion(v.VersionNumber)
 | |
| 		latestVersion, err2 := semver.NewVersion(latestValidVersion.VersionNumber)
 | |
| 		var semverCompare = 0
 | |
| 		// Only compare with semver if both are valid semver - otherwise compare by release date
 | |
| 		if err1 == nil && err2 == nil {
 | |
| 			semverCompare = currVersion.Compare(latestVersion)
 | |
| 		}
 | |
| 
 | |
| 		if semverCompare == 0 {
 | |
| 			//Semver is equal, compare date instead
 | |
| 			vDate, _ := time.Parse(time.RFC3339Nano, v.DatePublished)
 | |
| 			latestDate, _ := time.Parse(time.RFC3339Nano, latestValidVersion.DatePublished)
 | |
| 			if vDate.After(latestDate) {
 | |
| 				latestValidVersion = v
 | |
| 			}
 | |
| 		} else if semverCompare == 1 {
 | |
| 			latestValidVersion = v
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return latestValidVersion, nil
 | |
| }
 | |
| 
 | |
| func fetchMod(modID string) (Mod, error) {
 | |
| 	var mod Mod
 | |
| 
 | |
| 	resp, err := http.Get(modrinthApiUrl + "mod/" + modID)
 | |
| 	if err != nil {
 | |
| 		return mod, err
 | |
| 	}
 | |
| 
 | |
| 	if resp.StatusCode == 404 {
 | |
| 		return mod, errors.New("couldn't find mod: " + modID)
 | |
| 	}
 | |
| 
 | |
| 	defer resp.Body.Close()
 | |
| 	body, err := ioutil.ReadAll(resp.Body)
 | |
| 	if err != nil {
 | |
| 		return mod, err
 | |
| 	}
 | |
| 
 | |
| 	err = json.Unmarshal(body, &mod)
 | |
| 	if err != nil {
 | |
| 		return mod, err
 | |
| 	}
 | |
| 
 | |
| 	if mod.ID == "" {
 | |
| 		return mod, errors.New("invalid json whilst fetching mod: " + modID)
 | |
| 	}
 | |
| 
 | |
| 	return mod, nil
 | |
| }
 | |
| 
 | |
| func fetchVersion(versionId string) (Version, error) {
 | |
| 	var version Version
 | |
| 
 | |
| 	resp, err := http.Get(modrinthApiUrl + "version/" + versionId)
 | |
| 	if err != nil {
 | |
| 		return version, err
 | |
| 	}
 | |
| 
 | |
| 	if resp.StatusCode == 404 {
 | |
| 		return version, errors.New("couldn't find version: " + versionId)
 | |
| 	}
 | |
| 
 | |
| 	defer resp.Body.Close()
 | |
| 	body, err := ioutil.ReadAll(resp.Body)
 | |
| 	if err != nil {
 | |
| 		return version, err
 | |
| 	}
 | |
| 
 | |
| 	err = json.Unmarshal(body, &version)
 | |
| 	if err != nil {
 | |
| 		return version, err
 | |
| 	}
 | |
| 
 | |
| 	if version.ID == "" {
 | |
| 		return version, errors.New("invalid json whilst fetching version: " + versionId)
 | |
| 	}
 | |
| 
 | |
| 	return version, nil
 | |
| }
 | |
| 
 | |
| func (mod Mod) getSide() string {
 | |
| 	server := shouldDownloadOnSide(mod.ServerSide)
 | |
| 	client := shouldDownloadOnSide(mod.ClientSide)
 | |
| 
 | |
| 	if server && client {
 | |
| 		return core.UniversalSide
 | |
| 	} else if server {
 | |
| 		return core.ServerSide
 | |
| 	} else if client {
 | |
| 		return core.ClientSide
 | |
| 	} else {
 | |
| 		return ""
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func shouldDownloadOnSide(side string) bool {
 | |
| 	return side == "required" || side == "optional"
 | |
| }
 | |
| 
 | |
| func (v VersionFile) getBestHash() (string, string) {
 | |
| 	//try preferred hashes first
 | |
| 	val, exists := v.Hashes["sha256"]
 | |
| 	if exists {
 | |
| 		return "sha256", val
 | |
| 	}
 | |
| 	val, exists = v.Hashes["murmur2"]
 | |
| 	if exists {
 | |
| 		return "murmur2", val
 | |
| 	}
 | |
| 	val, exists = v.Hashes["sha512"]
 | |
| 	if exists {
 | |
| 		return "sha512", val
 | |
| 	}
 | |
| 
 | |
| 	//none of the preferred hashes are present, just get the first one
 | |
| 	for key, val := range v.Hashes {
 | |
| 		return key, val
 | |
| 	}
 | |
| 
 | |
| 	//No hashes were present
 | |
| 	return "", ""
 | |
| }
 | |
| 
 | |
| func getLoader(pack core.Pack) string {
 | |
| 	dependencies := pack.Versions
 | |
| 
 | |
| 	_, hasFabric := dependencies["fabric"]
 | |
| 	_, hasForge := dependencies["forge"]
 | |
| 	if hasFabric && hasForge {
 | |
| 		return "any"
 | |
| 	} else if hasFabric {
 | |
| 		return "fabric"
 | |
| 	} else if hasForge {
 | |
| 		return "forge"
 | |
| 	} else {
 | |
| 		return "any"
 | |
| 	}
 | |
| }
 |