package api import ( "bytes" "encoding/json" "fmt" "io" "net/http" "strings" "time" "github.com/google/uuid" ) type Servers struct { ServicesHost string SessionHost string ProfilesHost string } var DefaultServers = &Servers{ ServicesHost: "https://api.minecraftservices.com", SessionHost: "https://sessionserver.mojang.com", ProfilesHost: "https://api.mojang.com", } type Account struct { *Servers Name string Uuid uuid.UUID Ygg string } func (a *Account) doMcApiRequest(method string, url string, from any, to any) (err error) { var b []byte if method != "GET" { b, err = json.Marshal(from) if err != nil { return } } r, err := http.NewRequest(method, url, bytes.NewReader(b)) if err != nil { return } r.Header.Set("Content-Type", "application/json") r.Header.Set("Accept", "application/json") r.Header.Set("x-xbl-contract-version", "1") r.Header.Set("Authorization", "Bearer "+a.Ygg) resp, err := http.DefaultClient.Do(r) if err != nil { return } defer resp.Body.Close() if !(resp.StatusCode >= 200 && resp.StatusCode <= 299) { return fmt.Errorf("bad status code %v", resp.StatusCode) } b, err = io.ReadAll(resp.Body) if err != nil { return } err = json.Unmarshal(b, to) if err != nil { return } return } type PlayerKeys struct { ExpiresAt time.Time `json:"expiresAt"` KeyPair struct { PublicKey string `json:"publicKey"` PrivateKey string `json:"privateKey"` } `json:"keyPair"` PublicKeySignature string `json:"publicKeySignature"` PublicKeySignatureV2 string `json:"publicKeySignatureV2"` RefreshedAfter time.Time `json:"refreshedAfter"` } func (a *Account) FetchKeys() (keys PlayerKeys, err error) { err = a.doMcApiRequest("POST", a.ServicesHost+"/player/certificates", nil, &keys) return } func (a *Account) JoinServer(serverId string) (err error) { body, err := json.Marshal(struct { AccessToken string `json:"accessToken"` SelectedProfile string `json:"selectedProfile"` ServerId string `json:"serverId"` }{ AccessToken: a.Ygg, SelectedProfile: strings.ReplaceAll(a.Uuid.String(), "-", ""), ServerId: serverId, }) if err != nil { return } resp, err := http.Post(a.SessionHost+"/session/minecraft/join", "application/json", bytes.NewReader(body)) if err != nil { return } defer resp.Body.Close() if resp.StatusCode != 204 { body, err = io.ReadAll(resp.Body) if err != nil { return } err = fmt.Errorf("bad client code %v body %v", resp.StatusCode, string(body)) return } return } type NameChangResp struct { ChangedAt string `json:"changedAt"` CreatedAt string `json:"createdAt"` NameChangeAllowed bool `json:"nameChangeAllowed"` } func (a *Account) NameChangeInfo() (r NameChangResp, err error) { err = a.doMcApiRequest("GET", a.ServicesHost+"/minecraft/profile/namechange", nil, &r) return } // PlayerProfile represents the player's profile information type PlayerProfile struct { ID string `json:"id"` Name string `json:"name"` Skins []struct { ID string `json:"id"` State string `json:"state"` URL string `json:"url"` Variant string `json:"variant"` } `json:"skins"` Capes []struct { ID string `json:"id"` State string `json:"state"` URL string `json:"url"` Alias string `json:"alias"` } `json:"capes"` } // QueryProfile queries the player's profile func (a *Account) QueryProfile() (profile PlayerProfile, err error) { err = a.doMcApiRequest("GET", a.ServicesHost+"/minecraft/profile", nil, &profile) return } // PlayerAttributes represents the player's attributes type PlayerAttributes struct { Privileges struct { OnlineChat struct { Enabled bool `json:"enabled"` } `json:"onlineChat"` MultiplayerServer struct { Enabled bool `json:"enabled"` } `json:"multiplayerServer"` MultiplayerRealms struct { Enabled bool `json:"enabled"` } `json:"multiplayerRealms"` Telemetry struct { Enabled bool `json:"enabled"` } `json:"telemetry"` OptionalTelemetry struct { Enabled bool `json:"enabled"` } `json:"optionalTelemetry"` } `json:"privileges"` ProfanityFilterPreferences struct { ProfanityFilterOn bool `json:"profanityFilterOn"` } `json:"profanityFilterPreferences"` BanStatus struct { BannedScopes map[string]struct { BanID string `json:"banId"` Expires string `json:"expires,omitempty"` Reason string `json:"reason"` ReasonMessage string `json:"reasonMessage"` } `json:"bannedScopes"` } `json:"banStatus"` } // QueryAttributes queries the player's attributes func (a *Account) QueryAttributes() (attrs PlayerAttributes, err error) { err = a.doMcApiRequest("GET", a.ServicesHost+"/player/attributes", nil, &attrs) return } // ModifyAttributesRequest represents the request to modify player attributes type ModifyAttributesRequest struct { ProfanityFilterPreferences struct { ProfanityFilterOn bool `json:"profanityFilterOn"` } `json:"profanityFilterPreferences"` } // ModifyAttributes modifies the player's attributes func (a *Account) ModifyAttributes(req ModifyAttributesRequest) (attrs PlayerAttributes, err error) { err = a.doMcApiRequest("POST", a.ServicesHost+"/player/attributes", req, &attrs) return } // BlockedUsers represents the list of blocked users type BlockedUsers struct { BlockedProfiles []string `json:"blockedProfiles"` } // GetBlocklist gets the list of blocked users func (a *Account) GetBlocklist() (blocklist BlockedUsers, err error) { err = a.doMcApiRequest("GET", a.ServicesHost+"/privacy/blocklist", nil, &blocklist) return } type NameAvailability struct { Status string `json:"status"` // DUPLICATE, AVAILABLE, NOT_ALLOWED } // CheckNameAvailability checks if a name is available func (a *Account) CheckNameAvailability(name string) (avail NameAvailability, err error) { err = a.doMcApiRequest("GET", a.ServicesHost+"/minecraft/profile/name/"+name+"/available", nil, &avail) return } // ChangeName changes the player's name func (a *Account) ChangeName(newName string) (profile PlayerProfile, err error) { err = a.doMcApiRequest("PUT", a.ServicesHost+"/minecraft/profile/name/"+newName, nil, &profile) return } // ChangeSkinRequest represents the request to change skin type ChangeSkinRequest struct { Variant string `json:"variant"` // "classic" or "slim" URL string `json:"url"` } // ChangeSkin changes the player's skin func (a *Account) ChangeSkin(req ChangeSkinRequest) (profile PlayerProfile, err error) { err = a.doMcApiRequest("POST", a.ServicesHost+"/minecraft/profile/skins", req, &profile) return } // ResetSkin resets the player's skin to default func (a *Account) ResetSkin() (profile PlayerProfile, err error) { err = a.doMcApiRequest("DELETE", a.ServicesHost+"/minecraft/profile/skins/active", nil, &profile) return } // HideCape hides the player's active cape func (a *Account) HideCape() (profile PlayerProfile, err error) { err = a.doMcApiRequest("DELETE", a.ServicesHost+"/minecraft/profile/capes/active", nil, &profile) return } // ShowCapeRequest represents the request to show a cape type ShowCapeRequest struct { CapeID string `json:"capeId"` } // ShowCape shows a specific cape func (a *Account) ShowCape(capeID string) (profile PlayerProfile, err error) { req := ShowCapeRequest{CapeID: capeID} err = a.doMcApiRequest("PUT", a.ServicesHost+"/minecraft/profile/capes/active", req, &profile) return } // CheckGiftCodeValidity checks if a gift code is valid func (a *Account) CheckGiftCodeValidity(giftCode string) (valid bool, err error) { url := a.ServicesHost + "/productvoucher/" + giftCode r, err := http.NewRequest("GET", url, nil) if err != nil { return } r.Header.Set("Authorization", "Bearer "+a.Ygg) resp, err := http.DefaultClient.Do(r) if err != nil { return } defer resp.Body.Close() // Returns HTTP 200 or 204 if valid, 404 if invalid if resp.StatusCode == 200 || resp.StatusCode == 204 { valid = true return } return } // NewAccountFromYGG creates an Account by authenticating the YGG token against // Mojang's API. It makes an HTTP call to fetch the player profile and returns // an Account with name and UUID populated, or an error if authentication fails. func NewAccountFromYGG(ygg string) (a *Account, err error) { a = &Account{ Ygg: ygg, Servers: DefaultServers, } p, err := a.QueryProfile() if err != nil { return } a.Name = p.Name a.Uuid, err = uuid.Parse(strings.ReplaceAll(p.ID, "-", "")) return } // NewAccount creates an Account from explicit name, UUID, and YGG token without // making any HTTP calls. The account uses Mojang's default API servers. func NewAccount(name string, uuid uuid.UUID, ygg string) (a *Account) { return &Account{ Name: name, Uuid: uuid, Ygg: ygg, Servers: DefaultServers, } }