2019-04-14 20:06:27 +05:30
|
|
|
package mojang
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
2024-02-13 06:38:42 +05:30
|
|
|
"context"
|
2024-01-10 06:12:10 +05:30
|
|
|
"encoding/base64"
|
2019-04-14 20:06:27 +05:30
|
|
|
"encoding/json"
|
2020-04-02 04:59:14 +05:30
|
|
|
"fmt"
|
2024-01-10 06:12:10 +05:30
|
|
|
"io"
|
2019-04-14 20:06:27 +05:30
|
|
|
"net/http"
|
2020-04-03 22:53:34 +05:30
|
|
|
"strings"
|
2020-04-30 00:24:40 +05:30
|
|
|
"sync"
|
2019-04-14 20:06:27 +05:30
|
|
|
)
|
|
|
|
|
2024-01-10 06:12:10 +05:30
|
|
|
type MojangApi struct {
|
|
|
|
http *http.Client
|
|
|
|
batchUuidsUrl string
|
|
|
|
profileUrl string
|
2019-04-21 05:34:03 +05:30
|
|
|
}
|
2019-04-14 20:06:27 +05:30
|
|
|
|
2024-01-10 06:12:10 +05:30
|
|
|
func NewMojangApi(
|
|
|
|
http *http.Client,
|
|
|
|
batchUuidsUrl string,
|
|
|
|
profileUrl string,
|
|
|
|
) *MojangApi {
|
|
|
|
if batchUuidsUrl == "" {
|
|
|
|
batchUuidsUrl = "https://api.mojang.com/profiles/minecraft"
|
|
|
|
}
|
2019-04-27 04:16:15 +05:30
|
|
|
|
2024-01-10 06:12:10 +05:30
|
|
|
if profileUrl == "" {
|
|
|
|
profileUrl = "https://sessionserver.mojang.com/session/minecraft/profile/"
|
|
|
|
}
|
2019-04-14 20:06:27 +05:30
|
|
|
|
2024-01-10 06:12:10 +05:30
|
|
|
if !strings.HasSuffix(profileUrl, "/") {
|
|
|
|
profileUrl += "/"
|
|
|
|
}
|
2019-04-14 20:06:27 +05:30
|
|
|
|
2024-01-10 06:12:10 +05:30
|
|
|
return &MojangApi{
|
|
|
|
http,
|
|
|
|
batchUuidsUrl,
|
|
|
|
profileUrl,
|
|
|
|
}
|
2019-04-14 20:06:27 +05:30
|
|
|
}
|
|
|
|
|
2019-04-21 01:09:17 +05:30
|
|
|
// Exchanges usernames array to array of uuids
|
|
|
|
// See https://wiki.vg/Mojang_API#Playernames_-.3E_UUIDs
|
2024-02-13 06:38:42 +05:30
|
|
|
func (c *MojangApi) UsernamesToUuids(ctx context.Context, usernames []string) ([]*ProfileInfo, error) {
|
2019-04-14 20:06:27 +05:30
|
|
|
requestBody, _ := json.Marshal(usernames)
|
2024-02-13 06:38:42 +05:30
|
|
|
request, err := http.NewRequestWithContext(ctx, "POST", c.batchUuidsUrl, bytes.NewBuffer(requestBody))
|
2020-04-27 00:26:03 +05:30
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2019-04-14 20:06:27 +05:30
|
|
|
|
|
|
|
request.Header.Set("Content-Type", "application/json")
|
|
|
|
|
2024-01-10 06:12:10 +05:30
|
|
|
response, err := c.http.Do(request)
|
2019-04-14 20:06:27 +05:30
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
defer response.Body.Close()
|
|
|
|
|
2024-01-10 06:12:10 +05:30
|
|
|
if response.StatusCode != 200 {
|
|
|
|
return nil, errorFromResponse(response)
|
2019-04-14 20:06:27 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
var result []*ProfileInfo
|
|
|
|
|
2024-01-10 06:12:10 +05:30
|
|
|
body, _ := io.ReadAll(response.Body)
|
|
|
|
err = json.Unmarshal(body, &result)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2019-04-14 20:06:27 +05:30
|
|
|
|
|
|
|
return result, nil
|
|
|
|
}
|
|
|
|
|
2019-04-21 01:09:17 +05:30
|
|
|
// Obtains textures information for provided uuid
|
|
|
|
// See https://wiki.vg/Mojang_API#UUID_-.3E_Profile_.2B_Skin.2FCape
|
2024-02-13 06:38:42 +05:30
|
|
|
func (c *MojangApi) UuidToTextures(ctx context.Context, uuid string, signed bool) (*ProfileResponse, error) {
|
2020-04-03 22:53:34 +05:30
|
|
|
normalizedUuid := strings.ReplaceAll(uuid, "-", "")
|
2024-01-10 06:12:10 +05:30
|
|
|
url := c.profileUrl + normalizedUuid
|
2019-04-15 03:01:09 +05:30
|
|
|
if signed {
|
|
|
|
url += "?unsigned=false"
|
|
|
|
}
|
|
|
|
|
2024-02-13 06:38:42 +05:30
|
|
|
request, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
2020-04-27 00:26:03 +05:30
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2019-04-14 20:06:27 +05:30
|
|
|
|
2024-01-10 06:12:10 +05:30
|
|
|
response, err := c.http.Do(request)
|
2019-04-14 20:06:27 +05:30
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
defer response.Body.Close()
|
|
|
|
|
2024-01-10 06:12:10 +05:30
|
|
|
if response.StatusCode == 204 {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
if response.StatusCode != 200 {
|
|
|
|
return nil, errorFromResponse(response)
|
2019-04-14 20:06:27 +05:30
|
|
|
}
|
|
|
|
|
2024-01-30 13:35:04 +05:30
|
|
|
var result *ProfileResponse
|
2019-04-14 20:06:27 +05:30
|
|
|
|
2024-01-10 06:12:10 +05:30
|
|
|
body, _ := io.ReadAll(response.Body)
|
|
|
|
err = json.Unmarshal(body, &result)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2019-04-14 20:06:27 +05:30
|
|
|
|
|
|
|
return result, nil
|
|
|
|
}
|
|
|
|
|
2024-01-30 13:35:04 +05:30
|
|
|
type ProfileResponse struct {
|
2024-01-10 06:12:10 +05:30
|
|
|
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"`
|
|
|
|
}
|
|
|
|
|
2024-01-30 13:35:04 +05:30
|
|
|
func (t *ProfileResponse) DecodeTextures() (*TexturesProp, error) {
|
2024-01-10 06:12:10 +05:30
|
|
|
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 {
|
2019-04-21 01:34:29 +05:30
|
|
|
switch {
|
2019-04-21 05:34:03 +05:30
|
|
|
case response.StatusCode == 400:
|
|
|
|
type errorResponse struct {
|
|
|
|
Error string `json:"error"`
|
|
|
|
Message string `json:"errorMessage"`
|
|
|
|
}
|
|
|
|
|
|
|
|
var decodedError *errorResponse
|
2024-01-10 06:12:10 +05:30
|
|
|
body, _ := io.ReadAll(response.Body)
|
2019-04-21 05:34:03 +05:30
|
|
|
_ = json.Unmarshal(body, &decodedError)
|
|
|
|
|
|
|
|
return &BadRequestError{ErrorType: decodedError.Error, Message: decodedError.Message}
|
2019-11-08 04:02:26 +05:30
|
|
|
case response.StatusCode == 403:
|
|
|
|
return &ForbiddenError{}
|
2019-04-21 01:34:29 +05:30
|
|
|
case response.StatusCode == 429:
|
2019-04-21 01:09:17 +05:30
|
|
|
return &TooManyRequestsError{}
|
2019-04-21 01:34:29 +05:30
|
|
|
case response.StatusCode >= 500:
|
2019-04-21 05:34:03 +05:30
|
|
|
return &ServerError{Status: response.StatusCode}
|
2019-04-21 01:09:17 +05:30
|
|
|
}
|
|
|
|
|
2024-01-10 06:12:10 +05:30
|
|
|
return fmt.Errorf("unexpected response status code: %d", response.StatusCode)
|
2019-04-21 05:34:03 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
// When passed request params are invalid, Mojang returns 400 Bad Request error
|
|
|
|
type BadRequestError struct {
|
|
|
|
ErrorType string
|
|
|
|
Message string
|
|
|
|
}
|
|
|
|
|
|
|
|
func (e *BadRequestError) Error() string {
|
2020-04-02 04:59:14 +05:30
|
|
|
return fmt.Sprintf("400 %s: %s", e.ErrorType, e.Message)
|
2019-04-21 05:34:03 +05:30
|
|
|
}
|
|
|
|
|
2019-11-08 04:02:26 +05:30
|
|
|
// 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 {
|
2020-04-02 04:59:14 +05:30
|
|
|
return "403: Forbidden"
|
2019-11-08 04:02:26 +05:30
|
|
|
}
|
|
|
|
|
2019-04-21 01:09:17 +05:30
|
|
|
// When you exceed the set limit of requests, this error will be returned
|
2019-04-14 20:06:27 +05:30
|
|
|
type TooManyRequestsError struct {
|
|
|
|
}
|
|
|
|
|
2019-04-21 01:09:17 +05:30
|
|
|
func (*TooManyRequestsError) Error() string {
|
2020-04-02 04:59:14 +05:30
|
|
|
return "429: Too Many Requests"
|
2019-04-14 20:06:27 +05:30
|
|
|
}
|
2019-04-21 01:34:29 +05:30
|
|
|
|
|
|
|
// ServerError happens when Mojang's API returns any response with 50* status
|
|
|
|
type ServerError struct {
|
|
|
|
Status int
|
|
|
|
}
|
|
|
|
|
|
|
|
func (e *ServerError) Error() string {
|
2020-04-02 04:59:14 +05:30
|
|
|
return fmt.Sprintf("%d: %s", e.Status, "Server error")
|
2019-04-21 01:34:29 +05:30
|
|
|
}
|
2019-04-21 05:34:03 +05:30
|
|
|
|
2024-01-10 06:12:10 +05:30
|
|
|
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)
|
2019-04-21 05:34:03 +05:30
|
|
|
}
|