// Package api holds functions to interact with the artifacts API package api import ( "bytes" "encoding/json" "errors" "fmt" "io" "net/http" "net/url" "os" "slices" ) // custom error codes var ( ErrTokenNotProvided = errors.New("make sure to provide a token via the api.SetToken() function, or set it via env variable ARTIFACTS_TOKEN") ) const ( baseURL = "https://api.artifactsmmo.com" ) var token string // the token must be provided for the client to work type apiResponse struct { Data json.RawMessage `json:"data"` Total int `json:"total"` Page int `json:"page"` Size int `json:"size"` Pages int `json:"pages"` } // SetToken can be used to set the token, which must be provided before calling any other function. // It can be provided using this function or via environment variable 'ARTIFACTS_TOKEN'. func SetToken(t string) { token = t } func meta[T any](endpoint string, method string, body any, responseCodes map[int]string) (*T, error) { // make it possible to set a token via env variable if token == "" && os.Getenv("ARTIFACTS_TOKEN") != "" { SetToken(os.Getenv("ARTIFACTS_TOKEN")) } // check for token if token == "" { return nil, ErrTokenNotProvided } bodyBytes, err := json.Marshal(body) if err != nil { return nil, err } // create a new request to retrieve characters request, err := http.NewRequest(method, baseURL+endpoint, bytes.NewReader(bodyBytes)) if err != nil { return nil, err } // add auth header to the request request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) request.Header.Set("Content-Type", "application/json") // execute this request response, err := http.DefaultClient.Do(request) if err != nil { return nil, err } // check all additional status codes first for code, message := range responseCodes { if response.StatusCode != code { continue } // exit the function if a statuscode was found and return it's message return nil, fmt.Errorf("%d: %s", code, message) } // basic error handling if response.StatusCode != 200 { return nil, fmt.Errorf("%s", response.Status) } // read in the body of the response b, err := io.ReadAll(response.Body) if err != nil { return nil, err } // close the body when we are finished defer response.Body.Close() // convert it to a native go type var ar apiResponse if err := json.Unmarshal(b, &ar); err != nil { return nil, err } if ar.Page < ar.Pages { // parse endpoint u, err := url.Parse(endpoint) if err != nil { return nil, err } urlValues := u.Query() urlValues.Set("page", fmt.Sprintf("%d", ar.Page+1)) endpoint = u.Path nextPage, err := meta[T](fmt.Sprintf("%s?%s", endpoint, urlValues.Encode()), method, body, responseCodes) if err != nil { return nil, err } b1, err := ar.Data.MarshalJSON() if err != nil { return nil, err } b2, err := json.Marshal(nextPage) if err != nil { return nil, err } bCombined := slices.Concat(b1[0:len(b1)-1], []byte(","), b2[1:]) // copy combined dataset into ar.Data ar.Data.UnmarshalJSON(bCombined) // combine both things } d, err := ar.Data.MarshalJSON() if err != nil { return nil, err } // no error occurred var r T if err := json.Unmarshal(d, &r); err != nil { return nil, err } return &r, nil } // Post is used to call an endpoint using a POST request, responseCodes contains all possible // error codes that the API can respond with other than 200 OK func Post[T any](endpoint string, body any, responseCodes map[int]string) (output T, err error) { // use meta function to get a response response, err := meta[T](endpoint, http.MethodPost, body, responseCodes) if err != nil { return output, err } // return the data alongside any error that might have occurred return *response, err } // Get is used to call an endpoint using a GET request, responseCodes contains all possible // error codes that the API can respond with other than 200 OK func Get[T any](endpoint string, responseCodes map[int]string) (output T, err error) { // use meta function to get a response response, err := meta[T](endpoint, http.MethodGet, nil, responseCodes) if err != nil { return output, err } // return the data alongside any error that might have occurred return *response, err }