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