Refactor cache code, add comments
This commit is contained in:
92
src/cache.go
Normal file
92
src/cache.go
Normal 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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
159
src/util.go
159
src/util.go
@@ -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 {
|
var (
|
||||||
|
err error = nil
|
||||||
|
skinImage *image.NRGBA = nil
|
||||||
|
rawSkin []byte = nil
|
||||||
|
isSlim bool = skin.IsSlimFromUUID(uuid)
|
||||||
|
profile *MinecraftProfile = nil
|
||||||
|
rawTextures string = ""
|
||||||
|
texturesProperty *MinecraftDecodedTextures = nil
|
||||||
|
)
|
||||||
|
|
||||||
|
// Get the textures metadata from Mojang about the Minecraft player
|
||||||
|
{
|
||||||
|
if profile, err = GetMinecraftProfile(uuid); err != nil {
|
||||||
|
return skin.GetDefaultSkin(isSlim), true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
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
|
// Locate the skin information within the Minecraft profile properties
|
||||||
}
|
{
|
||||||
}
|
for _, property := range profile.Properties {
|
||||||
|
|
||||||
isSlimFromUUID := skin.IsSlimFromUUID(uuid)
|
|
||||||
|
|
||||||
textures, err := GetProfileTextures(uuid)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return skin.GetDefaultSkin(isSlimFromUUID), 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" {
|
if property.Name != "textures" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
value = property.Value
|
rawTextures = property.Value
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(value) < 1 {
|
if len(rawTextures) < 1 {
|
||||||
return skin.GetDefaultSkin(isSlimFromUUID), isSlimFromUUID, nil
|
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
|
return nil, false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(texturesResult.Textures.Skin.URL) < 1 {
|
if len(texturesProperty.Textures.Skin.URL) < 1 {
|
||||||
return skin.GetDefaultSkin(isSlimFromUUID), isSlimFromUUID, nil
|
return skin.GetDefaultSkin(isSlim), isSlim, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
slim := texturesResult.Textures.Skin.Metadata.Model == "slim"
|
isSlim = texturesProperty.Textures.Skin.Metadata.Model == "slim"
|
||||||
|
}
|
||||||
|
|
||||||
skin, err := FetchImage(texturesResult.Textures.Skin.URL)
|
// Fetch the raw skin image from the Mojang API
|
||||||
|
{
|
||||||
if err != nil {
|
if skinImage, err = FetchImage(texturesProperty.Textures.Skin.URL); err != nil {
|
||||||
return nil, false, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
encodedSkin, err := EncodePNG(skin)
|
if rawSkin, err = EncodePNG(skinImage); err != nil {
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, false, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Put the skin into cache so it can be used for future requests
|
||||||
if conf.Cache.SkinCacheDuration != nil {
|
if conf.Cache.SkinCacheDuration != nil {
|
||||||
if err = r.Set(fmt.Sprintf("skin:%s", uuid), encodedSkin, *conf.Cache.SkinCacheDuration); err != nil {
|
if err = r.Set(fmt.Sprintf("skin:%s", uuid), rawSkin, *conf.Cache.SkinCacheDuration); err != nil {
|
||||||
return nil, false, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if slim {
|
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]
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user