z_mirror/fmm

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

Caleb HeuerDon't print to console directly from ModPortale75cd02

trunk
5.5 KiB236 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 ModPortalPrintlnCallback func(...any)
19
20type ModPortal struct {
21	apiKey        string
22	baseVersion   *Version
23	downloadPath  string
24	mods          map[string]*PortalModInfo
25	playerData    PlayerData
26	server        string
27	printCallback ModPortalPrintlnCallback
28}
29
30// SetPrintlnCallback sets a function to be called when something is to be printed to the console.
31func (p *ModPortal) SetPrintlnCallback(cb ModPortalPrintlnCallback) {
32	p.printCallback = cb
33}
34
35func (p *ModPortal) println(msg ...any) {
36	if p.printCallback != nil {
37		p.printCallback(msg...)
38	}
39}
40
41// GetModInfo fetches information for the given mod from the mod portal.
42func (p *ModPortal) GetModInfo(name string) (*PortalModInfo, error) {
43	if mod := p.mods[name]; mod != nil {
44		return mod, nil
45	}
46
47	p.println("fetching info for", name)
48	url, err := url.JoinPath(p.server, "api/mods", name, "full")
49	if err != nil {
50		return nil, err
51	}
52	res, err := http.Get(url)
53	if err != nil {
54		return nil, err
55	}
56	if res.StatusCode != http.StatusOK {
57		return nil, errors.New(fmt.Sprintf("%s was not found on the mod portal", name))
58	}
59
60	body, err := io.ReadAll(res.Body)
61	if err != nil {
62		return nil, err
63	}
64	res.Body.Close()
65
66	var mod PortalModInfo
67	err = json.Unmarshal(body, &mod)
68	if err != nil {
69		return nil, err
70	}
71
72	p.mods[name] = &mod
73
74	return &mod, nil
75}
76
77// GetMatchingRelease fetches information for the newest release matching the given dependency.
78func (p *ModPortal) GetMatchingRelease(dep *Dependency) (*PortalModRelease, error) {
79	mod, err := p.GetModInfo(dep.Name)
80	if err != nil {
81		return nil, err
82	}
83	// Iterate backwards to get the newest release first
84	for i := len(mod.Releases) - 1; i >= 0; i-- {
85		release := &mod.Releases[i]
86		if dep.Test(&release.Version) && release.compatibleWithBaseVersion(p.baseVersion) {
87			return release, nil
88		}
89	}
90
91	return nil, ErrNoCompatibleRelease
92}
93
94// DownloadMatchingRelease downloads the latest mod release matching the given dependency.
95// Returns the filepath of the newly downloaded mod.
96func (p *ModPortal) DownloadMatchingRelease(dep *Dependency) (string, error) {
97	if p.playerData.Token == "" {
98		return "", errors.New("token was not specified")
99	}
100	if p.playerData.Username == "" {
101		return "", errors.New("username was not specified")
102	}
103	release, err := p.GetMatchingRelease(dep)
104	if err != nil {
105		return "", err
106	}
107
108	downloadUrl := fmt.Sprintf(
109		"%s/%s?username=%s&token=%s",
110		p.server,
111		release.DownloadUrl,
112		p.playerData.Username,
113		p.playerData.Token,
114	)
115	outPath := path.Join(p.downloadPath, release.FileName)
116
117	p.println("downloading", release.FileName)
118	res, err := grab.Get(outPath, downloadUrl)
119	if err != nil {
120		return "", err
121	}
122	return res.Filename, nil
123}
124
125// DownloadLatestRelease downloads the latest release compatible with the current base version.
126func (p *ModPortal) DownloadLatestRelease(name string) (string, error) {
127	return p.DownloadMatchingRelease(&Dependency{
128		Name: name,
129		Kind: DependencyRequired,
130		Req:  VersionAny,
131	})
132}
133
134func (p *ModPortal) DownloadRelease(name string, version *Version) (string, error) {
135	if version == nil {
136		return p.DownloadLatestRelease(name)
137	}
138	return p.DownloadMatchingRelease(&Dependency{
139		Name:    name,
140		Version: version,
141		Kind:    DependencyRequired,
142		Req:     VersionEq,
143	})
144}
145
146// UploadMod uploads the given file to the mod portal.
147func (p *ModPortal) UploadMod(filepath string) error {
148	// Init upload
149	initUploadBody := &bytes.Buffer{}
150	w := multipart.NewWriter(initUploadBody)
151	ident := NewModIdent(path.Base(filepath))
152	w.WriteField("mod", ident.Name)
153	w.Close()
154	url, err := url.JoinPath(p.server, "api/v2/mods/releases/init_upload")
155	if err != nil {
156		return err
157	}
158	req, err := http.NewRequest(http.MethodPost, url, initUploadBody)
159	if err != nil {
160		return err
161	}
162	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", p.apiKey))
163	req.Header.Set("Content-Type", w.FormDataContentType())
164	res, err := http.DefaultClient.Do(req)
165	if err != nil {
166		return err
167	}
168	var decoded ModInitUploadRes
169	err = json.NewDecoder(res.Body).Decode(&decoded)
170	if err != nil {
171		return err
172	}
173	if res.StatusCode != http.StatusOK {
174		return errors.New(*decoded.Message)
175	}
176	defer res.Body.Close()
177
178	// Open file
179	file, err := os.Open(filepath)
180	if err != nil {
181		return err
182	}
183	defer file.Close()
184
185	p.println("uploading", filepath)
186
187	// Upload file
188	uploadBody := &bytes.Buffer{}
189	w = multipart.NewWriter(uploadBody)
190	part, err := w.CreateFormFile("file", path.Base(file.Name()))
191	io.Copy(part, file)
192	w.Close()
193
194	r, err := http.NewRequest("POST", *decoded.UploadUrl, uploadBody)
195	if err != nil {
196		return err
197	}
198	r.Header.Add("Content-Type", w.FormDataContentType())
199	http.DefaultClient.Do(r)
200
201	return nil
202}
203
204type ModInitUploadRes struct {
205	UploadUrl *string `json:"upload_url"`
206	Message   *string // When an error occurs
207}
208
209type PortalModInfo struct {
210	Name     string
211	Releases []PortalModRelease
212	Title    string
213}
214
215type PortalModRelease struct {
216	DownloadUrl string   `json:"download_url"`
217	FileName    string   `json:"file_name"`
218	InfoJson    infoJson `json:"info_json"`
219	Version     Version  `json:"version"`
220}
221
222func (r *PortalModRelease) compatibleWithBaseVersion(baseVersion *Version) bool {
223	if r.InfoJson.FactorioVersion[0] != baseVersion[0] || r.InfoJson.FactorioVersion[1] != baseVersion[1] {
224		return false
225	}
226
227	for _, dep := range r.InfoJson.Dependencies {
228		if dep.Name == "base" {
229			if dep.Version == nil {
230				return true
231			}
232			return dep.Test(baseVersion)
233		}
234	}
235	return true
236}