mirror of
				https://github.com/elyby/chrly.git
				synced 2025-05-31 14:11:51 +05:30 
			
		
		
		
	
		
			
				
	
	
		
			267 lines
		
	
	
		
			5.9 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			267 lines
		
	
	
		
			5.9 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package mojang
 | |
| 
 | |
| import (
 | |
| 	"bytes"
 | |
| 	"context"
 | |
| 	"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(ctx context.Context, usernames []string) ([]*ProfileInfo, error) {
 | |
| 	requestBody, _ := json.Marshal(usernames)
 | |
| 	request, err := http.NewRequestWithContext(ctx, "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(ctx context.Context, uuid string, signed bool) (*ProfileResponse, error) {
 | |
| 	normalizedUuid := strings.ReplaceAll(uuid, "-", "")
 | |
| 	url := c.profileUrl + normalizedUuid
 | |
| 	if signed {
 | |
| 		url += "?unsigned=false"
 | |
| 	}
 | |
| 
 | |
| 	request, err := http.NewRequestWithContext(ctx, "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 *ProfileResponse
 | |
| 
 | |
| 	body, _ := io.ReadAll(response.Body)
 | |
| 	err = json.Unmarshal(body, &result)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	return result, nil
 | |
| }
 | |
| 
 | |
| type ProfileResponse 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 *ProfileResponse) 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)
 | |
| }
 |