chrly/mojang/client.go

266 lines
5.8 KiB
Go

package mojang
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"sync"
)
type MojangApi struct {
http *http.Client
batchUuidsUrl string
profileUrl string
}
func NewMojangApi(
http *http.Client,
batchUuidsUrl string,
profileUrl string,
) *MojangApi {
if batchUuidsUrl == "" {
batchUuidsUrl = "https://api.mojang.com/profiles/minecraft"
}
if profileUrl == "" {
profileUrl = "https://sessionserver.mojang.com/session/minecraft/profile/"
}
if !strings.HasSuffix(profileUrl, "/") {
profileUrl += "/"
}
return &MojangApi{
http,
batchUuidsUrl,
profileUrl,
}
}
// Exchanges usernames array to array of uuids
// See https://wiki.vg/Mojang_API#Playernames_-.3E_UUIDs
func (c *MojangApi) UsernamesToUuids(usernames []string) ([]*ProfileInfo, error) {
requestBody, _ := json.Marshal(usernames)
request, err := http.NewRequest("POST", c.batchUuidsUrl, bytes.NewBuffer(requestBody))
if err != nil {
return nil, err
}
request.Header.Set("Content-Type", "application/json")
response, err := c.http.Do(request)
if err != nil {
return nil, err
}
defer response.Body.Close()
if response.StatusCode != 200 {
return nil, errorFromResponse(response)
}
var result []*ProfileInfo
body, _ := io.ReadAll(response.Body)
err = json.Unmarshal(body, &result)
if err != nil {
return nil, err
}
return result, nil
}
// Obtains textures information for provided uuid
// See https://wiki.vg/Mojang_API#UUID_-.3E_Profile_.2B_Skin.2FCape
func (c *MojangApi) UuidToTextures(uuid string, signed bool) (*SignedTexturesResponse, error) {
normalizedUuid := strings.ReplaceAll(uuid, "-", "")
url := c.profileUrl + normalizedUuid
if signed {
url += "?unsigned=false"
}
request, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
response, err := c.http.Do(request)
if err != nil {
return nil, err
}
defer response.Body.Close()
if response.StatusCode == 204 {
return nil, nil
}
if response.StatusCode != 200 {
return nil, errorFromResponse(response)
}
var result *SignedTexturesResponse
body, _ := io.ReadAll(response.Body)
err = json.Unmarshal(body, &result)
if err != nil {
return nil, err
}
return result, nil
}
type SignedTexturesResponse struct {
Id string `json:"id"`
Name string `json:"name"`
Props []*Property `json:"properties"`
once sync.Once
decodedTextures *TexturesProp
decodedErr error
}
type TexturesProp struct {
Timestamp int64 `json:"timestamp"`
ProfileID string `json:"profileId"`
ProfileName string `json:"profileName"`
Textures *TexturesResponse `json:"textures"`
}
type TexturesResponse struct {
Skin *SkinTexturesResponse `json:"SKIN,omitempty"`
Cape *CapeTexturesResponse `json:"CAPE,omitempty"`
}
type SkinTexturesResponse struct {
Url string `json:"url"`
Metadata *SkinTexturesMetadata `json:"metadata,omitempty"`
}
type SkinTexturesMetadata struct {
Model string `json:"model"`
}
type CapeTexturesResponse struct {
Url string `json:"url"`
}
func (t *SignedTexturesResponse) DecodeTextures() (*TexturesProp, error) {
t.once.Do(func() {
var texturesProp string
for _, prop := range t.Props {
if prop.Name == "textures" {
texturesProp = prop.Value
break
}
}
if texturesProp == "" {
return
}
decodedTextures, err := DecodeTextures(texturesProp)
if err != nil {
t.decodedErr = err
} else {
t.decodedTextures = decodedTextures
}
})
return t.decodedTextures, t.decodedErr
}
type Property struct {
Name string `json:"name"`
Signature string `json:"signature,omitempty"`
Value string `json:"value"`
}
type ProfileInfo struct {
Id string `json:"id"`
Name string `json:"name"`
IsLegacy bool `json:"legacy,omitempty"`
IsDemo bool `json:"demo,omitempty"`
}
func errorFromResponse(response *http.Response) error {
switch {
case response.StatusCode == 400:
type errorResponse struct {
Error string `json:"error"`
Message string `json:"errorMessage"`
}
var decodedError *errorResponse
body, _ := io.ReadAll(response.Body)
_ = json.Unmarshal(body, &decodedError)
return &BadRequestError{ErrorType: decodedError.Error, Message: decodedError.Message}
case response.StatusCode == 403:
return &ForbiddenError{}
case response.StatusCode == 429:
return &TooManyRequestsError{}
case response.StatusCode >= 500:
return &ServerError{Status: response.StatusCode}
}
return fmt.Errorf("unexpected response status code: %d", response.StatusCode)
}
// When passed request params are invalid, Mojang returns 400 Bad Request error
type BadRequestError struct {
ErrorType string
Message string
}
func (e *BadRequestError) Error() string {
return fmt.Sprintf("400 %s: %s", e.ErrorType, e.Message)
}
// When Mojang decides you're such a bad guy, this error appears (even if the request has no authorization)
type ForbiddenError struct {
}
func (*ForbiddenError) Error() string {
return "403: Forbidden"
}
// When you exceed the set limit of requests, this error will be returned
type TooManyRequestsError struct {
}
func (*TooManyRequestsError) Error() string {
return "429: Too Many Requests"
}
// ServerError happens when Mojang's API returns any response with 50* status
type ServerError struct {
Status int
}
func (e *ServerError) Error() string {
return fmt.Sprintf("%d: %s", e.Status, "Server error")
}
func DecodeTextures(encodedTextures string) (*TexturesProp, error) {
jsonStr, err := base64.URLEncoding.DecodeString(encodedTextures)
if err != nil {
return nil, err
}
var result *TexturesProp
err = json.Unmarshal(jsonStr, &result)
if err != nil {
return nil, err
}
return result, nil
}
func EncodeTextures(textures *TexturesProp) string {
jsonSerialized, _ := json.Marshal(textures)
return base64.URLEncoding.EncodeToString(jsonSerialized)
}