|
| 1 | +package auth |
| 2 | + |
| 3 | +import ( |
| 4 | + "context" |
| 5 | + "encoding/json" |
| 6 | + "fmt" |
| 7 | + "io" |
| 8 | + "net/http" |
| 9 | + "net/url" |
| 10 | + "os" |
| 11 | + "path/filepath" |
| 12 | + |
| 13 | + "github.com/gustash/freecarnival/logger" |
| 14 | +) |
| 15 | + |
| 16 | +const productInfoUrl = "https://developers.indiegala.com/get_product_info" |
| 17 | + |
| 18 | +type ProductInfo struct { |
| 19 | + Status string `json:"status"` |
| 20 | + Message string `json:"message"` |
| 21 | + |
| 22 | + GameDetails *GameDetails `json:"product_data,omitempty"` |
| 23 | +} |
| 24 | + |
| 25 | +type GameDetails struct { |
| 26 | + ExePath string `json:"exe_path,omitempty"` |
| 27 | + Args string `json:"args,omitempty"` |
| 28 | + Cwd string `json:"cwd,omitempty"` |
| 29 | +} |
| 30 | + |
| 31 | +// productInfoDir returns the directory for storing game details |
| 32 | +func productInfoDir() (string, error) { |
| 33 | + dir, err := configDir() |
| 34 | + if err != nil { |
| 35 | + return "", err |
| 36 | + } |
| 37 | + productPath := filepath.Join(dir, "product") |
| 38 | + if err := os.MkdirAll(productPath, 0o700); err != nil { |
| 39 | + return "", err |
| 40 | + } |
| 41 | + return productPath, nil |
| 42 | +} |
| 43 | + |
| 44 | +// gameDetailsPath returns the file path to the specified game's details |
| 45 | +func gameDetailsPath(slug string) (string, error) { |
| 46 | + dir, err := productInfoDir() |
| 47 | + if err != nil { |
| 48 | + return "", err |
| 49 | + } |
| 50 | + return filepath.Join(dir, fmt.Sprintf("%s.json", slug)), nil |
| 51 | +} |
| 52 | + |
| 53 | +// SaveGameDetails saves the provided GameDetails for the specified game slug to a file. |
| 54 | +func SaveGameDetails(slug string, gameDetails *GameDetails) error { |
| 55 | + path, err := gameDetailsPath(slug) |
| 56 | + if err != nil { |
| 57 | + return err |
| 58 | + } |
| 59 | + |
| 60 | + data, err := json.MarshalIndent(gameDetails, "", " ") |
| 61 | + if err != nil { |
| 62 | + return err |
| 63 | + } |
| 64 | + |
| 65 | + return os.WriteFile(path, data, 0o600) |
| 66 | +} |
| 67 | + |
| 68 | +// FetchGameDetails gets game details from the API (e.g. exe_name, args...). |
| 69 | +func FetchGameDetails(ctx context.Context, client *http.Client, slug string) (*GameDetails, error) { |
| 70 | + return getProductInfoWithUrl(ctx, client, productInfoUrl, slug) |
| 71 | +} |
| 72 | + |
| 73 | +// getProductInfoWithUrl is an internal function that calls the server |
| 74 | +// to fetch game details |
| 75 | +func getProductInfoWithUrl(ctx context.Context, client *http.Client, targetURL, slug string) (*GameDetails, error) { |
| 76 | + products, err := LoadLibrary() |
| 77 | + if err != nil { |
| 78 | + return nil, err |
| 79 | + } |
| 80 | + product := FindProductBySlug(products, slug) |
| 81 | + if product == nil { |
| 82 | + return nil, fmt.Errorf("couldn't find %s in library", slug) |
| 83 | + } |
| 84 | + |
| 85 | + endpoint, err := url.Parse(targetURL) |
| 86 | + if err != nil { |
| 87 | + return nil, err |
| 88 | + } |
| 89 | + |
| 90 | + queryParams := url.Values{} |
| 91 | + queryParams.Add("dev_id", product.Namespace) |
| 92 | + queryParams.Add("prod_name", slug) |
| 93 | + |
| 94 | + endpoint.RawQuery = queryParams.Encode() |
| 95 | + |
| 96 | + req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil) |
| 97 | + if err != nil { |
| 98 | + return nil, err |
| 99 | + } |
| 100 | + |
| 101 | + req.Header.Set("User-Agent", "galaClient") |
| 102 | + req.Header.Set("Content-Type", "application/json") |
| 103 | + |
| 104 | + resp, err := client.Do(req) |
| 105 | + if err != nil { |
| 106 | + return nil, err |
| 107 | + } |
| 108 | + defer resp.Body.Close() |
| 109 | + |
| 110 | + bodyBytes, err := io.ReadAll(resp.Body) |
| 111 | + if err != nil { |
| 112 | + return nil, err |
| 113 | + } |
| 114 | + bodyStr := string(bodyBytes) |
| 115 | + |
| 116 | + var pi ProductInfo |
| 117 | + if err := json.Unmarshal(bodyBytes, &pi); err != nil { |
| 118 | + // If JSON decoding fails, treat as hard error |
| 119 | + return nil, fmt.Errorf("failed to decode product info response: %w (body: %s)", err, bodyStr) |
| 120 | + } |
| 121 | + |
| 122 | + if resp.StatusCode == http.StatusOK && pi.Status == "success" { |
| 123 | + if pi.GameDetails == nil { |
| 124 | + return nil, fmt.Errorf("server didn't respond with game details: (body: %s)", bodyStr) |
| 125 | + } |
| 126 | + |
| 127 | + if err := SaveGameDetails(slug, pi.GameDetails); err != nil { |
| 128 | + logger.Warn("Failed to save product info", "slug", slug, "error", err) |
| 129 | + } |
| 130 | + |
| 131 | + return pi.GameDetails, nil |
| 132 | + } |
| 133 | + |
| 134 | + return nil, fmt.Errorf("fetching product info failed: %s (%s)", pi.Message, pi.Status) |
| 135 | +} |
0 commit comments