diff --git a/config.example.yml b/config.example.yml index 1e2f3a4..aad5d11 100644 --- a/config.example.yml +++ b/config.example.yml @@ -3,34 +3,58 @@ redis: database: 0 routes: face: + default_overlay: true + default_download: false + default_fallback: true default_scale: 4 min_scale: 1 max_scale: 64 head: + default_overlay: true + default_download: false + default_fallback: true default_scale: 4 min_scale: 1 max_scale: 64 full_body: + default_overlay: true + default_download: false + default_fallback: true default_scale: 4 min_scale: 1 max_scale: 64 front_body: + default_overlay: true + default_download: false + default_fallback: true default_scale: 4 min_scale: 1 max_scale: 64 back_body: + default_overlay: true + default_download: false + default_fallback: true default_scale: 4 min_scale: 1 max_scale: 64 left_body: + default_overlay: true + default_download: false + default_fallback: true default_scale: 4 min_scale: 1 max_scale: 64 right_body: + default_overlay: true + default_download: false + default_fallback: true default_scale: 4 min_scale: 1 max_scale: 64 + raw_skin: + default_download: false + default_fallback: true cache: - uuid_cache_duration: 2678400 # 1 week - skin_cache_duration: 86400 # 1 day - render_cache_duration: 86400 # 1 day \ No newline at end of file + uuid_cache_duration: 168h # 1 week + skin_cache_duration: 12h # 12 hours + render_cache_duration: 12h # 12 hours \ No newline at end of file diff --git a/go.mod b/go.mod index 531e1f4..9d447ff 100644 --- a/go.mod +++ b/go.mod @@ -6,9 +6,10 @@ require ( github.com/buaazp/fasthttprouter v0.1.1 github.com/go-redis/redis/v8 v8.11.4 github.com/joho/godotenv v1.4.0 + github.com/mineatar-io/skin-render v1.0.1 + github.com/mineatar-io/yggdrasil v1.0.0 github.com/valyala/fasthttp v1.34.0 - golang.org/x/image v0.0.0-20220302094943-723b81ca9867 - gopkg.in/yaml.v2 v2.4.0 + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b ) require ( @@ -17,4 +18,5 @@ require ( github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/klauspost/compress v1.15.1 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect + golang.org/x/image v0.0.0-20220302094943-723b81ca9867 // indirect ) diff --git a/go.sum b/go.sum index 4899398..cc138ea 100644 --- a/go.sum +++ b/go.sum @@ -31,10 +31,13 @@ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/klauspost/compress v1.15.0 h1:xqfchp4whNFxn5A4XFyyYtitiWI8Hy5EW59jEwcyL6U= github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.15.1 h1:y9FcTHGyrebwfP0ZZqFiaxTaiDnUrGkJkI+f583BL1A= github.com/klauspost/compress v1.15.1/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/mineatar-io/skin-render v1.0.1 h1:ukZCagxGuaZM+E16h7XY+xGI+xlnI5R0EfEW0y+BIxs= +github.com/mineatar-io/skin-render v1.0.1/go.mod h1:DkRV/kQmFokYp6n6KDcbSRA3EnEJnUW39GgTwPnYygQ= +github.com/mineatar-io/yggdrasil v1.0.0 h1:ysDocS9XiQlzX43FNT715+2SBfcz7JrD11Gga6hvVg8= +github.com/mineatar-io/yggdrasil v1.0.0/go.mod h1:3WBd9LCY8AUy5rhftfp5hWGSE3VWfAIaSZS5rHROpJo= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= @@ -121,3 +124,5 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/src/conf/config.go b/src/conf/config.go index 9c786d3..9802be8 100644 --- a/src/conf/config.go +++ b/src/conf/config.go @@ -2,56 +2,39 @@ package conf import ( "io/ioutil" + "time" - "gopkg.in/yaml.v2" + "gopkg.in/yaml.v3" ) +type RouteConfig struct { + DefaultScale int `yaml:"default_scale"` + DefaultOverlay bool `yaml:"default_overlay"` + DefaultFallback bool `yaml:"default_fallback"` + DefaultDownload bool `yaml:"default_download"` + MinScale int `yaml:"min_scale"` + MaxScale int `yaml:"max_scale"` +} + type Configuration struct { Redis struct { URI string `yaml:"uri"` Database int `yaml:"database"` } `yaml:"redis"` Routes struct { - Face struct { - DefaultScale int `yaml:"default_scale"` - MinScale int `yaml:"min_scale"` - MaxScale int `yaml:"max_scale"` - } `yaml:"face"` - Head struct { - DefaultScale int `yaml:"default_scale"` - MinScale int `yaml:"min_scale"` - MaxScale int `yaml:"max_scale"` - } `yaml:"head"` - FullBody struct { - DefaultScale int `yaml:"default_scale"` - MinScale int `yaml:"min_scale"` - MaxScale int `yaml:"max_scale"` - } `yaml:"full_body"` - FrontBody struct { - DefaultScale int `yaml:"default_scale"` - MinScale int `yaml:"min_scale"` - MaxScale int `yaml:"max_scale"` - } `yaml:"front_body"` - BackBody struct { - DefaultScale int `yaml:"default_scale"` - MinScale int `yaml:"min_scale"` - MaxScale int `yaml:"max_scale"` - } `yaml:"back_body"` - LeftBody struct { - DefaultScale int `yaml:"default_scale"` - MinScale int `yaml:"min_scale"` - MaxScale int `yaml:"max_scale"` - } `yaml:"left_body"` - RightBody struct { - DefaultScale int `yaml:"default_scale"` - MinScale int `yaml:"min_scale"` - MaxScale int `yaml:"max_scale"` - } `yaml:"right_body"` + Face RouteConfig `yaml:"face"` + Head RouteConfig `yaml:"head"` + FullBody RouteConfig `yaml:"full_body"` + FrontBody RouteConfig `yaml:"front_body"` + BackBody RouteConfig `yaml:"back_body"` + LeftBody RouteConfig `yaml:"left_body"` + RightBody RouteConfig `yaml:"right_body"` + RawSkin RouteConfig `yaml:"raw_skin"` } `yaml:"routes"` Cache struct { - UUIDCacheDuration int64 `yaml:"uuid_cache_duration"` - SkinCacheDuration int64 `yaml:"skin_cache_duration"` - RenderCacheDuration int64 `yaml:"render_cache_duration"` + UUIDCacheDuration time.Duration `yaml:"uuid_cache_duration"` + SkinCacheDuration time.Duration `yaml:"skin_cache_duration"` + RenderCacheDuration time.Duration `yaml:"render_cache_duration"` } `yaml:"cache"` } diff --git a/src/main.go b/src/main.go index cf767ad..37fe86c 100644 --- a/src/main.go +++ b/src/main.go @@ -58,6 +58,21 @@ func init() { util.Init(r, config) } +func middleware(next fasthttp.RequestHandler) fasthttp.RequestHandler { + return func(ctx *fasthttp.RequestCtx) { + ctx.Response.Header.Set("Access-Control-Allow-Origin", "*") + ctx.Response.Header.Set("Access-Control-Allow-Headers", "*") + ctx.Response.Header.Set("Access-Control-Allow-Methods", "GET,POST,HEAD,OPTIONS") + ctx.Response.Header.Set("Access-Control-Expose-Headers", "X-Cache-Hit") + + if util.Debug { + log.Printf("%s %s (%s) - %s\n", ctx.Method(), ctx.URI(), ctx.RemoteAddr(), ctx.UserAgent()) + } + + next(ctx) + } +} + func main() { defer r.Close() @@ -76,5 +91,5 @@ func main() { log.Printf("Listening on %s:%d\n", host, port) - log.Fatal(fasthttp.ListenAndServe(fmt.Sprintf("%s:%d", host, port), router.Handler)) + log.Fatal(fasthttp.ListenAndServe(fmt.Sprintf("%s:%d", host, port), middleware(router.Handler))) } diff --git a/src/redis/redis.go b/src/redis/redis.go index d7f1413..811dec5 100644 --- a/src/redis/redis.go +++ b/src/redis/redis.go @@ -1,7 +1,10 @@ package redis import ( + "bytes" "context" + "image" + "image/draw" "time" "github.com/go-redis/redis/v8" @@ -72,6 +75,34 @@ func (r *Redis) GetBytes(key string) ([]byte, bool, error) { return data, true, err } +func (r *Redis) GetNRGBA(key string) (*image.NRGBA, bool, error) { + value, ok, err := r.GetBytes(key) + + if err != nil { + return nil, false, err + } + + if !ok { + return nil, false, nil + } + + img, format, err := image.Decode(bytes.NewReader(value)) + + if err != nil { + return nil, false, err + } + + if format != "NRGBA" { + outputImg := image.NewNRGBA(img.Bounds()) + + draw.Draw(outputImg, img.Bounds(), img, image.Pt(0, 0), draw.Src) + + return outputImg, true, nil + } + + return img.(*image.NRGBA), true, nil +} + func (r *Redis) Exists(key string) (bool, error) { ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) diff --git a/src/routes/body.go b/src/routes/body.go index 6832cce..a73f702 100644 --- a/src/routes/body.go +++ b/src/routes/body.go @@ -4,33 +4,18 @@ import ( "fmt" "log" "net/http" - "time" "github.com/mineatar-io/api-server/src/util" - "github.com/mineatar-io/api-server/src/util/renders" + "github.com/mineatar-io/skin-render" "github.com/valyala/fasthttp" ) func FullBodyHandler(ctx *fasthttp.RequestCtx) { user := ctx.UserValue("user").(string) - download := ctx.QueryArgs().GetBool("download") + opts := util.ParseQueryParams(ctx, config.Routes.FullBody) - scale, err := ctx.QueryArgs().GetUint("scale") - - if err != nil { - scale = config.Routes.FullBody.DefaultScale - } - - scale = util.Clamp(scale, config.Routes.FullBody.MinScale, config.Routes.FullBody.MaxScale) - - overlay := true - - if ctx.QueryArgs().Has("overlay") { - overlay = ctx.QueryArgs().GetBool("overlay") - } - - uuid, err := util.GetUUID(user) + uuid, ok, err := util.LookupUUID(user) if err != nil { log.Println(err) @@ -41,32 +26,45 @@ func FullBodyHandler(ctx *fasthttp.RequestCtx) { return } - cacheKey := fmt.Sprintf("result:fullbody-%d-%t-%s", scale, overlay, uuid) - - cache, ok, err := r.GetBytes(cacheKey) - - if err != nil { - log.Println(err) - - ctx.SetStatusCode(http.StatusInternalServerError) - ctx.SetBodyString(http.StatusText(http.StatusInternalServerError)) + if !ok && !opts.Fallback { + ctx.SetStatusCode(http.StatusNotFound) + ctx.SetBodyString(http.StatusText(http.StatusNotFound)) return } - if ok { - if download { - ctx.Response.Header.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s.png"`, user)) + cacheKey := fmt.Sprintf("result:fullbody-%d-%t-%s", opts.Scale, opts.Overlay, uuid) + + { + cache, ok, err := r.GetBytes(cacheKey) + + if err != nil { + log.Println(err) + + ctx.SetStatusCode(http.StatusInternalServerError) + ctx.SetBodyString(http.StatusText(http.StatusInternalServerError)) + + return } - ctx.Response.Header.Set("X-Cache-Hit", "TRUE") - ctx.SetContentType("image/png") - ctx.SetBody(cache) + if ok { + if opts.Download { + ctx.Response.Header.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s.png"`, user)) + } - return + if util.Debug { + log.Printf("Retrieved cache for full body render for '%s'\n", uuid) + } + + ctx.Response.Header.Set("X-Cache-Hit", "TRUE") + ctx.SetContentType("image/png") + ctx.SetBody(cache) + + return + } } - skin, slim, err := util.GetPlayerSkin(uuid) + rawSkin, slim, err := util.GetPlayerSkin(uuid) if err != nil { log.Println(err) @@ -77,16 +75,16 @@ func FullBodyHandler(ctx *fasthttp.RequestCtx) { return } - if skin == nil { - skin = util.GetDefaultSkin(slim) - } - - render := renders.RenderBody(skin, renders.RenderOptions{ - Overlay: overlay, + render := skin.RenderBody(rawSkin, skin.Options{ + Overlay: opts.Overlay, Slim: slim, - Scale: scale, + Scale: opts.Scale, }) + if util.Debug { + log.Printf("Rendered full body image for '%s'\n", uuid) + } + data, err := util.EncodePNG(render) if err != nil { @@ -98,7 +96,7 @@ func FullBodyHandler(ctx *fasthttp.RequestCtx) { return } - if err = r.Set(cacheKey, data, time.Duration(config.Cache.RenderCacheDuration)*time.Second); err != nil { + if err = r.Set(cacheKey, data, config.Cache.RenderCacheDuration); err != nil { log.Println(err) ctx.SetStatusCode(http.StatusInternalServerError) @@ -107,7 +105,7 @@ func FullBodyHandler(ctx *fasthttp.RequestCtx) { return } - if download { + if opts.Download { ctx.Response.Header.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s.png"`, user)) } @@ -119,23 +117,9 @@ func FullBodyHandler(ctx *fasthttp.RequestCtx) { func FrontBodyHandler(ctx *fasthttp.RequestCtx) { user := ctx.UserValue("user").(string) - download := ctx.QueryArgs().GetBool("download") + opts := util.ParseQueryParams(ctx, config.Routes.FrontBody) - scale, err := ctx.QueryArgs().GetUint("scale") - - if err != nil { - scale = config.Routes.FrontBody.DefaultScale - } - - scale = util.Clamp(scale, config.Routes.FrontBody.MinScale, config.Routes.FrontBody.MaxScale) - - overlay := true - - if ctx.QueryArgs().Has("overlay") { - overlay = ctx.QueryArgs().GetBool("overlay") - } - - uuid, err := util.GetUUID(user) + uuid, ok, err := util.LookupUUID(user) if err != nil { log.Println(err) @@ -146,32 +130,45 @@ func FrontBodyHandler(ctx *fasthttp.RequestCtx) { return } - cacheKey := fmt.Sprintf("result:frontbody-%d-%t-%s", scale, overlay, uuid) - - cache, ok, err := r.GetBytes(cacheKey) - - if err != nil { - log.Println(err) - - ctx.SetStatusCode(http.StatusInternalServerError) - ctx.SetBodyString(http.StatusText(http.StatusInternalServerError)) + if !ok && !opts.Fallback { + ctx.SetStatusCode(http.StatusNotFound) + ctx.SetBodyString(http.StatusText(http.StatusNotFound)) return } - if ok { - if download { - ctx.Response.Header.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s.png"`, user)) + cacheKey := fmt.Sprintf("result:frontbody-%d-%t-%s", opts.Scale, opts.Overlay, uuid) + + { + cache, ok, err := r.GetBytes(cacheKey) + + if err != nil { + log.Println(err) + + ctx.SetStatusCode(http.StatusInternalServerError) + ctx.SetBodyString(http.StatusText(http.StatusInternalServerError)) + + return } - ctx.Response.Header.Set("X-Cache-Hit", "TRUE") - ctx.SetContentType("image/png") - ctx.SetBody(cache) + if ok { + if opts.Download { + ctx.Response.Header.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s.png"`, user)) + } - return + if util.Debug { + log.Printf("Retrieved cache for front body render for '%s'\n", uuid) + } + + ctx.Response.Header.Set("X-Cache-Hit", "TRUE") + ctx.SetContentType("image/png") + ctx.SetBody(cache) + + return + } } - skin, slim, err := util.GetPlayerSkin(uuid) + rawSkin, slim, err := util.GetPlayerSkin(uuid) if err != nil { log.Println(err) @@ -182,16 +179,16 @@ func FrontBodyHandler(ctx *fasthttp.RequestCtx) { return } - if skin == nil { - skin = util.GetDefaultSkin(slim) - } - - render := renders.RenderFrontBody(skin, renders.RenderOptions{ - Overlay: overlay, + render := skin.RenderFrontBody(rawSkin, skin.Options{ + Overlay: opts.Overlay, Slim: slim, - Scale: scale, + Scale: opts.Scale, }) + if util.Debug { + log.Printf("Rendered front body image for '%s'\n", uuid) + } + data, err := util.EncodePNG(render) if err != nil { @@ -203,7 +200,7 @@ func FrontBodyHandler(ctx *fasthttp.RequestCtx) { return } - if err = r.Set(cacheKey, data, time.Duration(config.Cache.RenderCacheDuration)*time.Second); err != nil { + if err = r.Set(cacheKey, data, config.Cache.RenderCacheDuration); err != nil { log.Println(err) ctx.SetStatusCode(http.StatusInternalServerError) @@ -212,7 +209,7 @@ func FrontBodyHandler(ctx *fasthttp.RequestCtx) { return } - if download { + if opts.Download { ctx.Response.Header.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s.png"`, user)) } @@ -224,23 +221,9 @@ func FrontBodyHandler(ctx *fasthttp.RequestCtx) { func BackBodyHandler(ctx *fasthttp.RequestCtx) { user := ctx.UserValue("user").(string) - download := ctx.QueryArgs().GetBool("download") + opts := util.ParseQueryParams(ctx, config.Routes.BackBody) - scale, err := ctx.QueryArgs().GetUint("scale") - - if err != nil { - scale = config.Routes.BackBody.DefaultScale - } - - scale = util.Clamp(scale, config.Routes.BackBody.MinScale, config.Routes.BackBody.MaxScale) - - overlay := true - - if ctx.QueryArgs().Has("overlay") { - overlay = ctx.QueryArgs().GetBool("overlay") - } - - uuid, err := util.GetUUID(user) + uuid, ok, err := util.LookupUUID(user) if err != nil { log.Println(err) @@ -251,32 +234,45 @@ func BackBodyHandler(ctx *fasthttp.RequestCtx) { return } - cacheKey := fmt.Sprintf("result:backbody-%d-%t-%s", scale, overlay, uuid) - - cache, ok, err := r.GetBytes(cacheKey) - - if err != nil { - log.Println(err) - - ctx.SetStatusCode(http.StatusInternalServerError) - ctx.SetBodyString(http.StatusText(http.StatusInternalServerError)) + if !ok && !opts.Fallback { + ctx.SetStatusCode(http.StatusNotFound) + ctx.SetBodyString(http.StatusText(http.StatusNotFound)) return } - if ok { - if download { - ctx.Response.Header.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s.png"`, user)) + cacheKey := fmt.Sprintf("result:backbody-%d-%t-%s", opts.Scale, opts.Overlay, uuid) + + { + cache, ok, err := r.GetBytes(cacheKey) + + if err != nil { + log.Println(err) + + ctx.SetStatusCode(http.StatusInternalServerError) + ctx.SetBodyString(http.StatusText(http.StatusInternalServerError)) + + return } - ctx.Response.Header.Set("X-Cache-Hit", "TRUE") - ctx.SetContentType("image/png") - ctx.SetBody(cache) + if ok { + if opts.Download { + ctx.Response.Header.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s.png"`, user)) + } - return + if util.Debug { + log.Printf("Retrieved cache for back body render for '%s'\n", uuid) + } + + ctx.Response.Header.Set("X-Cache-Hit", "TRUE") + ctx.SetContentType("image/png") + ctx.SetBody(cache) + + return + } } - skin, slim, err := util.GetPlayerSkin(uuid) + rawSkin, slim, err := util.GetPlayerSkin(uuid) if err != nil { log.Println(err) @@ -287,16 +283,16 @@ func BackBodyHandler(ctx *fasthttp.RequestCtx) { return } - if skin == nil { - skin = util.GetDefaultSkin(slim) - } - - render := renders.RenderBackBody(skin, renders.RenderOptions{ - Overlay: overlay, + render := skin.RenderBackBody(rawSkin, skin.Options{ + Overlay: opts.Overlay, Slim: slim, - Scale: scale, + Scale: opts.Scale, }) + if util.Debug { + log.Printf("Rendered back body image for '%s'\n", uuid) + } + data, err := util.EncodePNG(render) if err != nil { @@ -308,7 +304,7 @@ func BackBodyHandler(ctx *fasthttp.RequestCtx) { return } - if err = r.Set(cacheKey, data, time.Duration(config.Cache.RenderCacheDuration)*time.Second); err != nil { + if err = r.Set(cacheKey, data, config.Cache.RenderCacheDuration); err != nil { log.Println(err) ctx.SetStatusCode(http.StatusInternalServerError) @@ -317,7 +313,7 @@ func BackBodyHandler(ctx *fasthttp.RequestCtx) { return } - if download { + if opts.Download { ctx.Response.Header.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s.png"`, user)) } @@ -329,23 +325,9 @@ func BackBodyHandler(ctx *fasthttp.RequestCtx) { func LeftBodyHandler(ctx *fasthttp.RequestCtx) { user := ctx.UserValue("user").(string) - download := ctx.QueryArgs().GetBool("download") + opts := util.ParseQueryParams(ctx, config.Routes.LeftBody) - scale, err := ctx.QueryArgs().GetUint("scale") - - if err != nil { - scale = config.Routes.LeftBody.DefaultScale - } - - scale = util.Clamp(scale, config.Routes.LeftBody.MinScale, config.Routes.LeftBody.MaxScale) - - overlay := true - - if ctx.QueryArgs().Has("overlay") { - overlay = ctx.QueryArgs().GetBool("overlay") - } - - uuid, err := util.GetUUID(user) + uuid, ok, err := util.LookupUUID(user) if err != nil { log.Println(err) @@ -356,32 +338,45 @@ func LeftBodyHandler(ctx *fasthttp.RequestCtx) { return } - cacheKey := fmt.Sprintf("result:leftbody-%d-%t-%s", scale, overlay, uuid) - - cache, ok, err := r.GetBytes(cacheKey) - - if err != nil { - log.Println(err) - - ctx.SetStatusCode(http.StatusInternalServerError) - ctx.SetBodyString(http.StatusText(http.StatusInternalServerError)) + if !ok && !opts.Fallback { + ctx.SetStatusCode(http.StatusNotFound) + ctx.SetBodyString(http.StatusText(http.StatusNotFound)) return } - if ok { - if download { - ctx.Response.Header.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s.png"`, user)) + cacheKey := fmt.Sprintf("result:leftbody-%d-%t-%s", opts.Scale, opts.Overlay, uuid) + + { + cache, ok, err := r.GetBytes(cacheKey) + + if err != nil { + log.Println(err) + + ctx.SetStatusCode(http.StatusInternalServerError) + ctx.SetBodyString(http.StatusText(http.StatusInternalServerError)) + + return } - ctx.Response.Header.Set("X-Cache-Hit", "TRUE") - ctx.SetContentType("image/png") - ctx.SetBody(cache) + if ok { + if opts.Download { + ctx.Response.Header.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s.png"`, user)) + } - return + if util.Debug { + log.Printf("Retrieved cache for left body render for '%s'\n", uuid) + } + + ctx.Response.Header.Set("X-Cache-Hit", "TRUE") + ctx.SetContentType("image/png") + ctx.SetBody(cache) + + return + } } - skin, slim, err := util.GetPlayerSkin(uuid) + rawSkin, slim, err := util.GetPlayerSkin(uuid) if err != nil { log.Println(err) @@ -392,16 +387,16 @@ func LeftBodyHandler(ctx *fasthttp.RequestCtx) { return } - if skin == nil { - skin = util.GetDefaultSkin(slim) - } - - render := renders.RenderLeftBody(skin, renders.RenderOptions{ - Overlay: overlay, + render := skin.RenderLeftBody(rawSkin, skin.Options{ + Overlay: opts.Overlay, Slim: slim, - Scale: scale, + Scale: opts.Scale, }) + if util.Debug { + log.Printf("Rendered left body image for '%s'\n", uuid) + } + data, err := util.EncodePNG(render) if err != nil { @@ -413,7 +408,7 @@ func LeftBodyHandler(ctx *fasthttp.RequestCtx) { return } - if err = r.Set(cacheKey, data, time.Duration(config.Cache.RenderCacheDuration)*time.Second); err != nil { + if err = r.Set(cacheKey, data, config.Cache.RenderCacheDuration); err != nil { log.Println(err) ctx.SetStatusCode(http.StatusInternalServerError) @@ -422,7 +417,7 @@ func LeftBodyHandler(ctx *fasthttp.RequestCtx) { return } - if download { + if opts.Download { ctx.Response.Header.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s.png"`, user)) } @@ -434,23 +429,9 @@ func LeftBodyHandler(ctx *fasthttp.RequestCtx) { func RightBodyHandler(ctx *fasthttp.RequestCtx) { user := ctx.UserValue("user").(string) - download := ctx.QueryArgs().GetBool("download") + opts := util.ParseQueryParams(ctx, config.Routes.RightBody) - scale, err := ctx.QueryArgs().GetUint("scale") - - if err != nil { - scale = config.Routes.RightBody.DefaultScale - } - - scale = util.Clamp(scale, config.Routes.RightBody.MinScale, config.Routes.RightBody.MaxScale) - - overlay := true - - if ctx.QueryArgs().Has("overlay") { - overlay = ctx.QueryArgs().GetBool("overlay") - } - - uuid, err := util.GetUUID(user) + uuid, ok, err := util.LookupUUID(user) if err != nil { log.Println(err) @@ -461,32 +442,45 @@ func RightBodyHandler(ctx *fasthttp.RequestCtx) { return } - cacheKey := fmt.Sprintf("result:rightbody-%d-%t-%s", scale, overlay, uuid) - - cache, ok, err := r.GetBytes(cacheKey) - - if err != nil { - log.Println(err) - - ctx.SetStatusCode(http.StatusInternalServerError) - ctx.SetBodyString(http.StatusText(http.StatusInternalServerError)) + if !ok && !opts.Fallback { + ctx.SetStatusCode(http.StatusNotFound) + ctx.SetBodyString(http.StatusText(http.StatusNotFound)) return } - if ok { - if download { - ctx.Response.Header.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s.png"`, user)) + cacheKey := fmt.Sprintf("result:rightbody-%d-%t-%s", opts.Scale, opts.Overlay, uuid) + + { + cache, ok, err := r.GetBytes(cacheKey) + + if err != nil { + log.Println(err) + + ctx.SetStatusCode(http.StatusInternalServerError) + ctx.SetBodyString(http.StatusText(http.StatusInternalServerError)) + + return } - ctx.Response.Header.Set("X-Cache-Hit", "TRUE") - ctx.SetContentType("image/png") - ctx.SetBody(cache) + if ok { + if opts.Download { + ctx.Response.Header.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s.png"`, user)) + } - return + if util.Debug { + log.Printf("Retrieved cache for right body render for '%s'\n", uuid) + } + + ctx.Response.Header.Set("X-Cache-Hit", "TRUE") + ctx.SetContentType("image/png") + ctx.SetBody(cache) + + return + } } - skin, slim, err := util.GetPlayerSkin(uuid) + rawSkin, slim, err := util.GetPlayerSkin(uuid) if err != nil { log.Println(err) @@ -497,16 +491,16 @@ func RightBodyHandler(ctx *fasthttp.RequestCtx) { return } - if skin == nil { - skin = util.GetDefaultSkin(slim) - } - - render := renders.RenderRightBody(skin, renders.RenderOptions{ - Overlay: overlay, + render := skin.RenderRightBody(rawSkin, skin.Options{ + Overlay: opts.Overlay, Slim: slim, - Scale: scale, + Scale: opts.Scale, }) + if util.Debug { + log.Printf("Rendered right body image for '%s'\n", uuid) + } + data, err := util.EncodePNG(render) if err != nil { @@ -518,7 +512,7 @@ func RightBodyHandler(ctx *fasthttp.RequestCtx) { return } - if err = r.Set(cacheKey, data, time.Duration(config.Cache.RenderCacheDuration)*time.Second); err != nil { + if err = r.Set(cacheKey, data, config.Cache.RenderCacheDuration); err != nil { log.Println(err) ctx.SetStatusCode(http.StatusInternalServerError) @@ -527,7 +521,7 @@ func RightBodyHandler(ctx *fasthttp.RequestCtx) { return } - if download { + if opts.Download { ctx.Response.Header.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s.png"`, user)) } diff --git a/src/routes/face.go b/src/routes/face.go index 835a9fa..f4dc7bd 100644 --- a/src/routes/face.go +++ b/src/routes/face.go @@ -4,33 +4,18 @@ import ( "fmt" "log" "net/http" - "time" "github.com/mineatar-io/api-server/src/util" - "github.com/mineatar-io/api-server/src/util/renders" + "github.com/mineatar-io/skin-render" "github.com/valyala/fasthttp" ) func FaceHandler(ctx *fasthttp.RequestCtx) { user := ctx.UserValue("user").(string) - download := ctx.QueryArgs().GetBool("download") + opts := util.ParseQueryParams(ctx, config.Routes.Face) - scale, err := ctx.QueryArgs().GetUint("scale") - - if err != nil { - scale = config.Routes.Face.DefaultScale - } - - scale = util.Clamp(scale, config.Routes.Face.MinScale, config.Routes.Face.MaxScale) - - overlay := true - - if ctx.QueryArgs().Has("overlay") { - overlay = ctx.QueryArgs().GetBool("overlay") - } - - uuid, err := util.GetUUID(user) + uuid, ok, err := util.LookupUUID(user) if err != nil { log.Println(err) @@ -41,32 +26,45 @@ func FaceHandler(ctx *fasthttp.RequestCtx) { return } - cacheKey := fmt.Sprintf("result:face-%d-%t-%s", scale, overlay, uuid) - - cache, ok, err := r.GetBytes(cacheKey) - - if err != nil { - log.Println(err) - - ctx.SetStatusCode(http.StatusInternalServerError) - ctx.SetBodyString(http.StatusText(http.StatusInternalServerError)) + if !ok && !opts.Fallback { + ctx.SetStatusCode(http.StatusNotFound) + ctx.SetBodyString(http.StatusText(http.StatusNotFound)) return } - if ok { - if download { - ctx.Response.Header.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s.png"`, user)) + cacheKey := fmt.Sprintf("result:face-%d-%t-%s", opts.Scale, opts.Overlay, uuid) + + { + cache, ok, err := r.GetBytes(cacheKey) + + if err != nil { + log.Println(err) + + ctx.SetStatusCode(http.StatusInternalServerError) + ctx.SetBodyString(http.StatusText(http.StatusInternalServerError)) + + return } - ctx.Response.Header.Set("X-Cache-Hit", "TRUE") - ctx.SetContentType("image/png") - ctx.SetBody(cache) + if ok { + if opts.Download { + ctx.Response.Header.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s.png"`, user)) + } - return + if util.Debug { + log.Printf("Retrieved cache for face render for '%s'\n", uuid) + } + + ctx.Response.Header.Set("X-Cache-Hit", "TRUE") + ctx.SetContentType("image/png") + ctx.SetBody(cache) + + return + } } - skin, slim, err := util.GetPlayerSkin(uuid) + rawSkin, slim, err := util.GetPlayerSkin(uuid) if err != nil { log.Println(err) @@ -77,16 +75,16 @@ func FaceHandler(ctx *fasthttp.RequestCtx) { return } - if skin == nil { - skin = util.GetDefaultSkin(slim) - } - - render := renders.RenderFace(skin, renders.RenderOptions{ - Overlay: overlay, + render := skin.RenderFace(rawSkin, skin.Options{ + Overlay: opts.Overlay, Slim: slim, - Scale: scale, + Scale: opts.Scale, }) + if util.Debug { + log.Printf("Rendered face image for '%s'\n", uuid) + } + data, err := util.EncodePNG(render) if err != nil { @@ -98,7 +96,7 @@ func FaceHandler(ctx *fasthttp.RequestCtx) { return } - if err = r.Set(cacheKey, data, time.Duration(config.Cache.RenderCacheDuration)*time.Second); err != nil { + if err = r.Set(cacheKey, data, config.Cache.RenderCacheDuration); err != nil { log.Println(err) ctx.SetStatusCode(http.StatusInternalServerError) @@ -107,7 +105,7 @@ func FaceHandler(ctx *fasthttp.RequestCtx) { return } - if download { + if opts.Download { ctx.Response.Header.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s.png"`, user)) } diff --git a/src/routes/head.go b/src/routes/head.go index fcf025f..10fbcdc 100644 --- a/src/routes/head.go +++ b/src/routes/head.go @@ -4,33 +4,18 @@ import ( "fmt" "log" "net/http" - "time" "github.com/mineatar-io/api-server/src/util" - "github.com/mineatar-io/api-server/src/util/renders" + "github.com/mineatar-io/skin-render" "github.com/valyala/fasthttp" ) func HeadHandler(ctx *fasthttp.RequestCtx) { user := ctx.UserValue("user").(string) - download := ctx.QueryArgs().GetBool("download") + opts := util.ParseQueryParams(ctx, config.Routes.Head) - scale, err := ctx.QueryArgs().GetUint("scale") - - if err != nil { - scale = config.Routes.Head.DefaultScale - } - - scale = util.Clamp(scale, config.Routes.Head.MinScale, config.Routes.Head.MaxScale) - - overlay := true - - if ctx.QueryArgs().Has("overlay") { - overlay = ctx.QueryArgs().GetBool("overlay") - } - - uuid, err := util.GetUUID(user) + uuid, ok, err := util.LookupUUID(user) if err != nil { log.Println(err) @@ -41,32 +26,45 @@ func HeadHandler(ctx *fasthttp.RequestCtx) { return } - cacheKey := fmt.Sprintf("result:head-%d-%t-%s", scale, overlay, uuid) - - cache, ok, err := r.GetBytes(cacheKey) - - if err != nil { - log.Println(err) - - ctx.SetStatusCode(http.StatusInternalServerError) - ctx.SetBodyString(http.StatusText(http.StatusInternalServerError)) + if !ok && !opts.Fallback { + ctx.SetStatusCode(http.StatusNotFound) + ctx.SetBodyString(http.StatusText(http.StatusNotFound)) return } - if ok { - if download { - ctx.Response.Header.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s.png"`, user)) + cacheKey := fmt.Sprintf("result:head-%d-%t-%s", opts.Scale, opts.Overlay, uuid) + + { + cache, ok, err := r.GetBytes(cacheKey) + + if err != nil { + log.Println(err) + + ctx.SetStatusCode(http.StatusInternalServerError) + ctx.SetBodyString(http.StatusText(http.StatusInternalServerError)) + + return } - ctx.Response.Header.Set("X-Cache-Hit", "TRUE") - ctx.SetContentType("image/png") - ctx.SetBody(cache) + if ok { + if opts.Download { + ctx.Response.Header.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s.png"`, user)) + } - return + if util.Debug { + log.Printf("Retrieved cache for head render for '%s'\n", uuid) + } + + ctx.Response.Header.Set("X-Cache-Hit", "TRUE") + ctx.SetContentType("image/png") + ctx.SetBody(cache) + + return + } } - skin, slim, err := util.GetPlayerSkin(uuid) + rawSkin, slim, err := util.GetPlayerSkin(uuid) if err != nil { log.Println(err) @@ -77,16 +75,16 @@ func HeadHandler(ctx *fasthttp.RequestCtx) { return } - if skin == nil { - skin = util.GetDefaultSkin(slim) - } - - render := renders.RenderHead(skin, renders.RenderOptions{ - Overlay: overlay, + render := skin.RenderHead(rawSkin, skin.Options{ + Overlay: opts.Overlay, Slim: slim, - Scale: scale, + Scale: opts.Scale, }) + if util.Debug { + log.Printf("Rendered head image for '%s'\n", uuid) + } + data, err := util.EncodePNG(render) if err != nil { @@ -98,7 +96,7 @@ func HeadHandler(ctx *fasthttp.RequestCtx) { return } - if err = r.Set(cacheKey, data, time.Duration(config.Cache.RenderCacheDuration)*time.Second); err != nil { + if err = r.Set(cacheKey, data, config.Cache.RenderCacheDuration); err != nil { log.Println(err) ctx.SetStatusCode(http.StatusInternalServerError) @@ -107,7 +105,7 @@ func HeadHandler(ctx *fasthttp.RequestCtx) { return } - if download { + if opts.Download { ctx.Response.Header.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s.png"`, user)) } diff --git a/src/routes/skin.go b/src/routes/skin.go index 35dbe73..11b636d 100644 --- a/src/routes/skin.go +++ b/src/routes/skin.go @@ -12,9 +12,9 @@ import ( func SkinHandler(ctx *fasthttp.RequestCtx) { user := ctx.UserValue("user").(string) - download := ctx.QueryArgs().GetBool("download") + opts := util.ParseQueryParams(ctx, config.Routes.RawSkin) - uuid, err := util.GetUUID(user) + uuid, ok, err := util.LookupUUID(user) if err != nil { log.Println(err) @@ -25,7 +25,14 @@ func SkinHandler(ctx *fasthttp.RequestCtx) { return } - skin, slim, err := util.GetPlayerSkin(uuid) + if !ok && !opts.Fallback { + ctx.SetStatusCode(http.StatusNotFound) + ctx.SetBodyString(http.StatusText(http.StatusNotFound)) + + return + } + + rawSkin, _, err := util.GetPlayerSkin(uuid) if err != nil { log.Println(err) @@ -36,11 +43,7 @@ func SkinHandler(ctx *fasthttp.RequestCtx) { return } - if skin == nil { - skin = util.GetDefaultSkin(slim) - } - - data, err := util.EncodePNG(skin) + data, err := util.EncodePNG(rawSkin) if err != nil { log.Println(err) @@ -51,7 +54,7 @@ func SkinHandler(ctx *fasthttp.RequestCtx) { return } - if download { + if opts.Download { ctx.Response.Header.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s.png"`, user)) } diff --git a/src/routes/uuid.go b/src/routes/uuid.go index cca92be..2683272 100644 --- a/src/routes/uuid.go +++ b/src/routes/uuid.go @@ -11,7 +11,7 @@ import ( func UUIDHandler(ctx *fasthttp.RequestCtx) { user := ctx.UserValue("user").(string) - uuid, err := util.GetUUID(user) + uuid, ok, err := util.LookupUUID(user) if err != nil { log.Println(err) @@ -22,7 +22,7 @@ func UUIDHandler(ctx *fasthttp.RequestCtx) { return } - if len(uuid) < 1 { + if !ok { ctx.SetStatusCode(404) ctx.SetBodyString(http.StatusText(http.StatusNotFound)) diff --git a/src/util/alex.png b/src/util/alex.png deleted file mode 100644 index ffd8e07..0000000 Binary files a/src/util/alex.png and /dev/null differ diff --git a/src/util/image.go b/src/util/image.go deleted file mode 100644 index 409ee32..0000000 --- a/src/util/image.go +++ /dev/null @@ -1,17 +0,0 @@ -package util - -import ( - "bytes" - "image" - "image/png" -) - -func EncodePNG(img image.Image) ([]byte, error) { - buf := &bytes.Buffer{} - - if err := png.Encode(buf, img); err != nil { - return nil, err - } - - return buf.Bytes(), nil -} diff --git a/src/util/init.go b/src/util/init.go index 3cdba6e..95267e5 100644 --- a/src/util/init.go +++ b/src/util/init.go @@ -1,14 +1,11 @@ package util import ( - "os" - "github.com/mineatar-io/api-server/src/conf" "github.com/mineatar-io/api-server/src/redis" ) var ( - Debug = os.Getenv("DEBUG") == "true" config *conf.Configuration r *redis.Redis ) diff --git a/src/util/limit.go b/src/util/limit.go deleted file mode 100644 index 1ea575b..0000000 --- a/src/util/limit.go +++ /dev/null @@ -1,15 +0,0 @@ -package util - -// This is used instead of `math.Min/Max` because of the -// unnecessary coercion from/to float64. -func Clamp(value, min, max int) int { - if value > max { - return max - } - - if value < min { - return min - } - - return value -} diff --git a/src/util/renders/backbody.go b/src/util/renders/backbody.go deleted file mode 100644 index 5bb3619..0000000 --- a/src/util/renders/backbody.go +++ /dev/null @@ -1,59 +0,0 @@ -package renders - -import ( - "image" -) - -func RenderBackBody(skin *image.NRGBA, opts RenderOptions) *image.NRGBA { - slimOffset := GetSlimOffset(opts.Slim) - - var ( - backHead *image.NRGBA = RemoveTransparency(Extract(skin, 24, 8, 8, 8)) - backTorso *image.NRGBA = RemoveTransparency(Extract(skin, 32, 20, 8, 12)) - backLeftArm *image.NRGBA = nil - backRightArm *image.NRGBA = RemoveTransparency(Extract(skin, 52-slimOffset, 20, 4-slimOffset, 12)) - backLeftLeg *image.NRGBA = nil - backRightLeg *image.NRGBA = RemoveTransparency(Extract(skin, 12, 20, 4, 12)) - ) - - if IsOldSkin(skin) { - backLeftArm = FlipHorizontal(backRightArm) - backLeftLeg = FlipHorizontal(backRightLeg) - } else { - backLeftArm = RemoveTransparency(Extract(skin, 44-slimOffset, 52, 4-slimOffset, 12)) - backLeftLeg = RemoveTransparency(Extract(skin, 28, 52, 4, 12)) - - if opts.Overlay { - overlaySkin := FixTransparency(skin) - - backHead = Composite(backHead, Extract(overlaySkin, 56, 8, 8, 8), 0, 0) - backTorso = Composite(backTorso, Extract(overlaySkin, 32, 36, 8, 12), 0, 0) - backLeftArm = Composite(backLeftArm, Extract(overlaySkin, 60-slimOffset, 52, 4-slimOffset, 64), 0, 0) - backRightArm = Composite(backRightArm, Extract(overlaySkin, 52-slimOffset, 36, 4-slimOffset, 48), 0, 0) - backLeftLeg = Composite(backLeftLeg, Extract(overlaySkin, 12, 52, 8, 64), 0, 0) - backRightLeg = Composite(backRightLeg, Extract(overlaySkin, 12, 36, 8, 48), 0, 0) - } - } - - output := image.NewNRGBA(image.Rect(0, 0, 16, 32)) - - // Face - output = Composite(output, backHead, 4, 0) - - // Torso - output = Composite(output, backTorso, 4, 8) - - // Left Arm - output = Composite(output, backLeftArm, slimOffset, 8) - - // Right Arm - output = Composite(output, backRightArm, 12, 8) - - // Left Leg - output = Composite(output, backLeftLeg, 4, 20) - - // Right Leg - output = Composite(output, backRightLeg, 8, 20) - - return Scale(output, opts.Scale) -} diff --git a/src/util/renders/body.go b/src/util/renders/body.go deleted file mode 100644 index 9d55f9c..0000000 --- a/src/util/renders/body.go +++ /dev/null @@ -1,90 +0,0 @@ -package renders - -import "image" - -func RenderBody(skin *image.NRGBA, opts RenderOptions) *image.NRGBA { - scale := float64(opts.Scale) - slimOffset := GetSlimOffset(opts.Slim) - - output := image.NewNRGBA(image.Rect(0, 0, 20*opts.Scale, 45*opts.Scale+int(scale*(1.0/16.0)))) - - var ( - frontHead *image.NRGBA = RemoveTransparency(Extract(skin, 8, 8, 8, 8)) - topHead *image.NRGBA = RemoveTransparency(Extract(skin, 8, 0, 8, 8)) - rightHead *image.NRGBA = RemoveTransparency(Extract(skin, 0, 8, 8, 8)) - frontTorso *image.NRGBA = RemoveTransparency(Extract(skin, 20, 20, 8, 12)) - frontLeftArm *image.NRGBA = nil - topLeftArm *image.NRGBA = nil - frontRightArm *image.NRGBA = RemoveTransparency(Extract(skin, 44, 20, 4-slimOffset, 12)) - topRightArm *image.NRGBA = RemoveTransparency(Extract(skin, 44, 16, 4-slimOffset, 4)) - rightRightArm *image.NRGBA = RemoveTransparency(Extract(skin, 40, 20, 4, 12)) - frontLeftLeg *image.NRGBA = nil - frontRightLeg *image.NRGBA = RemoveTransparency(Extract(skin, 4, 20, 4, 12)) - rightRightLeg *image.NRGBA = RemoveTransparency(Extract(skin, 0, 20, 4, 12)) - ) - - if IsOldSkin(skin) { - frontLeftArm = FlipHorizontal(frontRightArm) - topLeftArm = FlipHorizontal(topRightArm) - frontLeftLeg = FlipHorizontal(frontRightLeg) - } else { - frontLeftArm = RemoveTransparency(Extract(skin, 36, 52, 4-slimOffset, 12)) - topLeftArm = RemoveTransparency(Extract(skin, 36, 48, 4-slimOffset, 4)) - frontLeftLeg = RemoveTransparency(Extract(skin, 20, 52, 4, 12)) - - if opts.Overlay { - overlaySkin := FixTransparency(skin) - - frontHead = Composite(frontHead, Extract(overlaySkin, 40, 8, 8, 8), 0, 0) - topHead = Composite(topHead, Extract(overlaySkin, 40, 0, 8, 8), 0, 0) - rightHead = Composite(rightHead, Extract(overlaySkin, 32, 8, 8, 8), 0, 0) - frontTorso = Composite(frontTorso, Extract(overlaySkin, 20, 36, 8, 12), 0, 0) - frontLeftArm = Composite(frontLeftArm, Extract(overlaySkin, 52, 52, 4-slimOffset, 64), 0, 0) - topLeftArm = Composite(topLeftArm, Extract(overlaySkin, 52, 48, 4-slimOffset, 4), 0, 0) - frontRightArm = Composite(frontRightArm, Extract(overlaySkin, 44, 36, 4-slimOffset, 48), 0, 0) - topRightArm = Composite(topRightArm, Extract(overlaySkin, 44, 48, 4-slimOffset, 4), 0, 0) - rightRightArm = Composite(rightRightArm, Extract(overlaySkin, 40, 36, 4, 12), 0, 0) - frontLeftLeg = Composite(frontLeftLeg, Extract(overlaySkin, 4, 52, 4, 12), 0, 0) - frontRightLeg = Composite(frontRightLeg, Extract(overlaySkin, 4, 36, 4, 12), 0, 0) - rightRightLeg = Composite(rightRightLeg, Extract(overlaySkin, 0, 36, 4, 12), 0, 0) - } - } - - // Right Side of Right Leg - output = CompositeTransform(output, Scale(rightRightLeg, opts.Scale), TransformRight, 4*scale, 23*scale) - - // Front of Right Leg - output = CompositeTransform(output, Scale(frontRightLeg, opts.Scale), TransformForward, 8*scale, 31*scale) - - // Front of Left Leg - output = CompositeTransform(output, Scale(frontLeftLeg, opts.Scale), TransformForward, 12*scale, 31*scale) - - // Front of Torso - output = CompositeTransform(output, Scale(frontTorso, opts.Scale), TransformForward, 8*scale, 19*scale) - - // Front of Right Arm - output = CompositeTransform(output, Scale(frontRightArm, opts.Scale), TransformForward, float64(4+slimOffset)*scale, 19*scale-1) - - // Front of Left Arm - output = CompositeTransform(output, Scale(frontLeftArm, opts.Scale), TransformForward, 16*scale, 21*scale-1) - - // Top of Left Arm - output = CompositeTransform(output, Scale(topLeftArm, opts.Scale), TransformUp, -5*scale, 17*scale) - - // Right Side of Right Arm - output = CompositeTransform(output, Scale(rightRightArm, opts.Scale), TransformRight, float64(slimOffset)*scale, float64(15-slimOffset)*scale) - - // Top of Right Arm - output = CompositeTransform(output, Scale(topRightArm, opts.Scale), TransformUp, float64(-15+slimOffset)*scale, 15*scale) - - // Front of Head - output = CompositeTransform(output, Scale(frontHead, opts.Scale), TransformForward, 10*scale, 13*scale-1) - - // Top of Head - output = CompositeTransform(output, Scale(topHead, opts.Scale), TransformUp, -3*scale, 5*scale) - - // Right Side of Head - output = CompositeTransform(output, Scale(rightHead, opts.Scale), TransformRight, 2*scale, 3*scale) - - return output -} diff --git a/src/util/renders/face.go b/src/util/renders/face.go deleted file mode 100644 index 9c46f19..0000000 --- a/src/util/renders/face.go +++ /dev/null @@ -1,13 +0,0 @@ -package renders - -import "image" - -func RenderFace(skin *image.NRGBA, opts RenderOptions) *image.NRGBA { - output := RemoveTransparency(Extract(skin, 8, 8, 8, 8)) - - if opts.Overlay && !IsOldSkin(skin) { - output = Composite(output, Extract(skin, 40, 8, 8, 8), 0, 0) - } - - return Scale(output, opts.Scale) -} diff --git a/src/util/renders/frontbody.go b/src/util/renders/frontbody.go deleted file mode 100644 index c367ec2..0000000 --- a/src/util/renders/frontbody.go +++ /dev/null @@ -1,59 +0,0 @@ -package renders - -import ( - "image" -) - -func RenderFrontBody(skin *image.NRGBA, opts RenderOptions) *image.NRGBA { - slimOffset := GetSlimOffset(opts.Slim) - - var ( - frontHead *image.NRGBA = RemoveTransparency(Extract(skin, 8, 8, 8, 8)) - frontTorso *image.NRGBA = RemoveTransparency(Extract(skin, 20, 20, 8, 12)) - leftArm *image.NRGBA = nil - rightArm *image.NRGBA = RemoveTransparency(Extract(skin, 44, 20, 4-slimOffset, 12)) - leftLeg *image.NRGBA = nil - rightLeg *image.NRGBA = RemoveTransparency(Extract(skin, 4, 20, 4, 12)) - ) - - if IsOldSkin(skin) { - leftArm = FlipHorizontal(rightArm) - leftLeg = FlipHorizontal(rightLeg) - } else { - leftArm = RemoveTransparency(Extract(skin, 36, 52, 4-slimOffset, 12)) - leftLeg = RemoveTransparency(Extract(skin, 20, 52, 4, 12)) - - if opts.Overlay { - overlaySkin := FixTransparency(skin) - - frontHead = Composite(frontHead, Extract(overlaySkin, 40, 8, 8, 8), 0, 0) - frontTorso = Composite(frontTorso, Extract(overlaySkin, 20, 36, 8, 12), 0, 0) - leftArm = Composite(leftArm, Extract(overlaySkin, 52, 52, 4-slimOffset, 64), 0, 0) - rightArm = Composite(rightArm, Extract(overlaySkin, 44, 36, 4-slimOffset, 48), 0, 0) - leftLeg = Composite(leftLeg, Extract(overlaySkin, 4, 52, 4, 12), 0, 0) - rightLeg = Composite(rightLeg, Extract(overlaySkin, 4, 36, 4, 12), 0, 0) - } - } - - output := image.NewNRGBA(image.Rect(0, 0, 16, 32)) - - // Face - output = Composite(output, frontHead, 4, 0) - - // Torso - output = Composite(output, frontTorso, 4, 8) - - // Left Arm - output = Composite(output, leftArm, 12, 8) - - // Right Arm - output = Composite(output, rightArm, slimOffset, 8) - - // Left Leg - output = Composite(output, leftLeg, 8, 20) - - // Right Leg - output = Composite(output, rightLeg, 4, 20) - - return Scale(output, opts.Scale) -} diff --git a/src/util/renders/head.go b/src/util/renders/head.go deleted file mode 100644 index f3e3c2d..0000000 --- a/src/util/renders/head.go +++ /dev/null @@ -1,33 +0,0 @@ -package renders - -import "image" - -func RenderHead(skin *image.NRGBA, opts RenderOptions) *image.NRGBA { - scale := float64(opts.Scale) - output := image.NewNRGBA(image.Rect(0, 0, 16*opts.Scale, 19*opts.Scale-int(scale/2.0)-1)) - - var ( - frontHead *image.NRGBA = RemoveTransparency(Extract(skin, 8, 8, 8, 8)) - topHead *image.NRGBA = RemoveTransparency(Extract(skin, 8, 0, 8, 8)) - rightHead *image.NRGBA = RemoveTransparency(Extract(skin, 0, 8, 8, 8)) - ) - - if opts.Overlay && !IsOldSkin(skin) { - overlaySkin := FixTransparency(skin) - - frontHead = Composite(frontHead, Extract(overlaySkin, 40, 8, 8, 8), 0, 0) - topHead = Composite(topHead, Extract(overlaySkin, 40, 0, 8, 8), 0, 0) - rightHead = Composite(rightHead, Extract(overlaySkin, 32, 8, 8, 8), 0, 0) - } - - // Front Head - output = CompositeTransform(output, Scale(frontHead, opts.Scale), TransformForward, 8*scale, 12*scale-1) - - // Top Head - output = CompositeTransform(output, Scale(topHead, opts.Scale), TransformUp, -4*scale, 4*scale) - - // Right Head - output = CompositeTransform(output, Scale(rightHead, opts.Scale), TransformRight, 0, 4*scale) - - return output -} diff --git a/src/util/renders/leftbody.go b/src/util/renders/leftbody.go deleted file mode 100644 index 322d48d..0000000 --- a/src/util/renders/leftbody.go +++ /dev/null @@ -1,44 +0,0 @@ -package renders - -import ( - "image" -) - -func RenderLeftBody(skin *image.NRGBA, opts RenderOptions) *image.NRGBA { - slimOffset := GetSlimOffset(opts.Slim) - - var ( - leftHead *image.NRGBA = RemoveTransparency(Extract(skin, 16, 8, 8, 8)) - leftLeftArm *image.NRGBA = nil - leftLeftLeg *image.NRGBA = nil - ) - - if IsOldSkin(skin) { - leftLeftArm = FlipHorizontal(RemoveTransparency(Extract(skin, 40, 20, 4, 12))) - leftLeftLeg = FlipHorizontal(RemoveTransparency(Extract(skin, 0, 20, 4, 12))) - } else { - leftLeftArm = RemoveTransparency(Extract(skin, 40-slimOffset, 52, 4, 12)) - leftLeftLeg = RemoveTransparency(Extract(skin, 24, 52, 4, 12)) - - if opts.Overlay { - overlaySkin := FixTransparency(skin) - - leftHead = Composite(leftHead, Extract(overlaySkin, 48, 8, 8, 8), 0, 0) - leftLeftArm = Composite(leftLeftArm, Extract(overlaySkin, 56-slimOffset, 52, 4, 12), 0, 0) - leftLeftLeg = Composite(leftLeftLeg, Extract(overlaySkin, 8, 52, 4, 12), 0, 0) - } - } - - output := image.NewNRGBA(image.Rect(0, 0, 8, 32)) - - // Left Head - output = Composite(output, leftHead, 0, 0) - - // Left Arm - output = Composite(output, leftLeftArm, 2, 8) - - // Left Leg - output = Composite(output, leftLeftLeg, 2, 20) - - return Scale(output, opts.Scale) -} diff --git a/src/util/renders/matrix/transform.go b/src/util/renders/matrix/transform.go deleted file mode 100644 index aa00a49..0000000 --- a/src/util/renders/matrix/transform.go +++ /dev/null @@ -1,90 +0,0 @@ -package matrix - -import "math" - -// Credit: https://github.com/fogleman/gg - -type Matrix struct { - XX, YX, XY, YY, X0, Y0 float64 -} - -func Identity() Matrix { - return Matrix{ - 1, 0, - 0, 1, - 0, 0, - } -} - -func Translate(x, y float64) Matrix { - return Matrix{ - 1, 0, - 0, 1, - x, y, - } -} - -func Scale(x, y float64) Matrix { - return Matrix{ - x, 0, - 0, y, - 0, 0, - } -} - -func Rotate(angle float64) Matrix { - c := math.Cos(angle) - s := math.Sin(angle) - return Matrix{ - c, s, - -s, c, - 0, 0, - } -} - -func Shear(x, y float64) Matrix { - return Matrix{ - 1, y, - x, 1, - 0, 0, - } -} - -func (a Matrix) Multiply(b Matrix) Matrix { - return Matrix{ - a.XX*b.XX + a.YX*b.XY, - a.XX*b.YX + a.YX*b.YY, - a.XY*b.XX + a.YY*b.XY, - a.XY*b.YX + a.YY*b.YY, - a.X0*b.XX + a.Y0*b.XY + b.X0, - a.X0*b.YX + a.Y0*b.YY + b.Y0, - } -} - -func (a Matrix) TransformVector(x, y float64) (tx, ty float64) { - tx = a.XX*x + a.XY*y - ty = a.YX*x + a.YY*y - return -} - -func (a Matrix) TransformPoint(x, y float64) (tx, ty float64) { - tx = a.XX*x + a.XY*y + a.X0 - ty = a.YX*x + a.YY*y + a.Y0 - return -} - -func (a Matrix) Translate(x, y float64) Matrix { - return Translate(x, y).Multiply(a) -} - -func (a Matrix) Scale(x, y float64) Matrix { - return Scale(x, y).Multiply(a) -} - -func (a Matrix) Rotate(angle float64) Matrix { - return Rotate(angle).Multiply(a) -} - -func (a Matrix) Shear(x, y float64) Matrix { - return Shear(x, y).Multiply(a) -} diff --git a/src/util/renders/options.go b/src/util/renders/options.go deleted file mode 100644 index d8ce720..0000000 --- a/src/util/renders/options.go +++ /dev/null @@ -1,7 +0,0 @@ -package renders - -type RenderOptions struct { - Scale int - Overlay bool - Slim bool -} diff --git a/src/util/renders/rightbody.go b/src/util/renders/rightbody.go deleted file mode 100644 index e7457fe..0000000 --- a/src/util/renders/rightbody.go +++ /dev/null @@ -1,34 +0,0 @@ -package renders - -import ( - "image" -) - -func RenderRightBody(skin *image.NRGBA, opts RenderOptions) *image.NRGBA { - var ( - rightHead *image.NRGBA = RemoveTransparency(Extract(skin, 0, 8, 8, 8)) - rightRightArm *image.NRGBA = RemoveTransparency(Extract(skin, 40, 20, 4, 12)) - rightRightLeg *image.NRGBA = RemoveTransparency(Extract(skin, 0, 20, 4, 12)) - ) - - if opts.Overlay && !IsOldSkin(skin) { - overlaySkin := FixTransparency(skin) - - rightHead = Composite(rightHead, Extract(overlaySkin, 32, 8, 8, 8), 0, 0) - rightRightArm = Composite(rightRightArm, Extract(overlaySkin, 40, 36, 4, 12), 0, 0) - rightRightLeg = Composite(rightRightLeg, Extract(overlaySkin, 0, 36, 4, 12), 0, 0) - } - - output := image.NewNRGBA(image.Rect(0, 0, 8, 32)) - - // Right Head - output = Composite(output, rightHead, 0, 0) - - // Right Arm - output = Composite(output, rightRightArm, 2, 8) - - // Right Leg - output = Composite(output, rightRightLeg, 2, 20) - - return Scale(output, opts.Scale) -} diff --git a/src/util/renders/util.go b/src/util/renders/util.go deleted file mode 100644 index 03ce5fe..0000000 --- a/src/util/renders/util.go +++ /dev/null @@ -1,163 +0,0 @@ -package renders - -import ( - "image" - "image/draw" - - "github.com/mineatar-io/api-server/src/util/renders/matrix" - drw "golang.org/x/image/draw" - "golang.org/x/image/math/f64" -) - -var ( - SkewA float64 = 26.0 / 45.0 - SkewB float64 = SkewA * 2.0 - TransformForward matrix.Matrix = matrix.Matrix{ - XX: 1, YX: -SkewA, - XY: 0, YY: SkewB, - X0: 0, Y0: SkewA, - } - TransformUp matrix.Matrix = matrix.Matrix{ - XX: 1, YX: -SkewA, - XY: 1, YY: SkewA, - X0: 0, Y0: 0, - } - TransformRight matrix.Matrix = matrix.Matrix{ - XX: 1, YX: SkewA, - XY: 0, YY: SkewB, - X0: 0, Y0: 0, - } -) - -func Extract(img *image.NRGBA, x, y, width, height int) *image.NRGBA { - output := image.NewNRGBA(image.Rect(0, 0, width, height)) - - draw.Draw(output, output.Bounds(), img, image.Pt(x, y), draw.Src) - - return output -} - -func Scale(img *image.NRGBA, scale int) *image.NRGBA { - if scale == 1 { - return img - } - - bounds := img.Bounds().Max - output := image.NewNRGBA(image.Rect(0, 0, bounds.X*scale, bounds.Y*scale)) - - for x := 0; x < bounds.X; x++ { - for y := 0; y < bounds.Y; y++ { - color := img.At(x, y) - - for sx := 0; sx < scale; sx++ { - for sy := 0; sy < scale; sy++ { - output.Set(x*scale+sx, y*scale+sy, color) - } - } - } - } - - return output -} - -func RemoveTransparency(img *image.NRGBA) *image.NRGBA { - output := image.NewNRGBA(img.Bounds()) - - for i, l := 0, len(img.Pix); i < l; i += 4 { - output.Pix[i] = img.Pix[i] - output.Pix[i+1] = img.Pix[i+1] - output.Pix[i+2] = img.Pix[i+2] - output.Pix[i+3] = 255 - } - - return output -} - -func IsOldSkin(img *image.NRGBA) bool { - return img.Bounds().Max.Y < 64 -} - -func Composite(bottom, top *image.NRGBA, x, y int) *image.NRGBA { - output := image.NewNRGBA(bottom.Bounds()) - - topBounds := top.Bounds().Max - - draw.Draw(output, bottom.Bounds(), bottom, image.Pt(0, 0), draw.Src) - draw.Draw(output, image.Rect(0, 0, topBounds.X+x, topBounds.Y+y), top, image.Pt(-x, -y), draw.Over) - - return output -} - -func FlipHorizontal(img *image.NRGBA) *image.NRGBA { - data := img.Pix - bounds := img.Bounds() - - output := image.NewNRGBA(bounds) - - for x := 0; x < bounds.Max.X; x++ { - for y := 0; y < bounds.Max.Y; y++ { - fx := bounds.Max.X - x - 1 - fi := fx*4 + y*4*bounds.Max.X - ix := x*4 + y*4*bounds.Max.X - - for i := 0; i < 4; i++ { - output.Pix[ix+i] = data[fi+i] - } - } - } - - return output -} - -func FixTransparency(img *image.NRGBA) *image.NRGBA { - a := img.Pix[0:4] - - if a[3] == 0 { - return img - } - - output := Clone(img) - - for i, l := 0, len(output.Pix); i < l; i += 4 { - if output.Pix[i+0] != a[0] || output.Pix[i+1] != a[1] || output.Pix[i+2] != a[2] || output.Pix[i+3] != a[3] { - continue - } - - output.Pix[i+3] = 0 - } - - return output -} - -func Clone(img *image.NRGBA) *image.NRGBA { - bounds := img.Bounds() - output := image.NewNRGBA(bounds) - - draw.Draw(output, bounds, img, image.Pt(0, 0), draw.Src) - - return output -} - -func GetSlimOffset(slim bool) int { - if slim { - return 1 - } - - return 0 -} - -func CompositeTransform(bottom, top *image.NRGBA, mat matrix.Matrix, x, y float64) *image.NRGBA { - output := image.NewNRGBA(bottom.Bounds()) - - draw.Draw(output, bottom.Bounds(), bottom, image.Pt(0, 0), draw.Src) - - transformer := drw.NearestNeighbor - - fx, fy := float64(x), float64(y) - - m := mat.Translate(fx, fy) - - transformer.Transform(output, f64.Aff3{m.XX, m.XY, m.X0, m.YX, m.YY, m.Y0}, top, top.Bounds(), draw.Over, nil) - - return output -} diff --git a/src/util/skin.go b/src/util/skin.go deleted file mode 100644 index d990c09..0000000 --- a/src/util/skin.go +++ /dev/null @@ -1,47 +0,0 @@ -package util - -import ( - "bytes" - _ "embed" - "image" - "image/draw" - "image/png" - "log" -) - -var ( - //go:embed steve.png - rawSteveData []byte - - //go:embed alex.png - rawAlexData []byte - - steveSkin *image.NRGBA = image.NewNRGBA(image.Rect(0, 0, 64, 64)) - alexSkin *image.NRGBA = image.NewNRGBA(image.Rect(0, 0, 64, 64)) -) - -func init() { - rawSteveSkin, err := png.Decode(bytes.NewReader(rawSteveData)) - - if err != nil { - log.Fatal(err) - } - - draw.Draw(steveSkin, rawSteveSkin.Bounds(), rawSteveSkin, image.Pt(0, 0), draw.Src) - - rawAlexSkin, err := png.Decode(bytes.NewReader(rawAlexData)) - - if err != nil { - log.Fatal(err) - } - - draw.Draw(alexSkin, rawAlexSkin.Bounds(), rawAlexSkin, image.Pt(0, 0), draw.Src) -} - -func GetDefaultSkin(slim bool) *image.NRGBA { - if slim { - return alexSkin - } - - return steveSkin -} diff --git a/src/util/steve.png b/src/util/steve.png deleted file mode 100644 index 8d59ee2..0000000 Binary files a/src/util/steve.png and /dev/null differ diff --git a/src/util/util.go b/src/util/util.go new file mode 100644 index 0000000..8ff0ac2 --- /dev/null +++ b/src/util/util.go @@ -0,0 +1,271 @@ +package util + +import ( + "bytes" + "fmt" + "image" + "image/draw" + "image/png" + "log" + "net/http" + "os" + "strings" + "time" + + "github.com/mineatar-io/api-server/src/conf" + "github.com/mineatar-io/skin-render" + "github.com/mineatar-io/yggdrasil" + "github.com/valyala/fasthttp" +) + +var Debug bool = os.Getenv("DEBUG") == "true" + +type QueryParams struct { + Scale int + Download bool + Overlay bool + Fallback bool +} + +func FormatUUID(uuid string) string { + return strings.ToLower(strings.ReplaceAll(uuid, "-", "")) +} + +func LookupUUID(value string) (string, bool, error) { + value = FormatUUID(value) + + if len(value) == 32 { + return value, true, nil + } + + cacheKey := fmt.Sprintf("uuid:%s", value) + + cache, ok, err := r.GetString(cacheKey) + + if err != nil { + return "", false, err + } + + if ok { + if Debug { + log.Printf("Retrieved UUID from cache for '%s' (%s)\n", value, cache) + } + + return cache, true, nil + } + + profile, err := yggdrasil.UsernameToUUID(value) + + if err != nil { + return "", false, err + } + + if profile == nil { + if Debug { + log.Printf("Fetched UUID from Mojang for '%s', did not exist\n", value) + } + + return "", false, nil + } + + if err = r.Set(cacheKey, profile.UUID, config.Cache.UUIDCacheDuration); err != nil { + return "", true, err + } + + if Debug { + log.Printf("Fetched UUID from Mojang for '%s' (%s)\n", value, profile.UUID) + } + + return profile.UUID, true, nil +} + +func FetchImage(url string) (*image.NRGBA, error) { + resp, err := http.Get(url) + + if err != nil { + return nil, err + } + + if Debug { + log.Printf("Fetched image from URL: %s\n", url) + } + + defer resp.Body.Close() + + img, format, err := image.Decode(resp.Body) + + if err != nil { + return nil, err + } + + if format != "NRGBA" { + outputImg := image.NewNRGBA(img.Bounds()) + + draw.Draw(outputImg, img.Bounds(), img, image.Pt(0, 0), draw.Src) + + return outputImg, nil + } + + return img.(*image.NRGBA), nil +} + +func GetPlayerSkin(uuid string) (*image.NRGBA, bool, error) { + uuid = FormatUUID(uuid) + + if len(uuid) < 1 { + return skin.GetDefaultSkin(false), false, nil + } + + 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 Debug { + log.Printf("Retrieved skin for '%s' (slim: %t) from cache\n", uuid, slim) + } + + return cache, slim, nil + } + + textures, err := yggdrasil.GetProfileTextures(uuid) + + if err != nil { + return nil, false, err + } + + if textures == nil { + if Debug { + log.Printf("Fetched textures for '%s' from Mojang, none exists, using default skin\n", uuid) + } + + slim := skin.IsSlimFromUUID(uuid) + + return skin.GetDefaultSkin(slim), slim, nil + } + + if Debug { + log.Printf("Fetched textures for '%s' from Mojang\n", uuid) + } + + value := "" + + for _, property := range textures.Properties { + if property.Name != "textures" { + continue + } + + value = property.Value + } + + if len(value) < 1 { + slim := skin.IsSlimFromUUID(uuid) + + return skin.GetDefaultSkin(slim), slim, nil + } + + texturesResult, err := yggdrasil.GetDecodedTexturesValue(value) + + if err != nil { + return nil, false, err + } + + if len(texturesResult.Textures.Skin.URL) < 1 { + slim := skin.IsSlimFromUUID(uuid) + + return skin.GetDefaultSkin(slim), slim, 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 err = r.Set(fmt.Sprintf("skin:%s", uuid), encodedSkin, time.Duration(config.Cache.SkinCacheDuration)*time.Second); err != nil { + return nil, false, err + } + + if slim { + if err = r.Set(fmt.Sprintf("slim:%s", uuid), "true", time.Duration(config.Cache.SkinCacheDuration)*time.Second); err != nil { + return nil, false, err + } + } else { + if err = r.Delete(fmt.Sprintf("slim:%s", uuid)); err != nil { + return nil, false, err + } + } + + return skin, slim, nil +} + +// This is used instead of `math.Min/Max` because of the +// unnecessary coercion from/to float64. +func Clamp(value, min, max int) int { + if value > max { + return max + } + + if value < min { + return min + } + + return value +} + +func EncodePNG(img image.Image) ([]byte, error) { + buf := &bytes.Buffer{} + + if err := png.Encode(buf, img); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +func ParseQueryParams(ctx *fasthttp.RequestCtx, route conf.RouteConfig) *QueryParams { + args := ctx.QueryArgs() + + response := &QueryParams{ + Scale: route.DefaultScale, + Download: route.DefaultDownload, + Overlay: route.DefaultOverlay, + Fallback: route.DefaultFallback, + } + + if args.Has("scale") { + if scale, err := args.GetUint("scale"); err == nil { + response.Scale = Clamp(scale, route.MinScale, route.MaxScale) + } + } + + if args.Has("overlay") { + response.Overlay = args.GetBool("overlay") + } + + if args.Has("fallback") { + response.Fallback = args.GetBool("fallback") + } + + if args.Has("download") { + response.Download = args.GetBool("download") + } + + return response +} diff --git a/src/util/uuid.go b/src/util/uuid.go deleted file mode 100644 index c462609..0000000 --- a/src/util/uuid.go +++ /dev/null @@ -1,50 +0,0 @@ -package util - -import ( - "fmt" - "log" - "strings" - "time" -) - -func GetUUID(value string) (string, error) { - value = strings.ToLower(strings.ReplaceAll(value, "-", "")) - - if len(value) == 32 { - return value, nil - } - - cache, ok, err := r.GetString(fmt.Sprintf("uuid:%s", value)) - - if err != nil { - return "", err - } - - if ok { - if Debug { - log.Printf("[DEBUG] Retrieved UUID for player %s (%s) from cache\n", value, cache) - } - - return cache, nil - } - - profile, err := GetProfileByUsername(value) - - if err != nil { - return "", err - } - - if profile == nil { - return "", nil - } - - if err = r.Set(fmt.Sprintf("uuid:%s", value), profile.ID, time.Duration(config.Cache.UUIDCacheDuration)*time.Second); err != nil { - return "", err - } - - if Debug { - log.Printf("[DEBUG] Fetched UUID for player %s (%s) from Mojang\n", profile.Name, profile.ID) - } - - return profile.ID, nil -} diff --git a/src/util/yggdrasil.go b/src/util/yggdrasil.go deleted file mode 100644 index 03257de..0000000 --- a/src/util/yggdrasil.go +++ /dev/null @@ -1,242 +0,0 @@ -package util - -import ( - "bytes" - "encoding/base64" - "encoding/json" - "fmt" - "image" - "image/draw" - "io/ioutil" - "log" - "net/http" - "time" -) - -type BasicProfile struct { - Name string `json:"name"` - ID string `json:"id"` -} - -type Profile struct { - ID string `json:"id"` - Name string `json:"name"` - Properties []struct { - Name string `json:"name"` - Value string `json:"value"` - Signature string `json:"signature"` - } `yaml:"properties"` -} - -type Textures struct { - Textures map[string]struct { - URL string `json:"url"` - Metadata struct { - Model string `json:"model"` - } `json:"metadata"` - } `json:"textures"` -} - -func GetProfileByUsername(username string) (*BasicProfile, error) { - resp, err := http.Get(fmt.Sprintf("https://api.mojang.com/users/profiles/minecraft/%s", username)) - - if err != nil { - return nil, err - } - - if resp.StatusCode != 200 { - if resp.StatusCode == 204 { - return nil, nil - } - - return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) - } - - defer resp.Body.Close() - - result := &BasicProfile{} - - body, err := ioutil.ReadAll(resp.Body) - - if err != nil { - return nil, err - } - - if err = json.Unmarshal(body, result); err != nil { - return nil, err - } - - return result, nil -} - -func GetPlayerProfile(uuid string) (*Profile, error) { - resp, err := http.Get(fmt.Sprintf("https://sessionserver.mojang.com/session/minecraft/profile/%s", uuid)) - - if err != nil { - return nil, err - } - - if resp.StatusCode != 200 { - if resp.StatusCode == 404 { - return nil, nil - } - - return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) - } - - defer resp.Body.Close() - - result := &Profile{} - - body, err := ioutil.ReadAll(resp.Body) - - if err != nil { - return nil, err - } - - if err = json.Unmarshal(body, result); err != nil { - return nil, err - } - - return result, nil -} - -func GetPlayerSkin(uuid string) (*image.NRGBA, bool, error) { - if len(uuid) < 1 { - return GetDefaultSkin(false), false, nil - } - - cache, ok, err := r.GetBytes(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 - } - - img, format, err := image.Decode(bytes.NewReader(cache)) - - if err != nil { - return nil, false, err - } - - if Debug { - log.Printf("[DEBUG] Retrieved skin for %s from cache\n", uuid) - } - - if format != "NRGBA" { - newImage := image.NewNRGBA(img.Bounds()) - - draw.Draw(newImage, img.Bounds(), img, image.Pt(0, 0), draw.Src) - - return newImage, slim, nil - } - - return img.(*image.NRGBA), slim, nil - } - - profile, err := GetPlayerProfile(uuid) - - if err != nil { - return nil, false, err - } - - if profile == nil { - return nil, false, nil - } - - var value string - - for _, property := range profile.Properties { - if property.Name != "textures" { - continue - } - - value = property.Value - } - - if len(value) < 1 { - return nil, false, nil - } - - propertyValue, err := base64.StdEncoding.DecodeString(value) - - if err != nil { - return nil, false, err - } - - textures := &Textures{} - - if err = json.Unmarshal(propertyValue, textures); err != nil { - return nil, false, err - } - - skinProperty, ok := textures.Textures["SKIN"] - - if !ok { - return nil, false, nil - } - - slim := skinProperty.Metadata.Model == "slim" - - resp, err := http.Get(skinProperty.URL) - - if err != nil { - return nil, false, err - } - - if resp.StatusCode != 200 { - if resp.StatusCode == 404 { - return nil, slim, nil - } - - return nil, false, fmt.Errorf("unexpected status code: %d", resp.StatusCode) - } - - defer resp.Body.Close() - - body, err := ioutil.ReadAll(resp.Body) - - if err != nil { - return nil, false, err - } - - img, format, err := image.Decode(bytes.NewReader(body)) - - if err != nil { - return nil, false, err - } - - if slim { - if err = r.Set(fmt.Sprintf("slim:%s", uuid), "true", time.Duration(config.Cache.SkinCacheDuration)*time.Second); err != nil { - return nil, false, err - } - } else { - if err = r.Delete(fmt.Sprintf("slim:%s", uuid)); err != nil { - return nil, false, err - } - } - - if err = r.Set(fmt.Sprintf("skin:%s", uuid), body, time.Duration(config.Cache.SkinCacheDuration)*time.Second); err != nil { - return nil, false, err - } - - if Debug { - log.Printf("[DEBUG] Fetched skin for %s from Mojang\n", uuid) - } - - if format != "NRGBA" { - newImage := image.NewNRGBA(img.Bounds()) - - draw.Draw(newImage, img.Bounds(), img, image.Pt(0, 0), draw.Src) - - return newImage, slim, nil - } - - return img.(*image.NRGBA), slim, nil -}