z_mirror/fmm

git clone https://git.raiguard.me/z_mirror/fmm

Caleb HeuerInitial WIP811753c

updating
6.7 KiB285 linesraw
1package fmm
2
3import (
4	"bytes"
5	"encoding/json"
6	"errors"
7	"fmt"
8	"io"
9	"mime/multipart"
10	"net/http"
11	"net/url"
12	"os"
13	"path"
14
15	"github.com/cavaliergopher/grab/v3"
16)
17
18type ModPortal struct {
19	apiKey       string
20	baseVersion  *Version
21	downloadPath string
22	mods         map[string]*PortalModInfo
23	playerData   PlayerData
24	server       string
25}
26
27// // GetBulkModInfo fetches information for all of the given mods from the mod portal.
28// func (p *ModPortal) GetBulkModInfo(mods []ModIdent) error {
29// 	fmt.Println("fetching mod info")
30// 	modNames := []string{}
31// 	for _, mod := range mods {
32// 		modNames = append(modNames, mod.Name)
33// 	}
34// 	namelist := url.QueryEscape(strings.Join(modNames, ","))
35
36// 	path, err := url.JoinPath(p.server, "api/mods")
37// 	if err != nil {
38// 		return err
39// 	}
40
41// 	bodyContent := struct {
42// 		Namelist string `json:"namelist"`
43// 	}{namelist}
44// 	bodyBytes, err := json.Marshal(bodyContent)
45// 	if err != nil {
46// 		return err
47// 	}
48
49// 	fmt.Println(path)
50// 	req, err := http.NewRequest(http.MethodGet, path, bytes.NewReader(bodyBytes))
51// 	if err != nil {
52// 		return err
53// 	}
54// 	// q := req.URL.Query()
55// 	// q.Add("page_size", "max")
56// 	// req.URL.RawQuery = q.Encode()
57// 	res, err := http.DefaultClient.Do(req)
58// 	if err != nil {
59// 		return err
60// 	}
61// 	if res.StatusCode != http.StatusOK {
62// 		return errors.New(fmt.Sprintf("error code %d", res.StatusCode))
63// 	}
64
65// 	body, err := io.ReadAll(res.Body)
66// 	if err != nil {
67// 		return err
68// 	}
69// 	res.Body.Close()
70
71// 	fmt.Println(string(body))
72
73// 	var modInfos PortalBulkModInfo
74// 	err = json.Unmarshal(body, &modInfos)
75// 	if err != nil {
76// 		return err
77// 	}
78
79// 	for _, result := range modInfos.Results {
80// 		p.mods[result.Name] = &result
81// 	}
82
83// 	return nil
84// }
85
86// GetModInfo fetches information for the given mod from the mod portal.
87func (p *ModPortal) GetModInfo(name string) (*PortalModInfo, error) {
88	if mod := p.mods[name]; mod != nil {
89		return mod, nil
90	}
91
92	fmt.Println("fetching info for", name) // TODO: Relocate this
93	url, err := url.JoinPath(p.server, "api/mods", name, "full")
94	if err != nil {
95		return nil, err
96	}
97	res, err := http.Get(url)
98	if err != nil {
99		return nil, err
100	}
101	if res.StatusCode != http.StatusOK {
102		return nil, errors.New(fmt.Sprintf("%s was not found on the mod portal", name))
103	}
104
105	body, err := io.ReadAll(res.Body)
106	if err != nil {
107		return nil, err
108	}
109	res.Body.Close()
110
111	var mod PortalModInfo
112	err = json.Unmarshal(body, &mod)
113	if err != nil {
114		return nil, err
115	}
116
117	p.mods[name] = &mod
118
119	return &mod, nil
120}
121
122// GetMatchingRelease fetches information for the newest release matching the given dependency.
123func (p *ModPortal) GetMatchingRelease(dep *Dependency) (*PortalModRelease, error) {
124	mod, err := p.GetModInfo(dep.Name)
125	if err != nil {
126		return nil, err
127	}
128	// Iterate backwards to get the newest release first
129	for i := len(mod.Releases) - 1; i >= 0; i-- {
130		release := &mod.Releases[i]
131		if dep.Test(&release.Version) && release.compatibleWithBaseVersion(p.baseVersion) {
132			return release, nil
133		}
134	}
135
136	return nil, ErrNoCompatibleRelease
137}
138
139// DownloadMatchingRelease downloads the latest mod release matching the given dependency.
140// Returns the filepath of the newly downloaded mod.
141func (p *ModPortal) DownloadMatchingRelease(dep *Dependency) (string, error) {
142	if p.playerData.Token == "" {
143		return "", errors.New("token was not specified")
144	}
145	if p.playerData.Username == "" {
146		return "", errors.New("username was not specified")
147	}
148	release, err := p.GetMatchingRelease(dep)
149	if err != nil {
150		return "", err
151	}
152
153	downloadUrl := fmt.Sprintf(
154		"%s/%s?username=%s&token=%s",
155		p.server,
156		release.DownloadUrl,
157		p.playerData.Username,
158		p.playerData.Token,
159	)
160	outPath := path.Join(p.downloadPath, release.FileName)
161
162	fmt.Printf("downloading %s\n", release.FileName) // TODO: This doesn't belong here
163	res, err := grab.Get(outPath, downloadUrl)
164	if err != nil {
165		return "", err
166	}
167	return res.Filename, nil
168}
169
170// DownloadLatestRelease downloads the latest release compatible with the current base version.
171func (p *ModPortal) DownloadLatestRelease(name string) (string, error) {
172	return p.DownloadMatchingRelease(&Dependency{
173		Name: name,
174		Kind: DependencyRequired,
175		Req:  VersionAny,
176	})
177}
178
179func (p *ModPortal) DownloadRelease(name string, version *Version) (string, error) {
180	if version == nil {
181		return p.DownloadLatestRelease(name)
182	}
183	return p.DownloadMatchingRelease(&Dependency{
184		Name:    name,
185		Version: version,
186		Kind:    DependencyRequired,
187		Req:     VersionEq,
188	})
189}
190
191// UploadMod uploads the given file to the mod portal.
192func (p *ModPortal) UploadMod(filepath string) error {
193	// Init upload
194	initUploadBody := &bytes.Buffer{}
195	w := multipart.NewWriter(initUploadBody)
196	ident := NewModIdent(path.Base(filepath))
197	w.WriteField("mod", ident.Name)
198	w.Close()
199	url, err := url.JoinPath(p.server, "api/v2/mods/releases/init_upload")
200	if err != nil {
201		return err
202	}
203	req, err := http.NewRequest(http.MethodPost, url, initUploadBody)
204	if err != nil {
205		return err
206	}
207	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", p.apiKey))
208	req.Header.Set("Content-Type", w.FormDataContentType())
209	res, err := http.DefaultClient.Do(req)
210	if err != nil {
211		return err
212	}
213	var decoded ModInitUploadRes
214	err = json.NewDecoder(res.Body).Decode(&decoded)
215	if err != nil {
216		return err
217	}
218	if res.StatusCode != http.StatusOK {
219		return errors.New(*decoded.Message)
220	}
221	defer res.Body.Close()
222
223	// Open file
224	file, err := os.Open(filepath)
225	if err != nil {
226		return err
227	}
228	defer file.Close()
229
230	fmt.Printf("uploading %s\n", filepath) // TODO: Relocate this
231
232	// Upload file
233	uploadBody := &bytes.Buffer{}
234	w = multipart.NewWriter(uploadBody)
235	part, err := w.CreateFormFile("file", path.Base(file.Name()))
236	io.Copy(part, file)
237	w.Close()
238
239	r, err := http.NewRequest("POST", *decoded.UploadUrl, uploadBody)
240	if err != nil {
241		return err
242	}
243	r.Header.Add("Content-Type", w.FormDataContentType())
244	http.DefaultClient.Do(r)
245
246	return nil
247}
248
249type ModInitUploadRes struct {
250	UploadUrl *string `json:"upload_url"`
251	Message   *string // When an error occurs
252}
253
254type PortalBulkModInfo struct {
255	Results []PortalModInfo `json:"results"`
256}
257
258type PortalModInfo struct {
259	Name     string
260	Releases []PortalModRelease
261	Title    string
262}
263
264type PortalModRelease struct {
265	DownloadUrl string   `json:"download_url"`
266	FileName    string   `json:"file_name"`
267	InfoJson    infoJson `json:"info_json"`
268	Version     Version  `json:"version"`
269}
270
271func (r *PortalModRelease) compatibleWithBaseVersion(baseVersion *Version) bool {
272	if r.InfoJson.FactorioVersion[0] != baseVersion[0] || r.InfoJson.FactorioVersion[1] != baseVersion[1] {
273		return false
274	}
275
276	for _, dep := range r.InfoJson.Dependencies {
277		if dep.Name == "base" {
278			if dep.Version == nil {
279				return true
280			}
281			return dep.Test(baseVersion)
282		}
283	}
284	return true
285}