mirror of
https://github.com/packwiz/packwiz.git
synced 2025-04-19 13:06:30 +02:00
Move to go-modrinth lib (v2 API) and always supply UA in HTTP requests
This commit is contained in:
parent
5c02b31e20
commit
044c34e07c
@ -5,7 +5,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
@ -267,7 +266,7 @@ func (m mcVersionManifest) checkValid(version string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getValidMCVersions() (mcVersionManifest, error) {
|
func getValidMCVersions() (mcVersionManifest, error) {
|
||||||
res, err := http.Get("https://launchermeta.mojang.com/mc/game/version_manifest.json")
|
res, err := core.GetWithUA("https://launchermeta.mojang.com/mc/game/version_manifest.json", "application/json")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return mcVersionManifest{}, err
|
return mcVersionManifest{}, err
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,18 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const UserAgent = "packwiz/packwiz"
|
||||||
|
|
||||||
|
func GetWithUA(url string, contentType string) (resp *http.Response, err error) {
|
||||||
|
req, err := http.NewRequest("GET", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Set("User-Agent", UserAgent)
|
||||||
|
req.Header.Set("Accept", contentType)
|
||||||
|
return http.DefaultClient.Do(req)
|
||||||
|
}
|
||||||
|
|
||||||
const DownloadCacheImportFolder = "import"
|
const DownloadCacheImportFolder = "import"
|
||||||
|
|
||||||
type DownloadSession interface {
|
type DownloadSession interface {
|
||||||
@ -143,8 +155,7 @@ func downloadNewFile(task *downloadTask, cacheFolder string, hashesToObtain []st
|
|||||||
if len(hashesToObtain) > 0 {
|
if len(hashesToObtain) > 0 {
|
||||||
var data io.ReadCloser
|
var data io.ReadCloser
|
||||||
if task.url != "" {
|
if task.url != "" {
|
||||||
resp, err := http.Get(task.url)
|
resp, err := GetWithUA(task.url, "application/octet-stream")
|
||||||
// TODO: content type, user-agent?
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return CompletedDownload{}, fmt.Errorf("failed to download %s: %w", task.url, err)
|
return CompletedDownload{}, fmt.Errorf("failed to download %s: %w", task.url, err)
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,6 @@ package core
|
|||||||
import (
|
import (
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -53,7 +52,7 @@ var ModLoaders = map[string]ModLoaderComponent{
|
|||||||
|
|
||||||
func FetchMavenVersionList(url string) func(mcVersion string) ([]string, string, error) {
|
func FetchMavenVersionList(url string) func(mcVersion string) ([]string, string, error) {
|
||||||
return func(mcVersion string) ([]string, string, error) {
|
return func(mcVersion string) ([]string, string, error) {
|
||||||
res, err := http.Get(url)
|
res, err := GetWithUA(url, "application/xml")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return []string{}, "", err
|
return []string{}, "", err
|
||||||
}
|
}
|
||||||
@ -69,7 +68,7 @@ func FetchMavenVersionList(url string) func(mcVersion string) ([]string, string,
|
|||||||
|
|
||||||
func FetchMavenVersionPrefixedList(url string, friendlyName string) func(mcVersion string) ([]string, string, error) {
|
func FetchMavenVersionPrefixedList(url string, friendlyName string) func(mcVersion string) ([]string, string, error) {
|
||||||
return func(mcVersion string) ([]string, string, error) {
|
return func(mcVersion string) ([]string, string, error) {
|
||||||
res, err := http.Get(url)
|
res, err := GetWithUA(url, "application/xml")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return []string{}, "", err
|
return []string{}, "", err
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,6 @@ import (
|
|||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
"golang.org/x/exp/slices"
|
"golang.org/x/exp/slices"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
@ -540,8 +539,7 @@ func (m *cfDownloadMetadata) GetManualDownload() (bool, core.ManualDownload) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *cfDownloadMetadata) DownloadFile() (io.ReadCloser, error) {
|
func (m *cfDownloadMetadata) DownloadFile() (io.ReadCloser, error) {
|
||||||
resp, err := http.Get(m.url)
|
resp, err := core.GetWithUA(m.url, "application/octet-stream")
|
||||||
// TODO: content type, user-agent?
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to download %s: %w", m.url, err)
|
return nil, fmt.Errorf("failed to download %s: %w", m.url, err)
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/packwiz/packwiz/core"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
@ -40,8 +41,7 @@ func (c *cfApiClient) makeGet(endpoint string) (*http.Response, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: make this configurable application-wide
|
req.Header.Set("User-Agent", core.UserAgent)
|
||||||
req.Header.Set("User-Agent", "packwiz/packwiz client")
|
|
||||||
req.Header.Set("Accept", "application/json")
|
req.Header.Set("Accept", "application/json")
|
||||||
if cfApiKey == "" {
|
if cfApiKey == "" {
|
||||||
cfApiKey = decodeDefaultKey()
|
cfApiKey = decodeDefaultKey()
|
||||||
@ -65,8 +65,7 @@ func (c *cfApiClient) makePost(endpoint string, body io.Reader) (*http.Response,
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: make this configurable application-wide
|
req.Header.Set("User-Agent", core.UserAgent)
|
||||||
req.Header.Set("User-Agent", "packwiz/packwiz client")
|
|
||||||
req.Header.Set("Accept", "application/json")
|
req.Header.Set("Accept", "application/json")
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
if cfApiKey == "" {
|
if cfApiKey == "" {
|
||||||
|
7
go.mod
7
go.mod
@ -26,7 +26,11 @@ require (
|
|||||||
gopkg.in/dixonwille/wmenu.v4 v4.0.2
|
gopkg.in/dixonwille/wmenu.v4 v4.0.2
|
||||||
)
|
)
|
||||||
|
|
||||||
require golang.org/x/exp v0.0.0-20220414153411-bcd21879b8fd
|
require (
|
||||||
|
codeberg.org/jmansfield/go-modrinth v0.4.1
|
||||||
|
github.com/spf13/pflag v1.0.5
|
||||||
|
golang.org/x/exp v0.0.0-20220414153411-bcd21879b8fd
|
||||||
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect
|
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect
|
||||||
@ -40,7 +44,6 @@ require (
|
|||||||
github.com/spf13/afero v1.8.2 // indirect
|
github.com/spf13/afero v1.8.2 // indirect
|
||||||
github.com/spf13/cast v1.4.1 // indirect
|
github.com/spf13/cast v1.4.1 // indirect
|
||||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
|
||||||
github.com/subosito/gotenv v1.2.0 // indirect
|
github.com/subosito/gotenv v1.2.0 // indirect
|
||||||
gopkg.in/ini.v1 v1.66.4 // indirect
|
gopkg.in/ini.v1 v1.66.4 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
|
2
go.sum
2
go.sum
@ -35,6 +35,8 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl
|
|||||||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||||
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
|
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
|
||||||
|
codeberg.org/jmansfield/go-modrinth v0.4.1 h1:uCWqjep41jplMLLyarQ6rvD+4FyFIBsa5UcPfZWOGTo=
|
||||||
|
codeberg.org/jmansfield/go-modrinth v0.4.1/go.mod h1:feVF2NqtWdzIpCMgUT/Q/Wb8CpEVpi64m4TJXB+ejq0=
|
||||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I=
|
github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I=
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package modrinth
|
package modrinth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
modrinthApi "codeberg.org/jmansfield/go-modrinth/modrinth"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
@ -69,7 +70,7 @@ var installCmd = &cobra.Command{
|
|||||||
modStr = args[0]
|
modStr = args[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
mod, err := fetchMod(modStr)
|
mod, err := mrDefaultClient.Projects.Get(modStr)
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
//We found a mod with that id/slug
|
//We found a mod with that id/slug
|
||||||
@ -111,7 +112,8 @@ func installViaSearch(query string, pack core.Pack) error {
|
|||||||
menu := wmenu.NewMenu("Choose a number:")
|
menu := wmenu.NewMenu("Choose a number:")
|
||||||
menu.Option("Cancel", nil, false, nil)
|
menu.Option("Cancel", nil, false, nil)
|
||||||
for i, v := range results {
|
for i, v := range results {
|
||||||
menu.Option(v.Title, v, i == 0, nil)
|
// Should be non-nil (Title is a required field)
|
||||||
|
menu.Option(*v.Title, v, i == 0, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
menu.Action(func(menuRes []wmenu.Opt) error {
|
menu.Action(func(menuRes []wmenu.Opt) error {
|
||||||
@ -120,15 +122,13 @@ func installViaSearch(query string, pack core.Pack) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//Get the selected mod
|
//Get the selected mod
|
||||||
selectedMod, ok := menuRes[0].Value.(ModResult)
|
selectedMod, ok := menuRes[0].Value.(*modrinthApi.SearchResult)
|
||||||
if !ok {
|
if !ok {
|
||||||
return errors.New("error converting interface from wmenu")
|
return errors.New("error converting interface from wmenu")
|
||||||
}
|
}
|
||||||
|
|
||||||
//Install the selected mod
|
//Install the selected mod
|
||||||
modId := strings.TrimPrefix(selectedMod.ModID, "local-")
|
mod, err := mrDefaultClient.Projects.Get(*selectedMod.ProjectID)
|
||||||
|
|
||||||
mod, err := fetchMod(modId)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -139,21 +139,21 @@ func installViaSearch(query string, pack core.Pack) error {
|
|||||||
return menu.Run()
|
return menu.Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
func installMod(mod Mod, pack core.Pack) error {
|
func installMod(mod *modrinthApi.Project, pack core.Pack) error {
|
||||||
fmt.Printf("Found mod %s: '%s'.\n", mod.Title, mod.Description)
|
fmt.Printf("Found mod %s: '%s'.\n", *mod.Title, *mod.Description)
|
||||||
|
|
||||||
latestVersion, err := getLatestVersion(mod.ID, pack)
|
latestVersion, err := getLatestVersion(*mod.ID, pack)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get latest version: %v", err)
|
return fmt.Errorf("failed to get latest version: %v", err)
|
||||||
}
|
}
|
||||||
if latestVersion.ID == "" {
|
if latestVersion.ID == nil {
|
||||||
return errors.New("mod is not available for this Minecraft version (use the acceptable-game-versions option to accept more) or mod loader")
|
return errors.New("mod is not available for this Minecraft version (use the acceptable-game-versions option to accept more) or mod loader")
|
||||||
}
|
}
|
||||||
|
|
||||||
return installVersion(mod, latestVersion, pack)
|
return installVersion(mod, latestVersion, pack)
|
||||||
}
|
}
|
||||||
|
|
||||||
func installVersion(mod Mod, version Version, pack core.Pack) error {
|
func installVersion(mod *modrinthApi.Project, version *modrinthApi.Version, pack core.Pack) error {
|
||||||
var files = version.Files
|
var files = version.Files
|
||||||
|
|
||||||
if len(files) == 0 {
|
if len(files) == 0 {
|
||||||
@ -164,13 +164,13 @@ func installVersion(mod Mod, version Version, pack core.Pack) error {
|
|||||||
var file = files[0]
|
var file = files[0]
|
||||||
// Prefer the primary file
|
// Prefer the primary file
|
||||||
for _, v := range files {
|
for _, v := range files {
|
||||||
if v.Primary {
|
if *v.Primary {
|
||||||
file = v
|
file = v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//Install the file
|
//Install the file
|
||||||
fmt.Printf("Installing %s from version %s\n", file.Filename, version.VersionNumber)
|
fmt.Printf("Installing %s from version %s\n", *file.Filename, *version.VersionNumber)
|
||||||
index, err := pack.LoadIndex()
|
index, err := pack.LoadIndex()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -179,29 +179,29 @@ func installVersion(mod Mod, version Version, pack core.Pack) error {
|
|||||||
updateMap := make(map[string]map[string]interface{})
|
updateMap := make(map[string]map[string]interface{})
|
||||||
|
|
||||||
updateMap["modrinth"], err = mrUpdateData{
|
updateMap["modrinth"], err = mrUpdateData{
|
||||||
ModID: mod.ID,
|
ModID: *mod.ID,
|
||||||
InstalledVersion: version.ID,
|
InstalledVersion: *version.ID,
|
||||||
}.ToMap()
|
}.ToMap()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
side := mod.getSide()
|
side := getSide(mod)
|
||||||
if side == "" {
|
if side == "" {
|
||||||
return errors.New("version doesn't have a side that's supported. Server: " + mod.ServerSide + " Client: " + mod.ClientSide)
|
return errors.New("version doesn't have a side that's supported. Server: " + *mod.ServerSide + " Client: " + *mod.ClientSide)
|
||||||
}
|
}
|
||||||
|
|
||||||
algorithm, hash := file.getBestHash()
|
algorithm, hash := getBestHash(file)
|
||||||
if algorithm == "" {
|
if algorithm == "" {
|
||||||
return errors.New("file doesn't have a hash")
|
return errors.New("file doesn't have a hash")
|
||||||
}
|
}
|
||||||
|
|
||||||
modMeta := core.Mod{
|
modMeta := core.Mod{
|
||||||
Name: mod.Title,
|
Name: *mod.Title,
|
||||||
FileName: file.Filename,
|
FileName: *file.Filename,
|
||||||
Side: side,
|
Side: side,
|
||||||
Download: core.ModDownload{
|
Download: core.ModDownload{
|
||||||
URL: file.Url,
|
URL: *file.URL,
|
||||||
HashFormat: algorithm,
|
HashFormat: algorithm,
|
||||||
Hash: hash,
|
Hash: hash,
|
||||||
},
|
},
|
||||||
@ -212,10 +212,10 @@ func installVersion(mod Mod, version Version, pack core.Pack) error {
|
|||||||
if folder == "" {
|
if folder == "" {
|
||||||
folder = "mods"
|
folder = "mods"
|
||||||
}
|
}
|
||||||
if mod.Slug != "" {
|
if mod.Slug != nil {
|
||||||
path = modMeta.SetMetaPath(filepath.Join(viper.GetString("meta-folder-base"), folder, mod.Slug+core.MetaExtension))
|
path = modMeta.SetMetaPath(filepath.Join(viper.GetString("meta-folder-base"), folder, *mod.Slug+core.MetaExtension))
|
||||||
} else {
|
} else {
|
||||||
path = modMeta.SetMetaPath(filepath.Join(viper.GetString("meta-folder-base"), folder, mod.Title+core.MetaExtension))
|
path = modMeta.SetMetaPath(filepath.Join(viper.GetString("meta-folder-base"), folder, *mod.Title+core.MetaExtension))
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the file already exists, this will overwrite it!!!
|
// If the file already exists, this will overwrite it!!!
|
||||||
@ -247,14 +247,14 @@ func installVersion(mod Mod, version Version, pack core.Pack) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func installVersionById(versionId string, pack core.Pack) error {
|
func installVersionById(versionId string, pack core.Pack) error {
|
||||||
version, err := fetchVersion(versionId)
|
version, err := mrDefaultClient.Versions.Get(versionId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to fetch version %s: %v", versionId, err)
|
return fmt.Errorf("failed to fetch version %s: %v", versionId, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
mod, err := fetchMod(version.ModID)
|
mod, err := mrDefaultClient.Projects.Get(*version.ProjectID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to fetch mod %s: %v", version.ModID, err)
|
return fmt.Errorf("failed to fetch mod %s: %v", *version.ProjectID, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return installVersion(mod, version, pack)
|
return installVersion(mod, version, pack)
|
||||||
|
@ -1,227 +1,72 @@
|
|||||||
package modrinth
|
package modrinth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
modrinthApi "codeberg.org/jmansfield/go-modrinth/modrinth"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
"golang.org/x/exp/slices"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/Masterminds/semver/v3"
|
"github.com/Masterminds/semver/v3"
|
||||||
"github.com/packwiz/packwiz/cmd"
|
"github.com/packwiz/packwiz/cmd"
|
||||||
"github.com/packwiz/packwiz/core"
|
"github.com/packwiz/packwiz/core"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
const modrinthApiUrl = "https://api.modrinth.com/api/v1/"
|
|
||||||
|
|
||||||
var modrinthApiUrlParsed, _ = url.Parse(modrinthApiUrl)
|
|
||||||
|
|
||||||
var modrinthCmd = &cobra.Command{
|
var modrinthCmd = &cobra.Command{
|
||||||
Use: "modrinth",
|
Use: "modrinth",
|
||||||
Aliases: []string{"mr"},
|
Aliases: []string{"mr"},
|
||||||
Short: "Manage modrinth-based mods",
|
Short: "Manage modrinth-based mods",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var mrDefaultClient = modrinthApi.NewClient(&http.Client{})
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
cmd.Add(modrinthCmd)
|
cmd.Add(modrinthCmd)
|
||||||
core.Updaters["modrinth"] = mrUpdater{}
|
core.Updaters["modrinth"] = mrUpdater{}
|
||||||
|
|
||||||
|
mrDefaultClient.UserAgent = core.UserAgent
|
||||||
}
|
}
|
||||||
|
|
||||||
type License struct {
|
func getModIdsViaSearch(query string, versions []string) ([]*modrinthApi.SearchResult, error) {
|
||||||
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 uint32 `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 uint32 `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 uint32 `json:"offset"` //The number of results that were skipped by the query
|
|
||||||
Limit uint32 `json:"limit"` //The number of mods returned by the query
|
|
||||||
TotalHits uint32 `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 uint32 `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)
|
facets := make([]string, 0)
|
||||||
for _, v := range versions {
|
for _, v := range versions {
|
||||||
facets = append(facets, "versions:"+v)
|
facets = append(facets, "versions:"+v)
|
||||||
}
|
}
|
||||||
facetsEncoded, err := json.Marshal(facets)
|
|
||||||
|
res, err := mrDefaultClient.Projects.Search(&modrinthApi.SearchOptions{
|
||||||
|
Limit: 5,
|
||||||
|
Index: "relevance",
|
||||||
|
// Filters by mod since currently only mods and modpacks are supported by Modrinth
|
||||||
|
Facets: [][]string{facets, {"project_type:mod"}},
|
||||||
|
Query: query,
|
||||||
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return []ModResult{}, err
|
return nil, err
|
||||||
}
|
}
|
||||||
params.Add("facets", "["+string(facetsEncoded)+"]")
|
return res.Hits, nil
|
||||||
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) {
|
func getLatestVersion(modID string, pack core.Pack) (*modrinthApi.Version, error) {
|
||||||
mcVersion, err := pack.GetMCVersion()
|
mcVersion, err := pack.GetMCVersion()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Version{}, err
|
return nil, err
|
||||||
}
|
}
|
||||||
gameVersions := append([]string{mcVersion}, viper.GetStringSlice("acceptable-game-versions")...)
|
gameVersions := append([]string{mcVersion}, viper.GetStringSlice("acceptable-game-versions")...)
|
||||||
gameVersionsEncoded, err := json.Marshal(gameVersions)
|
|
||||||
if err != nil {
|
|
||||||
return Version{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
loadersEncoded, err := json.Marshal(pack.GetLoaders())
|
result, err := mrDefaultClient.Versions.ListVersions(modID, modrinthApi.ListVersionsOptions{
|
||||||
if err != nil {
|
GameVersions: gameVersions,
|
||||||
return Version{}, err
|
Loaders: pack.GetLoaders(),
|
||||||
}
|
})
|
||||||
|
|
||||||
baseUrl := *modrinthApiUrlParsed
|
|
||||||
baseUrl.Path += "mod/"
|
|
||||||
baseUrl.Path += modID
|
|
||||||
baseUrl.Path += "/version"
|
|
||||||
|
|
||||||
params := url.Values{}
|
|
||||||
params.Add("game_versions", string(gameVersionsEncoded))
|
|
||||||
params.Add("loaders", string(loadersEncoded))
|
|
||||||
|
|
||||||
baseUrl.RawQuery = params.Encode()
|
|
||||||
|
|
||||||
resp, err := http.Get(baseUrl.String())
|
|
||||||
if err != nil {
|
|
||||||
return Version{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.StatusCode == 404 {
|
|
||||||
return Version{}, fmt.Errorf("mod not found (for URL %v)", baseUrl)
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.StatusCode != 200 {
|
|
||||||
return Version{}, fmt.Errorf("invalid response status %v for URL %v", resp.Status, baseUrl)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
if len(result) == 0 {
|
||||||
return Version{}, errors.New("no valid versions found")
|
return nil, errors.New("no valid versions found")
|
||||||
}
|
}
|
||||||
|
|
||||||
latestValidVersion := result[0]
|
latestValidVersion := result[0]
|
||||||
for _, v := range result[1:] {
|
for _, v := range result[1:] {
|
||||||
currVersion, err1 := semver.NewVersion(v.VersionNumber)
|
currVersion, err1 := semver.NewVersion(*v.VersionNumber)
|
||||||
latestVersion, err2 := semver.NewVersion(latestValidVersion.VersionNumber)
|
latestVersion, err2 := semver.NewVersion(*latestValidVersion.VersionNumber)
|
||||||
var semverCompare = 0
|
var semverCompare = 0
|
||||||
// Only compare with semver if both are valid semver - otherwise compare by release date
|
// Only compare with semver if both are valid semver - otherwise compare by release date
|
||||||
if err1 == nil && err2 == nil {
|
if err1 == nil && err2 == nil {
|
||||||
@ -236,9 +81,7 @@ func getLatestVersion(modID string, pack core.Pack) (Version, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//Semver is equal, compare date instead
|
//Semver is equal, compare date instead
|
||||||
vDate, _ := time.Parse(time.RFC3339Nano, v.DatePublished)
|
if v.DatePublished.After(*latestValidVersion.DatePublished) {
|
||||||
latestDate, _ := time.Parse(time.RFC3339Nano, latestValidVersion.DatePublished)
|
|
||||||
if vDate.After(latestDate) {
|
|
||||||
latestValidVersion = v
|
latestValidVersion = v
|
||||||
}
|
}
|
||||||
} else if semverCompare == 1 {
|
} else if semverCompare == 1 {
|
||||||
@ -249,77 +92,9 @@ func getLatestVersion(modID string, pack core.Pack) (Version, error) {
|
|||||||
return latestValidVersion, nil
|
return latestValidVersion, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchMod(modID string) (Mod, error) {
|
func getSide(mod *modrinthApi.Project) string {
|
||||||
var mod Mod
|
server := shouldDownloadOnSide(*mod.ServerSide)
|
||||||
|
client := shouldDownloadOnSide(*mod.ClientSide)
|
||||||
resp, err := http.Get(modrinthApiUrl + "mod/" + modID)
|
|
||||||
if err != nil {
|
|
||||||
return mod, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.StatusCode == 404 {
|
|
||||||
return mod, fmt.Errorf("mod not found (for URL %v)", modrinthApiUrl+"mod/"+modID)
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.StatusCode != 200 {
|
|
||||||
return mod, fmt.Errorf("invalid response status %v for URL %v", resp.Status, modrinthApiUrl+"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, fmt.Errorf("version not found (for URL %v)", modrinthApiUrl+"version/"+versionId)
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.StatusCode != 200 {
|
|
||||||
return version, fmt.Errorf("invalid response status %v for URL %v", resp.Status, modrinthApiUrl+"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 {
|
if server && client {
|
||||||
return core.UniversalSide
|
return core.UniversalSide
|
||||||
@ -336,7 +111,7 @@ func shouldDownloadOnSide(side string) bool {
|
|||||||
return side == "required" || side == "optional"
|
return side == "required" || side == "optional"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v VersionFile) getBestHash() (string, string) {
|
func getBestHash(v *modrinthApi.File) (string, string) {
|
||||||
// Try preferred hashes first; SHA1 is first as it is required for Modrinth pack exporting
|
// Try preferred hashes first; SHA1 is first as it is required for Modrinth pack exporting
|
||||||
val, exists := v.Hashes["sha1"]
|
val, exists := v.Hashes["sha1"]
|
||||||
if exists {
|
if exists {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package modrinth
|
package modrinth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
modrinthApi "codeberg.org/jmansfield/go-modrinth/modrinth"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
@ -29,7 +30,7 @@ func (u mrUpdater) ParseUpdate(updateUnparsed map[string]interface{}) (interface
|
|||||||
|
|
||||||
type cachedStateStore struct {
|
type cachedStateStore struct {
|
||||||
ModID string
|
ModID string
|
||||||
Version Version
|
Version *modrinthApi.Version
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u mrUpdater) CheckUpdate(mods []core.Mod, mcVersion string, pack core.Pack) ([]core.UpdateCheck, error) {
|
func (u mrUpdater) CheckUpdate(mods []core.Mod, mcVersion string, pack core.Pack) ([]core.UpdateCheck, error) {
|
||||||
@ -50,12 +51,7 @@ func (u mrUpdater) CheckUpdate(mods []core.Mod, mcVersion string, pack core.Pack
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if newVersion.ID == "" { //There is no version available for this minecraft version or loader.
|
if *newVersion.ID == data.InstalledVersion { //The latest version from the site is the same as the installed one
|
||||||
results[i] = core.UpdateCheck{UpdateAvailable: false}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if newVersion.ID == data.InstalledVersion { //The latest version from the site is the same as the installed one
|
|
||||||
results[i] = core.UpdateCheck{UpdateAvailable: false}
|
results[i] = core.UpdateCheck{UpdateAvailable: false}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -68,14 +64,14 @@ func (u mrUpdater) CheckUpdate(mods []core.Mod, mcVersion string, pack core.Pack
|
|||||||
newFilename := newVersion.Files[0].Filename
|
newFilename := newVersion.Files[0].Filename
|
||||||
// Prefer the primary file
|
// Prefer the primary file
|
||||||
for _, v := range newVersion.Files {
|
for _, v := range newVersion.Files {
|
||||||
if v.Primary {
|
if *v.Primary {
|
||||||
newFilename = v.Filename
|
newFilename = v.Filename
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
results[i] = core.UpdateCheck{
|
results[i] = core.UpdateCheck{
|
||||||
UpdateAvailable: true,
|
UpdateAvailable: true,
|
||||||
UpdateString: mod.FileName + " -> " + newFilename,
|
UpdateString: mod.FileName + " -> " + *newFilename,
|
||||||
CachedState: cachedStateStore{data.ModID, newVersion},
|
CachedState: cachedStateStore{data.ModID, newVersion},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -91,19 +87,19 @@ func (u mrUpdater) DoUpdate(mods []*core.Mod, cachedState []interface{}) error {
|
|||||||
var file = version.Files[0]
|
var file = version.Files[0]
|
||||||
// Prefer the primary file
|
// Prefer the primary file
|
||||||
for _, v := range version.Files {
|
for _, v := range version.Files {
|
||||||
if v.Primary {
|
if *v.Primary {
|
||||||
file = v
|
file = v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
algorithm, hash := file.getBestHash()
|
algorithm, hash := getBestHash(file)
|
||||||
if algorithm == "" {
|
if algorithm == "" {
|
||||||
return errors.New("file for mod " + mod.Name + " doesn't have a hash")
|
return errors.New("file for mod " + mod.Name + " doesn't have a hash")
|
||||||
}
|
}
|
||||||
|
|
||||||
mod.FileName = file.Filename
|
mod.FileName = *file.Filename
|
||||||
mod.Download = core.ModDownload{
|
mod.Download = core.ModDownload{
|
||||||
URL: file.Url,
|
URL: *file.URL,
|
||||||
HashFormat: algorithm,
|
HashFormat: algorithm,
|
||||||
Hash: hash,
|
Hash: hash,
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user