Refactor cache code, add comments

This commit is contained in:
Jacob Gunther
2023-07-27 14:50:39 -05:00
parent 55fd9f7bb4
commit 2e08b91250
3 changed files with 183 additions and 112 deletions

92
src/cache.go Normal file
View File

@@ -0,0 +1,92 @@
package main
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"image"
"log"
)
type ResultCacheKey struct {
UUID string `json:"uuid"`
Type string `json:"type"`
Scale int `json:"scale"`
Overlay bool `json:"overlay"`
}
// GetCacheKey returns the key used in the cache based on the rendering options, calculated as an SHA-256 hash.
func GetResultCacheKey(uuid, renderType string, opts *QueryParams) (string, error) {
rawKey := ResultCacheKey{
UUID: uuid,
Type: renderType,
Scale: opts.Scale,
Overlay: opts.Overlay,
}
rawKeyData, err := json.Marshal(rawKey)
if err != nil {
return "", err
}
hash := sha256.Sum256(rawKeyData)
return fmt.Sprintf("result:%s", hex.EncodeToString(hash[:])), nil
}
// GetCachedRenderResult returns the render result from Redis cache, or nil if it does not exist or cache is disabled.
func GetCachedRenderResult(renderType, uuid string, opts *QueryParams) ([]byte, error) {
if conf.Cache.RenderCacheDuration == nil {
return nil, nil
}
key, err := GetResultCacheKey(uuid, renderType, opts)
if err != nil {
return nil, err
}
return r.GetBytes(key)
}
// SetCachedRenderResult puts the render result into cache, or does nothing is cache is disabled.
func SetCachedRenderResult(renderType, uuid string, opts *QueryParams, data []byte) error {
if conf.Cache.RenderCacheDuration == nil {
return nil
}
key, err := GetResultCacheKey(uuid, renderType, opts)
if err != nil {
return err
}
return r.Set(key, data, *conf.Cache.RenderCacheDuration)
}
// GetCachedSkin returns the raw skin of a player by UUID from the cache, also returning if the player has a slim player model.
func GetCachedSkin(uuid string) (*image.NRGBA, bool, error) {
cache, ok, err := r.GetNRGBA(fmt.Sprintf("skin:%s", uuid))
if err != nil {
return nil, false, err
}
if ok {
slim, err := r.Exists(fmt.Sprintf("slim:%s", uuid))
if err != nil {
return nil, false, err
}
if conf.Environment == "development" {
log.Printf("Retrieved player skin from cache (uuid=%s, slim=%v)\n", uuid, slim)
}
return cache, slim, nil
}
return nil, false, nil
}

View File

@@ -8,8 +8,8 @@ import (
"net/http" "net/http"
) )
// MinecraftProfileTextures is texture information about a Minecraft profile returned from the Mojang API. // MinecraftProfile is metadata about a Minecraft player returned from the Mojang API.
type MinecraftProfileTextures struct { type MinecraftProfile struct {
UUID string `json:"id"` UUID string `json:"id"`
Username string `json:"name"` Username string `json:"name"`
Legacy bool `json:"legacy"` Legacy bool `json:"legacy"`
@@ -20,7 +20,7 @@ type MinecraftProfileTextures struct {
} `json:"properties"` } `json:"properties"`
} }
// MinecraftDecodedTextures is the decoded object of the Base64-encoded values property in a MinecraftProfileTextures texture value. // MinecraftDecodedTextures is the decoded object of the base64-encoded values property in a MinecraftProfile properties value.
type MinecraftDecodedTextures struct { type MinecraftDecodedTextures struct {
Timestamp int64 `json:"timestamp"` Timestamp int64 `json:"timestamp"`
UUID string `json:"uuid"` UUID string `json:"uuid"`
@@ -39,8 +39,8 @@ type MinecraftDecodedTextures struct {
} `json:"textures"` } `json:"textures"`
} }
// GetProfileTextures returns the textures of a Minecraft player from Mojang. // GetMinecraftProfile returns the textures of a Minecraft player from Mojang.
func GetProfileTextures(uuid string) (*MinecraftProfileTextures, error) { func GetMinecraftProfile(uuid string) (*MinecraftProfile, error) {
req, err := http.NewRequest("GET", fmt.Sprintf("https://sessionserver.mojang.com/session/minecraft/profile/%s", uuid), nil) req, err := http.NewRequest("GET", fmt.Sprintf("https://sessionserver.mojang.com/session/minecraft/profile/%s", uuid), nil)
if err != nil { if err != nil {
@@ -71,7 +71,7 @@ func GetProfileTextures(uuid string) (*MinecraftProfileTextures, error) {
return nil, err return nil, err
} }
response := MinecraftProfileTextures{} response := MinecraftProfile{}
if err = json.Unmarshal(body, &response); err != nil { if err = json.Unmarshal(body, &response); err != nil {
return nil, err return nil, err

View File

@@ -42,6 +42,20 @@ func PointerOf[T any](v T) *T {
return &v return &v
} }
// Clamp clamps the input value between the minimum and maximum values.
// This method is preferred over `math.Min()` and `math.Max()` to prevent any type coercion between floats and integers.
func Clamp[T int | uint | int8 | uint8 | int16 | uint16 | int32 | uint32 | int64 | uint64](value, min, max T) T {
if value > max {
return max
}
if value < min {
return min
}
return value
}
// Render will render the image using the specified details and return the result. // Render will render the image using the specified details and return the result.
func Render(renderType, uuid string, rawSkin *image.NRGBA, isSlim bool, opts *QueryParams) ([]byte, bool, error) { func Render(renderType, uuid string, rawSkin *image.NRGBA, isSlim bool, opts *QueryParams) ([]byte, bool, error) {
if conf.Cache.EnableLocks { if conf.Cache.EnableLocks {
@@ -138,32 +152,14 @@ func Render(renderType, uuid string, rawSkin *image.NRGBA, isSlim bool, opts *Qu
return data, false, nil return data, false, nil
} }
// GetCachedRenderResult returns the render result from Redis cache, or nil if it does not exist or cache is disabled. // ExtractUUID returns the UUID from the route param, allowing values such as "<uuid>.png" to be returned as "<uuid>".
func GetCachedRenderResult(renderType, uuid string, opts *QueryParams) ([]byte, error) { func ExtractUUID(ctx *fiber.Ctx) string {
if conf.Cache.RenderCacheDuration == nil { return strings.Split(ctx.Params("uuid"), ".")[0]
return nil, nil
}
return r.GetBytes(fmt.Sprintf("result:%s-%d-%t-%s", renderType, opts.Scale, opts.Overlay, uuid))
}
// SetCachedRenderResult puts the render result into cache, or does nothing is cache is disabled.
func SetCachedRenderResult(renderType, uuid string, opts *QueryParams, data []byte) error {
if conf.Cache.RenderCacheDuration == nil {
return nil
}
return r.Set(fmt.Sprintf("result:%s-%d-%t-%s", renderType, opts.Scale, opts.Overlay, uuid), data, *conf.Cache.RenderCacheDuration)
}
// FormatUUID returns the UUID string without any dashes.
func FormatUUID(uuid string) string {
return strings.ToLower(strings.ReplaceAll(uuid, "-", ""))
} }
// ParseUUID parses the UUID given by the route parameters, and returns a boolean if the UUID is valid. // ParseUUID parses the UUID given by the route parameters, and returns a boolean if the UUID is valid.
func ParseUUID(value string) (string, bool) { func ParseUUID(value string) (string, bool) {
value = FormatUUID(value) value = strings.ToLower(strings.ReplaceAll(value, "-", ""))
if len(value) != 32 { if len(value) != 32 {
return "", false return "", false
@@ -208,88 +204,90 @@ func GetPlayerSkin(uuid string) (*image.NRGBA, bool, error) {
defer mutex.Unlock() defer mutex.Unlock()
} }
// Get skin from cache, and return if it exists
if conf.Cache.SkinCacheDuration != nil { if conf.Cache.SkinCacheDuration != nil {
cache, ok, err := r.GetNRGBA(fmt.Sprintf("skin:%s", uuid)) rawSkin, slim, err := GetCachedSkin(uuid)
if err != nil { if err != nil {
return nil, false, err return nil, false, err
} }
if ok { if rawSkin != nil {
slim, err := r.Exists(fmt.Sprintf("slim:%s", uuid)) return rawSkin, slim, nil
if err != nil {
return nil, false, err
}
if conf.Environment == "development" {
log.Printf("Retrieved player skin from cache (uuid=%s, slim=%v)\n", uuid, slim)
}
return cache, slim, nil
} }
} }
isSlimFromUUID := skin.IsSlimFromUUID(uuid) var (
err error = nil
skinImage *image.NRGBA = nil
rawSkin []byte = nil
isSlim bool = skin.IsSlimFromUUID(uuid)
profile *MinecraftProfile = nil
rawTextures string = ""
texturesProperty *MinecraftDecodedTextures = nil
)
textures, err := GetProfileTextures(uuid) // Get the textures metadata from Mojang about the Minecraft player
{
if err != nil { if profile, err = GetMinecraftProfile(uuid); err != nil {
return skin.GetDefaultSkin(isSlimFromUUID), true, nil return skin.GetDefaultSkin(isSlim), true, nil
}
if textures == nil {
return skin.GetDefaultSkin(isSlimFromUUID), isSlimFromUUID, nil
}
if err = r.Set(fmt.Sprintf("unique:%s", textures.UUID), "0", 0); err != nil {
return nil, false, err
}
value := ""
for _, property := range textures.Properties {
if property.Name != "textures" {
continue
} }
value = property.Value if profile == nil {
return skin.GetDefaultSkin(isSlim), isSlim, nil
}
if err = r.Set(fmt.Sprintf("unique:%s", profile.UUID), "0", 0); err != nil {
return nil, false, err
}
} }
if len(value) < 1 { // Locate the skin information within the Minecraft profile properties
return skin.GetDefaultSkin(isSlimFromUUID), isSlimFromUUID, nil {
for _, property := range profile.Properties {
if property.Name != "textures" {
continue
}
rawTextures = property.Value
}
if len(rawTextures) < 1 {
return skin.GetDefaultSkin(isSlim), isSlim, nil
}
} }
texturesResult, err := GetDecodedTexturesValue(value) // Decode the raw textures value returned from the player's properties
{
if err != nil { if texturesProperty, err = GetDecodedTexturesValue(rawTextures); err != nil {
return nil, false, err
}
if len(texturesResult.Textures.Skin.URL) < 1 {
return skin.GetDefaultSkin(isSlimFromUUID), isSlimFromUUID, nil
}
slim := texturesResult.Textures.Skin.Metadata.Model == "slim"
skin, err := FetchImage(texturesResult.Textures.Skin.URL)
if err != nil {
return nil, false, err
}
encodedSkin, err := EncodePNG(skin)
if err != nil {
return nil, false, err
}
if conf.Cache.SkinCacheDuration != nil {
if err = r.Set(fmt.Sprintf("skin:%s", uuid), encodedSkin, *conf.Cache.SkinCacheDuration); err != nil {
return nil, false, err return nil, false, err
} }
if slim { if len(texturesProperty.Textures.Skin.URL) < 1 {
return skin.GetDefaultSkin(isSlim), isSlim, nil
}
isSlim = texturesProperty.Textures.Skin.Metadata.Model == "slim"
}
// Fetch the raw skin image from the Mojang API
{
if skinImage, err = FetchImage(texturesProperty.Textures.Skin.URL); err != nil {
return nil, false, err
}
if rawSkin, err = EncodePNG(skinImage); err != nil {
return nil, false, err
}
}
// Put the skin into cache so it can be used for future requests
if conf.Cache.SkinCacheDuration != nil {
if err = r.Set(fmt.Sprintf("skin:%s", uuid), rawSkin, *conf.Cache.SkinCacheDuration); err != nil {
return nil, false, err
}
if isSlim {
if err = r.Set(fmt.Sprintf("slim:%s", uuid), "true", *conf.Cache.SkinCacheDuration); err != nil { if err = r.Set(fmt.Sprintf("slim:%s", uuid), "true", *conf.Cache.SkinCacheDuration); err != nil {
return nil, false, err return nil, false, err
} }
@@ -301,24 +299,10 @@ func GetPlayerSkin(uuid string) (*image.NRGBA, bool, error) {
} }
if conf.Environment == "development" { if conf.Environment == "development" {
log.Printf("Fetched player skin from Mojang (uuid=%s, slim=%v)\n", uuid, slim) log.Printf("Fetched player skin from Mojang (uuid=%s, slim=%v)\n", uuid, isSlim)
} }
return skin, slim, nil return skinImage, isSlim, nil
}
// Clamp clamps the input value between the minimum and maximum values.
// This method is preferred over `math.Min()` and `math.Max()` to prevent any type coercion between floats and integers.
func Clamp[T int | uint | int8 | uint8 | int16 | uint16 | int32 | uint32 | int64 | uint64](value, min, max T) T {
if value > max {
return max
}
if value < min {
return min
}
return value
} }
// EncodePNG encodes the image into PNG format and returns the data as a byte array. // EncodePNG encodes the image into PNG format and returns the data as a byte array.
@@ -373,8 +357,3 @@ func GetInstanceID() (uint16, error) {
return 0, nil return 0, nil
} }
// ExtractUUID returns the user name from the route param, allowing values such as "PassTheMayo.png" to be returned as "PassTheMayo".
func ExtractUUID(ctx *fiber.Ctx) string {
return strings.Split(ctx.Params("uuid"), ".")[0]
}