diff --git a/README.md b/README.md index 004556f..fffc3c6 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@
The Dofus encyclopedia API.
+Unofficial Dofus Encyclopedia API.
diff --git a/error.go b/error.go new file mode 100644 index 0000000..c7d2894 --- /dev/null +++ b/error.go @@ -0,0 +1,75 @@ +package main + +import ( + "encoding/json" + "net/http" + + "github.com/charmbracelet/log" +) + +var ( + ERR_INVALID_FILTER_VALUE = "INVALID_FILTER_NAME" + ERR_INVALID_FILTER_VALUE_MESSAGE = "The filter value you provided is not valid. Please check the format and try again." + + ERR_INVALID_QUERY_VALUE = "INVALID_QUERY_PARAMETER" + ERR_INVALID_QUERY_MESSAGE = "The query parameter you provided is not valid. Please check the format and try again." + + ERR_INVALID_JSON_BODY = "INVALID_JSON_BODY" + ERR_INVALID_JSON_MESSAGE = "The JSON body you provided is not valid. Please check the format and try again." + + ERR_SERVER_ERROR = "SERVER_ERROR" + ERR_SERVER_MESSAGE = "A server error occurred. This is not your fault. Please try again later and contact the administrator." + + ERR_NOT_FOUND = "NOT_FOUND" + ERR_NOT_FOUND_MESSAGE = "The requested resource was not found." +) + +type ApiError struct { + Status int `json:"status"` + Error string `json:"error"` + Code string `json:"code"` + Message string `json:"message"` + Details string `json:"details,omitempty"` +} + +func writeNotFoundResponse(w http.ResponseWriter, details string) { + writeErrorResponse(w, http.StatusNotFound, ERR_NOT_FOUND, ERR_NOT_FOUND_MESSAGE, details) +} + +func writeServerErrorResponse(w http.ResponseWriter, details string) { + writeErrorResponse(w, http.StatusInternalServerError, ERR_SERVER_ERROR, ERR_SERVER_MESSAGE, details) +} + +func writeInvalidFilterResponse(w http.ResponseWriter, details string) { + writeErrorResponse(w, http.StatusBadRequest, ERR_INVALID_FILTER_VALUE, ERR_INVALID_FILTER_VALUE_MESSAGE, details) +} + +func writeInvalidQueryResponse(w http.ResponseWriter, details string) { + writeErrorResponse(w, http.StatusBadRequest, ERR_INVALID_QUERY_VALUE, ERR_INVALID_QUERY_MESSAGE, details) +} + +func writeInvalidJsonResponse(w http.ResponseWriter, details string) { + writeErrorResponse(w, http.StatusBadRequest, ERR_INVALID_JSON_BODY, ERR_INVALID_JSON_MESSAGE, details) +} + +func writeErrorResponse(w http.ResponseWriter, status int, code, message, details string) { + apiErr := ApiError{ + Status: status, + Error: http.StatusText(status), + Code: code, + Message: message, + Details: details, + } + + if status == http.StatusInternalServerError { + log.Error("Internal Server Error", "code", code, "message", message, "details", details) + } + + if status == http.StatusBadRequest { + log.Warn("Bad Request", "code", code, "message", message, "details", details) + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + json.NewEncoder(w).Encode(apiErr) +} diff --git a/go.mod b/go.mod index 211af0e..f8c7cdd 100644 --- a/go.mod +++ b/go.mod @@ -6,16 +6,16 @@ toolchain go1.23.0 require ( github.com/charmbracelet/bubbles v0.20.0 - github.com/charmbracelet/bubbletea v1.1.1 + github.com/charmbracelet/bubbletea v1.2.4 github.com/charmbracelet/lipgloss v1.0.0 github.com/charmbracelet/log v0.4.0 - github.com/dofusdude/ankabuffer v0.0.8 - github.com/dofusdude/dodumap v0.3.0 + github.com/dofusdude/ankabuffer v0.0.9 + github.com/dofusdude/dodumap v0.5.0 github.com/emirpasic/gods v1.18.1 github.com/go-chi/chi/v5 v5.1.0 github.com/hashicorp/go-memdb v1.3.4 github.com/joho/godotenv v1.5.1 - github.com/meilisearch/meilisearch-go v0.28.0 + github.com/meilisearch/meilisearch-go v0.29.0 github.com/prometheus/client_golang v1.20.5 github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.19.0 @@ -23,15 +23,16 @@ require ( ) require ( + github.com/andybalholm/brotli v1.1.1 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/charmbracelet/x/ansi v0.4.2 // indirect - github.com/charmbracelet/x/term v0.2.0 // indirect + github.com/charmbracelet/x/ansi v0.5.2 // indirect + github.com/charmbracelet/x/term v0.2.1 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect - github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/fsnotify/fsnotify v1.8.0 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect - github.com/golang-jwt/jwt/v4 v4.5.0 // indirect + github.com/golang-jwt/jwt/v4 v4.5.1 // indirect github.com/google/flatbuffers v24.3.25+incompatible // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect github.com/hashicorp/golang-lru v1.0.2 // indirect @@ -52,7 +53,7 @@ require ( github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.60.0 // indirect + github.com/prometheus/common v0.60.1 // indirect github.com/prometheus/procfs v0.15.1 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/sagikazarmark/locafero v0.6.0 // indirect @@ -64,11 +65,11 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.6.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect - golang.org/x/sync v0.8.0 // indirect - golang.org/x/sys v0.26.0 // indirect - golang.org/x/text v0.19.0 // indirect - google.golang.org/protobuf v1.35.1 // indirect + golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f // indirect + golang.org/x/sync v0.9.0 // indirect + golang.org/x/sys v0.27.0 // indirect + golang.org/x/text v0.20.0 // indirect + google.golang.org/protobuf v1.35.2 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 298e7b7..e5a1961 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,6 @@ +github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= +github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= +github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -6,39 +9,40 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE= github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU= -github.com/charmbracelet/bubbletea v1.1.1 h1:KJ2/DnmpfqFtDNVTvYZ6zpPFL9iRCRr0qqKOCvppbPY= -github.com/charmbracelet/bubbletea v1.1.1/go.mod h1:9Ogk0HrdbHolIKHdjfFpyXJmiCzGwy+FesYkZr7hYU4= +github.com/charmbracelet/bubbletea v1.2.4 h1:KN8aCViA0eps9SCOThb2/XPIlea3ANJLUkv3KnQRNCE= +github.com/charmbracelet/bubbletea v1.2.4/go.mod h1:Qr6fVQw+wX7JkWWkVyXYk/ZUQ92a6XNekLXa3rR18MM= github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg= github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo= github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM= github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM= -github.com/charmbracelet/x/ansi v0.4.2 h1:0JM6Aj/g/KC154/gOP4vfxun0ff6itogDYk41kof+qk= -github.com/charmbracelet/x/ansi v0.4.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= -github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0= -github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0= +github.com/charmbracelet/x/ansi v0.5.2 h1:dEa1x2qdOZXD/6439s+wF7xjV+kZLu/iN00GuXXrU9E= +github.com/charmbracelet/x/ansi v0.5.2/go.mod h1:KBUFw1la39nl0dLl10l5ORDAqGXaeurTQmwyyVKse/Q= +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dofusdude/ankabuffer v0.0.8 h1:sWcUweUA8MNSNBRStbb+LxxiI21gJckWhpLX21TEWuI= -github.com/dofusdude/ankabuffer v0.0.8/go.mod h1:thGiWt9q3jQ9HXF4toodro2t9cPSECFALSeVq2CEphg= -github.com/dofusdude/dodumap v0.3.0 h1:oJx57HhRPnBBHivFdK7t6xaB92ShXSAm2FdaAS6yR1Y= -github.com/dofusdude/dodumap v0.3.0/go.mod h1:51KG2eMd02UJnXErOubAukVftYuJproDHqJcbIHSzIE= +github.com/dofusdude/ankabuffer v0.0.9 h1:EDVBf60QUq2TQr2mEjHrauUrkM7E3kEY0VUqNGeHRaI= +github.com/dofusdude/ankabuffer v0.0.9/go.mod h1:H84vCl3zg8ibH+h6mFGvYxrLEIBOpnjYVi3WJXBFbNg= +github.com/dofusdude/dodumap v0.5.0 h1:wl5TaVr0A3ue2aYx9aTJYCEubCTG1aspR2h/YJ+IkH4= +github.com/dofusdude/dodumap v0.5.0/go.mod h1:51KG2eMd02UJnXErOubAukVftYuJproDHqJcbIHSzIE= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= +github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= -github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo= +github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/google/flatbuffers v24.3.25+incompatible h1:CX395cjN9Kke9mmalRoL3d81AtFUxJM+yDthflgJGkI= github.com/google/flatbuffers v24.3.25+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= @@ -82,8 +86,8 @@ github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2J github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/meilisearch/meilisearch-go v0.28.0 h1:f3XJ66ZM+R8bANAOLqsjvoq/HhQNpVJPYoNt6QgNzME= -github.com/meilisearch/meilisearch-go v0.28.0/go.mod h1:Szcc9CaDiKIfjdgdt49jlmDKpEzjD+x+b6Y6heMdlQ0= +github.com/meilisearch/meilisearch-go v0.29.0 h1:HZ9NEKN59USINQ/DXJge/aaXq8IrsKbXGTdAoBaaDz4= +github.com/meilisearch/meilisearch-go v0.29.0/go.mod h1:2cRCAn4ddySUsFfNDLVPod/plRibQsJkXF/4gLhxbOk= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= @@ -103,8 +107,8 @@ github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+ github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.60.0 h1:+V9PAREWNvJMAuJ1x1BaWl9dewMW4YrHZQbx0sJNllA= -github.com/prometheus/common v0.60.0/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw= +github.com/prometheus/common v0.60.1 h1:FUas6GcOw66yB/73KC+BOZoFJmbo/1pojoILArPAaSc= +github.com/prometheus/common v0.60.1/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -141,22 +145,24 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/zyedidia/generic v1.2.1 h1:Zv5KS/N2m0XZZiuLS82qheRG4X1o5gsWreGb0hR7XDc= github.com/zyedidia/generic v1.2.1/go.mod h1:ly2RBz4mnz1yeuVbQA/VFwGjK3mnHGRj1JuoG336Bis= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY= -golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8= -golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo= +golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak= +golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= +golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= -golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= -golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= -google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= +golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= +google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/handler.go b/handler.go index 2929974..0e6f37c 100644 --- a/handler.go +++ b/handler.go @@ -34,7 +34,7 @@ var ( mountAllowedExpandFields = []string{"effects"} setAllowedExpandFields = Concat(mountAllowedExpandFields, []string{"equipment_ids"}) itemAllowedExpandFields = Concat(mountAllowedExpandFields, []string{"recipe", "description", "conditions"}) - equipmentAllowedExpandFields = Concat(itemAllowedExpandFields, []string{"range", "parent_set", "is_weapon", "pods", "critical_hit_probability", "critical_hit_bonus", "is_two_handed", "max_cast_per_turn", "ap_cost"}) + equipmentAllowedExpandFields = Concat(itemAllowedExpandFields, []string{"range", "parent_set", "is_weapon", "pods", "critical_hit_probability", "critical_hit_bonus", "max_cast_per_turn", "ap_cost"}) ) func GetRecipeIfExists(itemId int, txn *memdb.Txn) (mapping.MappedMultilangRecipe, bool) { @@ -103,8 +103,7 @@ type UpdateMessage struct { func UpdateHandler(w http.ResponseWriter, r *http.Request) { var updateMessage UpdateMessage if err := json.NewDecoder(r.Body).Decode(&updateMessage); err != nil { - log.Error(err) - w.WriteHeader(http.StatusBadRequest) + writeInvalidJsonResponse(w, err.Error()) return } @@ -115,13 +114,12 @@ func UpdateHandler(w http.ResponseWriter, r *http.Request) { release = "main" } - ReleaseUrl = fmt.Sprintf("https://github.com/dofusdude/dofus2-%s/releases/download/%s", release, updateMessage.Version) + ReleaseUrl = fmt.Sprintf("https://github.com/dofusdude/dofus3-%s/releases/download/%s", release, updateMessage.Version) log.Info("Updating to version", updateMessage.Version) err := DownloadImages() if err != nil { - log.Error("Error while downloading images", "err", err) - w.WriteHeader(http.StatusInternalServerError) + writeServerErrorResponse(w, "Could not download images: "+err.Error()) return } @@ -185,11 +183,12 @@ func ListMounts(w http.ResponseWriter, r *http.Request) { lang := r.Context().Value("lang").(string) pagination := PageninationWithState(r.Context().Value("pagination").(string)) - filterFamilyName := r.URL.Query().Get("filter[family_name]") + filterFamilyName := r.URL.Query().Get("filter[family.name]") + filterFamilyIdStr := r.URL.Query().Get("filter[family.id]") expansionsParam := strings.ToLower(r.URL.Query().Get("fields[mount]")) expansions := parseFields(expansionsParam) if !validateFields(expansions, mountAllowedExpandFields) { - w.WriteHeader(http.StatusBadRequest) + writeInvalidQueryResponse(w, "fields[mount] has invalid fields.") return } @@ -198,7 +197,7 @@ func ListMounts(w http.ResponseWriter, r *http.Request) { it, err := txn.Get(fmt.Sprintf("%s-%s", CurrentRedBlueVersionStr(Version.MemDb), "mounts"), "id") if err != nil || it == nil { - w.WriteHeader(http.StatusNotFound) + writeNotFoundResponse(w, "No mounts found.") return } @@ -213,6 +212,16 @@ func ListMounts(w http.ResponseWriter, r *http.Request) { continue } } + if filterFamilyIdStr != "" { + filterFamilyId, err := strconv.Atoi(filterFamilyIdStr) + if err != nil { + writeInvalidFilterResponse(w, "filter[family.id] is not a number.") + return + } + if p.FamilyId != filterFamilyId { + continue + } + } mount := RenderMountListEntry(p, lang) if expansions.Has("effects") { @@ -226,12 +235,12 @@ func ListMounts(w http.ResponseWriter, r *http.Request) { total := len(mounts) if total == 0 { - w.WriteHeader(http.StatusNotFound) + writeNotFoundResponse(w, "No mounts left after filtering.") return } if pagination.ValidatePagination(total) != 0 { - w.WriteHeader(http.StatusBadRequest) + writeInvalidQueryResponse(w, "Invalid pagination parameters.") return } @@ -247,8 +256,7 @@ func ListMounts(w http.ResponseWriter, r *http.Request) { WriteCacheHeader(&w) err = json.NewEncoder(w).Encode(response) if err != nil { - log.Error("Error while encoding JSON", "error", err) - w.WriteHeader(http.StatusInternalServerError) + writeServerErrorResponse(w, "Could not encode JSON: "+err.Error()) return } } @@ -260,16 +268,17 @@ func ListSets(w http.ResponseWriter, r *http.Request) { expansionsParam := strings.ToLower(r.URL.Query().Get("fields[set]")) expansions := parseFields(expansionsParam) if !validateFields(expansions, setAllowedExpandFields) { - w.WriteHeader(http.StatusBadRequest) + writeInvalidQueryResponse(w, "fields[set] has invalid fields.") return } sortLevel := strings.ToLower(r.URL.Query().Get("sort[level]")) filterMinLevel := strings.ToLower(r.URL.Query().Get("filter[min_highest_equipment_level]")) filterMaxLevel := strings.ToLower(r.URL.Query().Get("filter[max_highest_equipment_level]")) - filterIsCosmeticStr := strings.ToLower(r.URL.Query().Get("filter[is_cosmetic]")) + filterContainsCosmeticsStr := strings.ToLower(r.URL.Query().Get("filter[contains_cosmetics]")) + filterContainsCosmeticsOnlyStr := strings.ToLower(r.URL.Query().Get("filter[contains_cosmetics_only]")) filterMinLevelInt, filterMaxLevelInt, err := MinMaxLevelInt(filterMinLevel, filterMaxLevel, "highest_equipment_level") if err != nil { - w.WriteHeader(http.StatusBadRequest) + writeInvalidFilterResponse(w, "filter[min_level] or filter[max_level] has invalid fields: "+err.Error()) return } @@ -278,7 +287,7 @@ func ListSets(w http.ResponseWriter, r *http.Request) { it, err := txn.Get(fmt.Sprintf("%s-%s", CurrentRedBlueVersionStr(Version.MemDb), "sets"), "id") if err != nil || it == nil { - w.WriteHeader(http.StatusNotFound) + writeNotFoundResponse(w, "No sets found.") return } @@ -287,16 +296,28 @@ func ListSets(w http.ResponseWriter, r *http.Request) { var sets []APIListSet for obj := it.Next(); obj != nil; obj = it.Next() { - p := obj.(*mapping.MappedMultilangSet) + p := obj.(*mapping.MappedMultilangSetUnity) - if filterIsCosmeticStr != "" { - filterIsCosmetic, err := strconv.ParseBool(filterIsCosmeticStr) + if filterContainsCosmeticsOnlyStr != "" { + filterIsCosmetic, err := strconv.ParseBool(filterContainsCosmeticsOnlyStr) if err != nil { - w.WriteHeader(http.StatusBadRequest) + writeInvalidFilterResponse(w, "filter[contains_cosmetics_only] is not a boolean.") return } - if p.IsCosmetic != filterIsCosmetic { + if p.ContainsCosmeticsOnly != filterIsCosmetic { + continue + } + } + + if filterContainsCosmeticsStr != "" { + filterIsCosmetic, err := strconv.ParseBool(filterContainsCosmeticsStr) + if err != nil { + writeInvalidFilterResponse(w, "filter[contains_cosmetics] is not a boolean.") + return + } + + if p.ContainsCosmetics != filterIsCosmetic { continue } } @@ -316,8 +337,9 @@ func ListSets(w http.ResponseWriter, r *http.Request) { set := RenderSetListEntry(p, lang) if expansions.Has("effects") { - for _, effect := range p.Effects { - set.Effects = append(set.Effects, RenderSetEffects(&effect, lang)) + set.Effects = make(map[int][]ApiEffect, 0) + for itemCombination, effect := range p.Effects { + set.Effects[itemCombination] = RenderEffects(&effect, lang) } } @@ -330,7 +352,7 @@ func ListSets(w http.ResponseWriter, r *http.Request) { total := len(sets) if total == 0 { - w.WriteHeader(http.StatusNotFound) + writeNotFoundResponse(w, "No sets left after filtering.") return } @@ -348,7 +370,7 @@ func ListSets(w http.ResponseWriter, r *http.Request) { } if pagination.ValidatePagination(total) != 0 { - w.WriteHeader(http.StatusBadRequest) + writeInvalidQueryResponse(w, "Invalid pagination parameters.") return } @@ -364,8 +386,7 @@ func ListSets(w http.ResponseWriter, r *http.Request) { WriteCacheHeader(&w) err = json.NewEncoder(w).Encode(response) if err != nil { - log.Error("Error while encoding JSON", "error", err) - w.WriteHeader(http.StatusInternalServerError) + writeServerErrorResponse(w, "Could not encode JSON: "+err.Error()) return } } @@ -485,38 +506,37 @@ func ListItems(itemType string, w http.ResponseWriter, r *http.Request) { if itemType == "equipment" || itemType == "cosmetics" { expansions = parseFields(expansionsParam) if !validateFields(expansions, equipmentAllowedExpandFields) { - w.WriteHeader(http.StatusBadRequest) + writeInvalidQueryResponse(w, "fields[item] has invalid fields.") return } } else { expansions = parseFields(expansionsParam) if !validateFields(expansions, itemAllowedExpandFields) { - w.WriteHeader(http.StatusBadRequest) + writeInvalidQueryResponse(w, "fields[item] has invalid fields.") return } } - typeFiltering := strings.ToLower(r.URL.Query().Get("filter[type_enum]")) + typeFiltering := strings.ToLower(r.URL.Query().Get("filter[type.name_id]")) filterset := parseFields(typeFiltering) additiveTypes, err := includeTypes(filterset, nil) if err != nil { - w.WriteHeader(http.StatusBadRequest) + writeInvalidQueryResponse(w, "filter[type.name_id] has invalid fields: "+err.Error()) return } removedTypes, err := excludeTypes(filterset, nil) if err != nil { - w.WriteHeader(http.StatusBadRequest) + writeInvalidQueryResponse(w, "filter[type.name_id] has invalid fields: "+err.Error()) return } sortLevel := strings.ToLower(r.URL.Query().Get("sort[level]")) - filterTypeName := strings.ToLower(r.URL.Query().Get("filter[type_name]")) filterMinLevel := strings.ToLower(r.URL.Query().Get("filter[min_level]")) filterMaxLevel := strings.ToLower(r.URL.Query().Get("filter[max_level]")) filterMinLevelInt, filterMaxLevelInt, err := MinMaxLevelInt(filterMinLevel, filterMaxLevel, "level") if err != nil { - w.WriteHeader(http.StatusBadRequest) + writeInvalidFilterResponse(w, "filter[min_level] or filter[max_level] has invalid fields: "+err.Error()) return } @@ -525,7 +545,7 @@ func ListItems(itemType string, w http.ResponseWriter, r *http.Request) { it, err := txn.Get(fmt.Sprintf("%s-%s", CurrentRedBlueVersionStr(Version.MemDb), itemType), "id") if err != nil || it == nil { - w.WriteHeader(http.StatusNotFound) + writeNotFoundResponse(w, "No items found.") return } @@ -534,7 +554,7 @@ func ListItems(itemType string, w http.ResponseWriter, r *http.Request) { var items []APIListItem for obj := it.Next(); obj != nil; obj = it.Next() { - p := obj.(*mapping.MappedMultilangItem) + p := obj.(*mapping.MappedMultilangItemUnity) enTypeName := strings.ToLower(p.Type.Name["en"]) @@ -546,12 +566,6 @@ func ListItems(itemType string, w http.ResponseWriter, r *http.Request) { continue } - if filterTypeName != "" { - if strings.ToLower(p.Type.Name[lang]) != filterTypeName { - continue - } - } - if filterMinLevel != "" { if p.Level < filterMinLevelInt { continue @@ -582,11 +596,7 @@ func ListItems(itemType string, w http.ResponseWriter, r *http.Request) { if expansions.Has("conditions") { if p.Conditions != nil { - item.Conditions = RenderConditions(&p.Conditions, lang) - } - - if p.ConditionTree != nil { - item.ConditionTree = RenderConditionTree(p.ConditionTree, lang) + item.Conditions = RenderConditionTree(p.Conditions, lang) } } @@ -628,10 +638,6 @@ func ListItems(itemType string, w http.ResponseWriter, r *http.Request) { item.CriticalHitBonus = &p.CriticalHitBonus } - if expansions.Has("is_two_handed") { - item.TwoHanded = &p.TwoHanded - } - if expansions.Has("max_cast_per_turn") { item.MaxCastPerTurn = &p.MaxCastPerTurn } @@ -652,7 +658,7 @@ func ListItems(itemType string, w http.ResponseWriter, r *http.Request) { } if len(items) == 0 { - w.WriteHeader(http.StatusNotFound) + writeNotFoundResponse(w, "No items left after filtering.") return } @@ -672,7 +678,7 @@ func ListItems(itemType string, w http.ResponseWriter, r *http.Request) { total := len(items) if pagination.ValidatePagination(total) != 0 { - w.WriteHeader(http.StatusBadRequest) + writeInvalidQueryResponse(w, "Invalid pagination parameters.") return } @@ -688,8 +694,7 @@ func ListItems(itemType string, w http.ResponseWriter, r *http.Request) { WriteCacheHeader(&w) err = json.NewEncoder(w).Encode(response) if err != nil { - log.Error("Error while encoding JSON", "error", err) - w.WriteHeader(http.StatusInternalServerError) + writeServerErrorResponse(w, "Could not encode JSON: "+err.Error()) return } } @@ -738,22 +743,21 @@ func SearchAlmanaxBonuses(w http.ResponseWriter, r *http.Request) { query := r.URL.Query().Get("query") if query == "" { - w.WriteHeader(http.StatusBadRequest) + writeInvalidQueryResponse(w, "Query parameter is required.") return } lang := r.Context().Value("lang").(string) if lang == "pt" { - log.Info("SearchAlmanaxBonuses: pt is not supported") - w.WriteHeader(http.StatusBadRequest) + writeInvalidQueryResponse(w, "Portuguese language is not translated for Almanax Bonuses.") return } var searchLimit int64 var err error if searchLimit, err = getLimitInBoundary(r.URL.Query().Get("limit")); err != nil { - w.WriteHeader(http.StatusBadRequest) + writeInvalidQueryResponse(w, "Invalid limit value: "+err.Error()) return } @@ -765,8 +769,7 @@ func SearchAlmanaxBonuses(w http.ResponseWriter, r *http.Request) { var searchResp *meilisearch.SearchResponse if searchResp, err = index.Search(query, request); err != nil { - log.Error("SearchAlmanaxBonuses: index not found", "err", err) - w.WriteHeader(http.StatusInternalServerError) + writeServerErrorResponse(w, "Could not search: "+err.Error()) return } @@ -774,7 +777,7 @@ func SearchAlmanaxBonuses(w http.ResponseWriter, r *http.Request) { requestsSearchTotal.Inc() if searchResp.EstimatedTotalHits == 0 { - w.WriteHeader(http.StatusNotFound) + writeNotFoundResponse(w, "No results found.") return } @@ -791,8 +794,7 @@ func SearchAlmanaxBonuses(w http.ResponseWriter, r *http.Request) { WriteCacheHeader(&w) err = json.NewEncoder(w).Encode(results) if err != nil { - log.Error("Error while encoding JSON", "error", err) - w.WriteHeader(http.StatusInternalServerError) + writeServerErrorResponse(w, "Could not encode JSON: "+err.Error()) return } } @@ -804,25 +806,40 @@ func SearchMounts(w http.ResponseWriter, r *http.Request) { var err error query := r.URL.Query().Get("query") if query == "" { - w.WriteHeader(http.StatusBadRequest) + writeInvalidQueryResponse(w, "Query parameter is required.") return } var searchLimit int64 if searchLimit, err = getLimitInBoundary(r.URL.Query().Get("limit")); err != nil { - w.WriteHeader(http.StatusBadRequest) + writeInvalidQueryResponse(w, "Invalid limit value: "+err.Error()) return } - familyName := strings.ToLower(r.URL.Query().Get("filter[family_name]")) + filterFamilyName := r.URL.Query().Get("filter[family.name]") + filterFamilyIdStr := r.URL.Query().Get("filter[family.id]") lang := r.Context().Value("lang").(string) index := client.Index(fmt.Sprintf("%s-mounts-%s", CurrentRedBlueVersionStr(Version.Search), lang)) var request *meilisearch.SearchRequest filterString := "" - if familyName != "" { - filterString = fmt.Sprintf("family_name=%s", familyName) + if filterFamilyName != "" { + filterString = fmt.Sprintf("family.name=%s", filterFamilyName) + } + + if filterFamilyIdStr != "" { + filterFamilyId, err := strconv.Atoi(filterFamilyIdStr) + if err != nil { + writeInvalidQueryResponse(w, "Family ID must be an integer.") + return + } + + if filterString != "" { + filterString = fmt.Sprintf("%s AND family.id=%d", filterString, filterFamilyId) + } else { + filterString = fmt.Sprintf("family.id=%d", filterFamilyId) + } } if filterString == "" { @@ -838,7 +855,7 @@ func SearchMounts(w http.ResponseWriter, r *http.Request) { searchResp, err := index.Search(query, request) if err != nil { - w.WriteHeader(http.StatusNotFound) + writeServerErrorResponse(w, "Could not search: "+err.Error()) return } @@ -846,7 +863,7 @@ func SearchMounts(w http.ResponseWriter, r *http.Request) { requestsMountsSearch.Inc() if searchResp.EstimatedTotalHits == 0 { - w.WriteHeader(http.StatusNotFound) + writeNotFoundResponse(w, "No results found.") return } @@ -860,7 +877,7 @@ func SearchMounts(w http.ResponseWriter, r *http.Request) { raw, err := txn.First(fmt.Sprintf("%s-%s", CurrentRedBlueVersionStr(Version.MemDb), "mounts"), "id", itemId) if err != nil || raw == nil { - w.WriteHeader(http.StatusNotFound) + writeServerErrorResponse(w, "Could not find mount in database: "+err.Error()) return } @@ -871,8 +888,7 @@ func SearchMounts(w http.ResponseWriter, r *http.Request) { WriteCacheHeader(&w) err = json.NewEncoder(w).Encode(mounts) if err != nil { - log.Error("Error while encoding JSON", "error", err) - w.WriteHeader(http.StatusInternalServerError) + writeServerErrorResponse(w, "Could not encode JSON: "+err.Error()) return } } @@ -883,36 +899,50 @@ func SearchSets(w http.ResponseWriter, r *http.Request) { query := r.URL.Query().Get("query") if query == "" { - w.WriteHeader(http.StatusBadRequest) + writeInvalidQueryResponse(w, "Query parameter is required.") return } lang := r.Context().Value("lang").(string) filterMinLevel := strings.ToLower(r.URL.Query().Get("filter[min_highest_equipment_level]")) filterMaxLevel := strings.ToLower(r.URL.Query().Get("filter[max_highest_equipment_level]")) - filterIsCosmeticStr := strings.ToLower(r.URL.Query().Get("filter[is_cosmetic]")) + filterContainsCosmeticsStr := strings.ToLower(r.URL.Query().Get("filter[contains_cosmetics]")) + filterContainsCosmeticsOnlyStr := strings.ToLower(r.URL.Query().Get("filter[contains_cosmetics_only]")) filterString, err := MinMaxLevelMeiliFilterFromParams(filterMinLevel, filterMaxLevel, "highest_equipment_level") if err != nil { - w.WriteHeader(http.StatusBadRequest) + writeInvalidFilterResponse(w, "Min/Max level filter is invalid: "+err.Error()) return } - if filterIsCosmeticStr != "" { - filterIsCosmetic, err := strconv.ParseBool(filterIsCosmeticStr) + if filterContainsCosmeticsOnlyStr != "" { + filterIsCosmetic, err := strconv.ParseBool(filterContainsCosmeticsOnlyStr) if err != nil { - w.WriteHeader(http.StatusBadRequest) + writeInvalidFilterResponse(w, "filter[contains_cosmetics_only] must be a boolean.") return } isCosmeticMeiliFilterBoolString := strconv.FormatBool(filterIsCosmetic) if filterString != "" { filterString += " AND " } - filterString += fmt.Sprintf("is_cosmetic=%s", isCosmeticMeiliFilterBoolString) + filterString += fmt.Sprintf("contains_cosmetics_only=%s", isCosmeticMeiliFilterBoolString) + } + + if filterContainsCosmeticsStr != "" { + filterIsCosmetic, err := strconv.ParseBool(filterContainsCosmeticsStr) + if err != nil { + writeInvalidFilterResponse(w, "filter[contains_cosmetics] must be a boolean.") + return + } + isCosmeticMeiliFilterBoolString := strconv.FormatBool(filterIsCosmetic) + if filterString != "" { + filterString += " AND " + } + filterString += fmt.Sprintf("contains_cosmetics=%s", isCosmeticMeiliFilterBoolString) } var searchLimit int64 if searchLimit, err = getLimitInBoundary(r.URL.Query().Get("limit")); err != nil { - w.WriteHeader(http.StatusBadRequest) + writeInvalidQueryResponse(w, "Limit parameter is invalid: "+err.Error()) return } @@ -932,7 +962,7 @@ func SearchSets(w http.ResponseWriter, r *http.Request) { searchResp, err := index.Search(query, request) if err != nil { - w.WriteHeader(http.StatusNotFound) + writeServerErrorResponse(w, "Could not search: "+err.Error()) return } @@ -940,7 +970,7 @@ func SearchSets(w http.ResponseWriter, r *http.Request) { requestsSetsSearch.Inc() if searchResp.EstimatedTotalHits == 0 { - w.WriteHeader(http.StatusNotFound) + writeNotFoundResponse(w, "No results found.") return } @@ -954,19 +984,18 @@ func SearchSets(w http.ResponseWriter, r *http.Request) { raw, err := txn.First(fmt.Sprintf("%s-%s", CurrentRedBlueVersionStr(Version.MemDb), "sets"), "id", itemId) if err != nil || raw == nil { - w.WriteHeader(http.StatusNotFound) + writeServerErrorResponse(w, "Could not find set in database: "+err.Error()) return } - item := raw.(*mapping.MappedMultilangSet) + item := raw.(*mapping.MappedMultilangSetUnity) sets = append(sets, RenderSetListEntry(item, lang)) } WriteCacheHeader(&w) err = json.NewEncoder(w).Encode(sets) if err != nil { - log.Error("Error while encoding JSON", "error", err) - w.WriteHeader(http.StatusInternalServerError) + writeServerErrorResponse(w, "Could not encode JSON: "+err.Error()) return } } @@ -977,7 +1006,7 @@ func SearchAllIndices(w http.ResponseWriter, r *http.Request) { query := r.URL.Query().Get("query") if query == "" { - w.WriteHeader(http.StatusBadRequest) + writeInvalidQueryResponse(w, "Query parameter is required.") return } @@ -988,14 +1017,14 @@ func SearchAllIndices(w http.ResponseWriter, r *http.Request) { parsedIndices := parseFields(filterSearchIndex) if !validateFields(parsedIndices, searchAllowedIndices) { - w.WriteHeader(http.StatusBadRequest) + writeInvalidFilterResponse(w, "filter[type] has invalid fields.") return } itemExpansionsParam := strings.ToLower(r.URL.Query().Get("fields[item]")) itemExpansions := parseFields(itemExpansionsParam) if !validateFields(itemExpansions, searchAllItemAllowedExpandFields) { - w.WriteHeader(http.StatusBadRequest) + writeInvalidQueryResponse(w, "fields[item] has invalid fields.") return } @@ -1004,22 +1033,22 @@ func SearchAllIndices(w http.ResponseWriter, r *http.Request) { var searchLimit int64 var err error if searchLimit, err = getLimitInBoundary(r.URL.Query().Get("limit")); err != nil { - w.WriteHeader(http.StatusBadRequest) + writeInvalidQueryResponse(w, "Limit parameter is invalid: "+err.Error()) return } - typeFiltering := strings.ToLower(r.URL.Query().Get("filter[type_enum]")) + typeFiltering := strings.ToLower(r.URL.Query().Get("filter[type.name_id]")) exceptions := []string{"mount", "set"} filterset := parseFields(typeFiltering) additiveTypes, err := includeTypes(filterset, &exceptions) if err != nil { - w.WriteHeader(http.StatusBadRequest) + writeInvalidFilterResponse(w, "filter[type.name_id] is invalid: "+err.Error()) return } removedTypes, err := excludeTypes(filterset, &exceptions) if err != nil { - w.WriteHeader(http.StatusBadRequest) + writeInvalidFilterResponse(w, "filter[type.name_id] is invalid: "+err.Error()) return } @@ -1052,7 +1081,7 @@ func SearchAllIndices(w http.ResponseWriter, r *http.Request) { filterString += "(" if len(addTypesArr) > 0 { - filterString += "type_id=" + strings.Join(addTypesArr, " OR type_id=") + filterString += "type.name_id=" + strings.Join(addTypesArr, " OR type.name_id=") } if len(plural) > 0 && len(addTypesArr) > 0 { @@ -1060,7 +1089,7 @@ func SearchAllIndices(w http.ResponseWriter, r *http.Request) { } if len(plural) > 0 { - filterString += "stuff_type=" + strings.Join(plural, " OR stuff_type=") + filterString += "stuff_type.name_id=" + strings.Join(plural, " OR stuff_type.name_id=") } filterString += ")" @@ -1079,7 +1108,7 @@ func SearchAllIndices(w http.ResponseWriter, r *http.Request) { filterString += "(" if len(remTypesArr) > 0 { - filterString += "NOT type_id=" + strings.Join(remTypesArr, " AND NOT type_id=") + filterString += "NOT type.name_id=" + strings.Join(remTypesArr, " AND NOT type.name_id=") } if len(plural) > 0 && len(remTypesArr) > 0 { @@ -1087,7 +1116,7 @@ func SearchAllIndices(w http.ResponseWriter, r *http.Request) { } if len(plural) > 0 { - filterString += "NOT stuff_type=" + strings.Join(plural, " AND NOT stuff_type=") + filterString += "NOT stuff_type.name_id=" + strings.Join(plural, " AND NOT stuff_type.name_id=") } filterString += ")" @@ -1099,7 +1128,7 @@ func SearchAllIndices(w http.ResponseWriter, r *http.Request) { filterString += " AND " } - filterString += "(stuff_type=" + strings.Join(parsedIndices.Keys(), " OR stuff_type=") + ")" + filterString += "(stuff_type.name_id=" + strings.Join(parsedIndices.Keys(), " OR stuff_type.name_id=") + ")" } } @@ -1110,8 +1139,7 @@ func SearchAllIndices(w http.ResponseWriter, r *http.Request) { var searchResp *meilisearch.SearchResponse if searchResp, err = index.Search(query, request); err != nil { - log.Warn("SearchAllIndices: index not found: ", "err", err) - w.WriteHeader(http.StatusInternalServerError) + writeServerErrorResponse(w, "Failed to search for query: "+err.Error()) return } @@ -1119,7 +1147,7 @@ func SearchAllIndices(w http.ResponseWriter, r *http.Request) { requestsSearchTotal.Inc() if searchResp.EstimatedTotalHits == 0 { - w.WriteHeader(http.StatusNotFound) + writeNotFoundResponse(w, "No results found.") return } @@ -1130,9 +1158,12 @@ func SearchAllIndices(w http.ResponseWriter, r *http.Request) { for _, hit := range searchResp.Hits { indexed := hit.(map[string]interface{}) - isItem := strings.HasPrefix(indexed["stuff_type"].(string), "items-") + stuffType := indexed["stuff_type"].(struct { + NameId string `json:"name_id"` + }).NameId + + isItem := strings.HasPrefix(stuffType, "items-") ankamaId := int(indexed["id"].(float64)) - stuffType := indexed["stuff_type"].(string) var itemInclude *ApiAllSearchItem if isItem { @@ -1149,8 +1180,7 @@ func SearchAllIndices(w http.ResponseWriter, r *http.Request) { case "items-quest_items": itemType = "quest_items" default: - log.Error("Unknown stuff type", "stuff_type", stuffType) - w.WriteHeader(http.StatusInternalServerError) + writeServerErrorResponse(w, "Unknown stuff type: "+stuffType) return } @@ -1159,17 +1189,16 @@ func SearchAllIndices(w http.ResponseWriter, r *http.Request) { raw, err := txn.First(fmt.Sprintf("%s-%s", CurrentRedBlueVersionStr(Version.MemDb), itemType), "id", ankamaId) if err != nil { - log.Error("Error while getting memdb detailed type", "err", err) - w.WriteHeader(http.StatusInternalServerError) + writeServerErrorResponse(w, "Could not find entity in database: "+err.Error()) return } if raw == nil { - log.Warn("Item not found in memdb.", "id", ankamaId) + log.Warn("Could not find item in memdb", "id", ankamaId) continue } - item := raw.(*mapping.MappedMultilangItem) + item := raw.(*mapping.MappedMultilangItemUnity) itemFields := RenderItemListEntry(item, lang) itemInclude = &ApiAllSearchItem{} @@ -1192,9 +1221,11 @@ func SearchAllIndices(w http.ResponseWriter, r *http.Request) { } result := ApiAllSearchResult{ - Id: ankamaId, - Name: indexed["name"].(string), - Type: stuffType, + Id: ankamaId, + Name: indexed["name"].(string), + Type: ApiAllSearchResultType{ + NameId: stuffType, + }, ItemFields: itemInclude, } @@ -1204,8 +1235,7 @@ func SearchAllIndices(w http.ResponseWriter, r *http.Request) { WriteCacheHeader(&w) err = json.NewEncoder(w).Encode(stuffs) if err != nil { - log.Error("Error while encoding JSON", "error", err) - w.WriteHeader(http.StatusInternalServerError) + writeServerErrorResponse(w, "Could not encode JSON: "+err.Error()) return } } @@ -1216,16 +1246,15 @@ func SearchItems(itemType string, all bool, w http.ResponseWriter, r *http.Reque query := r.URL.Query().Get("query") if query == "" { - w.WriteHeader(http.StatusBadRequest) + writeInvalidQueryResponse(w, "Query parameter is required.") return } - filterTypeName := strings.ToLower(r.URL.Query().Get("filter[type_name]")) filterMinLevel := strings.ToLower(r.URL.Query().Get("filter[min_level]")) filterMaxLevel := strings.ToLower(r.URL.Query().Get("filter[max_level]")) filterString, err := MinMaxLevelMeiliFilterFromParams(filterMinLevel, filterMaxLevel, "level") if err != nil { - w.WriteHeader(http.StatusBadRequest) + writeInvalidFilterResponse(w, "Min/Max level filter is invalid: "+err.Error()) return } @@ -1233,21 +1262,21 @@ func SearchItems(itemType string, all bool, w http.ResponseWriter, r *http.Reque var searchLimit int64 if searchLimit, err = getLimitInBoundary(r.URL.Query().Get("limit")); err != nil { - w.WriteHeader(http.StatusBadRequest) + writeInvalidQueryResponse(w, "Limit parameter is invalid: "+err.Error()) return } - typeFiltering := strings.ToLower(r.URL.Query().Get("filter[type_enum]")) + typeFiltering := strings.ToLower(r.URL.Query().Get("filter[type.name_id]")) filterset := parseFields(typeFiltering) additiveTypes, err := includeTypes(filterset, nil) if err != nil { - w.WriteHeader(http.StatusBadRequest) + writeInvalidFilterResponse(w, "filter[type.name_id] is invalid: "+err.Error()) return } removedTypes, err := excludeTypes(filterset, nil) if err != nil { - w.WriteHeader(http.StatusBadRequest) + writeInvalidFilterResponse(w, "filter[type.name_id] is invalid: "+err.Error()) return } @@ -1255,7 +1284,7 @@ func SearchItems(itemType string, all bool, w http.ResponseWriter, r *http.Reque if filterString != "" { filterString += " AND " } - filterString += "(type_id=" + strings.Join(additiveTypes.Keys(), " OR type_id=") + ")" + filterString += "(type.name_id=" + strings.Join(additiveTypes.Keys(), " OR type.name_id=") + ")" } if removedTypes.Size() > 0 { @@ -1263,32 +1292,16 @@ func SearchItems(itemType string, all bool, w http.ResponseWriter, r *http.Reque filterString += " AND " } - filterString += "(NOT type_id=" + strings.Join(removedTypes.Keys(), " AND NOT type_id=") + ")" + filterString += "(NOT type.name_id=" + strings.Join(removedTypes.Keys(), " AND NOT type.name_id=") + ")" } index := client.Index(fmt.Sprintf("%s-all_items-%s", CurrentRedBlueVersionStr(Version.Search), lang)) var request *meilisearch.SearchRequest - if all { - if filterTypeName != "" { - if filterString == "" { - filterString += fmt.Sprintf("type_name=%s", filterTypeName) - } else { - filterString += fmt.Sprintf(" AND type_name=%s", filterTypeName) - } - } - } else { - if filterTypeName != "" { - if filterString == "" { // not already set with MinMaxLevels - filterString += fmt.Sprintf("type_name=%s", filterTypeName) - } else { - filterString += fmt.Sprintf(" AND type_name=%s", filterTypeName) - } - } - + if !all { if filterString == "" { - filterString += fmt.Sprintf("super_type=%s", itemType) + filterString += fmt.Sprintf("super_type.name_id=%s", itemType) } else { - filterString += fmt.Sprintf(" AND super_type=%s", itemType) + filterString += fmt.Sprintf(" AND super_type.name_id=%s", itemType) } } @@ -1305,7 +1318,7 @@ func SearchItems(itemType string, all bool, w http.ResponseWriter, r *http.Reque searchResp, err := index.Search(query, request) if err != nil { - w.WriteHeader(http.StatusNotFound) + writeServerErrorResponse(w, "Could not search: "+err.Error()) return } @@ -1313,7 +1326,7 @@ func SearchItems(itemType string, all bool, w http.ResponseWriter, r *http.Reque requestsItemsSearch.Inc() if searchResp.EstimatedTotalHits == 0 { - w.WriteHeader(http.StatusNotFound) + writeNotFoundResponse(w, "No results found.") return } @@ -1334,8 +1347,7 @@ func SearchItems(itemType string, all bool, w http.ResponseWriter, r *http.Reque } if err != nil { - log.Error("Error while getting memdb detailed type", "err", err) - w.WriteHeader(http.StatusInternalServerError) + writeServerErrorResponse(w, "Could not find item in database: "+err.Error()) return } @@ -1344,7 +1356,7 @@ func SearchItems(itemType string, all bool, w http.ResponseWriter, r *http.Reque continue } - item := raw.(*mapping.MappedMultilangItem) + item := raw.(*mapping.MappedMultilangItemUnity) if all { typedItems = append(typedItems, RenderTypedItemListEntry(item, lang)) } else { @@ -1360,8 +1372,7 @@ func SearchItems(itemType string, all bool, w http.ResponseWriter, r *http.Reque encodeErr = json.NewEncoder(w).Encode(items) } if encodeErr != nil { - log.Error("Error while encoding JSON", "error", encodeErr) - w.WriteHeader(http.StatusInternalServerError) + writeServerErrorResponse(w, "Could not encode JSON: "+err.Error()) return } } @@ -1401,19 +1412,18 @@ func GetSingleSetHandler(w http.ResponseWriter, r *http.Request) { raw, err := txn.First(fmt.Sprintf("%s-%s", CurrentRedBlueVersionStr(Version.MemDb), "sets"), "id", ankamaId) if err != nil || raw == nil { - w.WriteHeader(http.StatusNotFound) + writeServerErrorResponse(w, "Could not find set in database: "+err.Error()) return } requestsTotal.Inc() requestsSetsSingle.Inc() - set := RenderSet(raw.(*mapping.MappedMultilangSet), lang) + set := RenderSet(raw.(*mapping.MappedMultilangSetUnity), lang) WriteCacheHeader(&w) err = json.NewEncoder(w).Encode(set) if err != nil { - log.Error("Error while encoding JSON", "error", err) - w.WriteHeader(http.StatusInternalServerError) + writeServerErrorResponse(w, "Could not encode JSON: "+err.Error()) return } } @@ -1427,7 +1437,7 @@ func GetSingleMountHandler(w http.ResponseWriter, r *http.Request) { raw, err := txn.First(fmt.Sprintf("%s-%s", CurrentRedBlueVersionStr(Version.MemDb), "mounts"), "id", ankamaId) if err != nil || raw == nil { - w.WriteHeader(http.StatusNotFound) + writeServerErrorResponse(w, "Could not find mount in database: "+err.Error()) return } @@ -1438,8 +1448,7 @@ func GetSingleMountHandler(w http.ResponseWriter, r *http.Request) { WriteCacheHeader(&w) err = json.NewEncoder(w).Encode(mount) if err != nil { - log.Error("Error while encoding JSON", "error", err) - w.WriteHeader(http.StatusInternalServerError) + writeServerErrorResponse(w, "Could not encode JSON: "+err.Error()) return } } @@ -1453,14 +1462,14 @@ func GetSingleItemWithOptionalRecipeHandler(itemType string, w http.ResponseWrit raw, err := txn.First(fmt.Sprintf("%s-%s", CurrentRedBlueVersionStr(Version.MemDb), itemType), "id", ankamaId) if err != nil || raw == nil { - w.WriteHeader(http.StatusNotFound) + writeServerErrorResponse(w, "Could not find item in database: "+err.Error()) return } requestsTotal.Inc() requestsItemsSingle.Inc() - resource := RenderResource(raw.(*mapping.MappedMultilangItem), lang) + resource := RenderResource(raw.(*mapping.MappedMultilangItemUnity), lang) recipe, exists := GetRecipeIfExists(ankamaId, txn) if exists { resource.Recipe = RenderRecipe(recipe, Db) @@ -1468,8 +1477,7 @@ func GetSingleItemWithOptionalRecipeHandler(itemType string, w http.ResponseWrit WriteCacheHeader(&w) err = json.NewEncoder(w).Encode(resource) if err != nil { - log.Error("Error while encoding JSON", "error", err) - w.WriteHeader(http.StatusInternalServerError) + writeServerErrorResponse(w, "Could not encode JSON: "+err.Error()) return } } @@ -1510,14 +1518,14 @@ func GetSingleEquipmentLikeHandler(cosmetic bool, w http.ResponseWriter, r *http raw, err := txn.First(fmt.Sprintf("%s-%s", CurrentRedBlueVersionStr(Version.MemDb), dbType), "id", ankamaId) if err != nil || raw == nil { - w.WriteHeader(http.StatusNotFound) + writeNotFoundResponse(w, "Item not found: "+strconv.Itoa(ankamaId)) return } requestsTotal.Inc() requestsItemsSingle.Inc() - item := raw.(*mapping.MappedMultilangItem) + item := raw.(*mapping.MappedMultilangItemUnity) if item.Type.SuperTypeId == 2 { // is weapon weapon := RenderWeapon(item, lang) recipe, exists := GetRecipeIfExists(ankamaId, txn) @@ -1527,8 +1535,7 @@ func GetSingleEquipmentLikeHandler(cosmetic bool, w http.ResponseWriter, r *http WriteCacheHeader(&w) err = json.NewEncoder(w).Encode(weapon) if err != nil { - log.Error("Error while encoding JSON", "error", err) - w.WriteHeader(http.StatusInternalServerError) + writeServerErrorResponse(w, "Could not encode JSON: "+err.Error()) return } } else { @@ -1540,8 +1547,7 @@ func GetSingleEquipmentLikeHandler(cosmetic bool, w http.ResponseWriter, r *http WriteCacheHeader(&w) err = json.NewEncoder(w).Encode(equipment) if err != nil { - log.Error("Error while encoding JSON", "error", err) - w.WriteHeader(http.StatusInternalServerError) + writeServerErrorResponse(w, "Could not encode JSON: "+err.Error()) return } diff --git a/indexing.go b/indexing.go index ab89cca..fcf6097 100644 --- a/indexing.go +++ b/indexing.go @@ -18,30 +18,39 @@ import ( mapping "github.com/dofusdude/dodumap" ) +type SearchStuffType struct { + NameId string `json:"name_id"` +} + +type SearchType struct { + Name string `json:"name"` // old "type_name" + NameId string `json:"name_id"` // old "type_id" +} + type SearchIndexedItem struct { - Id int `json:"id"` - Name string `json:"name"` - Description string `json:"description"` - SuperType string `json:"super_type"` - TypeName string `json:"type_name"` - TypeId string `json:"type_id"` - Level int `json:"level"` - StuffType string `json:"stuff_type"` + Id int `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + SuperType SearchStuffType `json:"super_type"` + Type SearchType `json:"type"` + Level int `json:"level"` + StuffType SearchStuffType `json:"stuff_type"` } type SearchIndexedMount struct { - Id int `json:"id"` - Name string `json:"name"` - FamilyName string `json:"family_name"` - StuffType string `json:"stuff_type"` + Id int `json:"id"` + Name string `json:"name"` + Family ApiType `json:"family"` // family_name before, now with id and translated name + StuffType SearchStuffType `json:"stuff_type"` } type SearchIndexedSet struct { - Id int `json:"id"` - Name string `json:"name"` - Level int `json:"highest_equipment_level"` - IsCosmetic bool `json:"is_cosmetic"` - StuffType string `json:"stuff_type"` + Id int `json:"id"` + Name string `json:"name"` + Level int `json:"highest_equipment_level"` + ContainsCosmetics bool `json:"contains_cosmetics"` + ContainsCosmeticsOnly bool `json:"contains_cosmetics_only"` + StuffType SearchStuffType `json:"stuff_type"` } type EffectConditionDbEntry struct { @@ -55,8 +64,8 @@ type ItemTypeId struct { } func IndexApiData(version *VersionT) (*memdb.MemDB, map[string]SearchIndexes) { - var items []mapping.MappedMultilangItem - var sets []mapping.MappedMultilangSet + var items []mapping.MappedMultilangItemUnity + var sets []mapping.MappedMultilangSetUnity var recipes []mapping.MappedMultilangRecipe var mounts []mapping.MappedMultilangMount @@ -522,7 +531,7 @@ func UpdateAlmanaxBonusIndex(init bool) int { return added } -func GenerateDatabase(items *[]mapping.MappedMultilangItem, sets *[]mapping.MappedMultilangSet, recipes *[]mapping.MappedMultilangRecipe, mounts *[]mapping.MappedMultilangMount, version *VersionT) (*memdb.MemDB, map[string]SearchIndexes) { +func GenerateDatabase(items *[]mapping.MappedMultilangItemUnity, sets *[]mapping.MappedMultilangSetUnity, recipes *[]mapping.MappedMultilangRecipe, mounts *[]mapping.MappedMultilangMount, version *VersionT) (*memdb.MemDB, map[string]SearchIndexes) { /* item_category_mapping := hashbidimap.New() item_category_Put(0, 862817) // Ausrüstung @@ -622,17 +631,16 @@ func GenerateDatabase(items *[]mapping.MappedMultilangItem, sets *[]mapping.Mapp // add filters allStuffIdx := client.Index(allIndexUid) if _, err = allStuffIdx.UpdateFilterableAttributes(&[]string{ - "stuff_type", - "type_id", + "stuff_type.name_id", + "type.name_id", }); err != nil { log.Fatal(err) } allItemsIdx := client.Index(itemIndexUid) if _, err = allItemsIdx.UpdateFilterableAttributes(&[]string{ - "super_type", - "type_name", - "type_id", + "super_type.name_id", + "type.name_id", "level", }); err != nil { log.Fatal(err) @@ -640,7 +648,8 @@ func GenerateDatabase(items *[]mapping.MappedMultilangItem, sets *[]mapping.Mapp mountsIdx := client.Index(mountIndexUid) if _, err = mountsIdx.UpdateFilterableAttributes(&[]string{ - "family_name", + "family.name", + "family.id", }); err != nil { log.Fatal(err) } @@ -648,7 +657,8 @@ func GenerateDatabase(items *[]mapping.MappedMultilangItem, sets *[]mapping.Mapp setsIdx := client.Index(setIndexUid) if _, err = setsIdx.UpdateFilterableAttributes(&[]string{ "highest_equipment_level", - "is_cosmetic", + "constains_cosmetics", + "constains_cosmetics_only", }); err != nil { log.Fatal(err) } @@ -725,11 +735,17 @@ func GenerateDatabase(items *[]mapping.MappedMultilangItem, sets *[]mapping.Mapp Name: itemCp.Name[lang], Id: itemCp.AnkamaId, Description: itemCp.Description[lang], - SuperType: insertCategoryTable, - TypeName: strings.ToLower(itemCp.Type.Name[lang]), - TypeId: enTypeId, - Level: itemCp.Level, - StuffType: fmt.Sprintf("items-%s", insertCategoryTable), + SuperType: SearchStuffType{ + NameId: insertCategoryTable, + }, + Type: SearchType{ + Name: strings.ToLower(itemCp.Type.Name[lang]), + NameId: enTypeId, + }, + Level: itemCp.Level, + StuffType: SearchStuffType{ + NameId: fmt.Sprintf("items-%s", insertCategoryTable), + }, } itemTypeIds.Put(enTypeId) @@ -768,11 +784,14 @@ func GenerateDatabase(items *[]mapping.MappedMultilangItem, sets *[]mapping.Mapp for _, lang := range Languages { object := SearchIndexedSet{ - Name: setCp.Name[lang], - Id: setCp.AnkamaId, - Level: setCp.Level, - IsCosmetic: setCp.IsCosmetic, - StuffType: "sets", + Name: setCp.Name[lang], + Id: setCp.AnkamaId, + Level: setCp.Level, + ContainsCosmetics: setCp.ContainsCosmetics, + ContainsCosmeticsOnly: setCp.ContainsCosmeticsOnly, + StuffType: SearchStuffType{ + NameId: "sets", + }, } setIndexBatch[lang] = append(setIndexBatch[lang], object) @@ -801,10 +820,15 @@ func GenerateDatabase(items *[]mapping.MappedMultilangItem, sets *[]mapping.Mapp for _, lang := range Languages { object := SearchIndexedMount{ - Name: mountCp.Name[lang], - Id: mountCp.AnkamaId, - FamilyName: strings.ToLower(mountCp.FamilyName[lang]), - StuffType: "mounts", + Name: mountCp.Name[lang], + Id: mountCp.AnkamaId, + Family: ApiType{ + Name: strings.ToLower(mountCp.FamilyName[lang]), + Id: mountCp.FamilyId, + }, + StuffType: SearchStuffType{ + NameId: "mounts", + }, } mountIndexBatch[lang] = append(mountIndexBatch[lang], object) diff --git a/main.go b/main.go index 6083695..218939c 100644 --- a/main.go +++ b/main.go @@ -19,6 +19,17 @@ import ( "github.com/spf13/cobra" ) +var ( + DoduapiMajor = 1 // Major version also used for prefixing API routes. + DoduapiVersion = fmt.Sprintf("v%d.0.0-rc.1", DoduapiMajor) // change with every release + DoduapiShort = "doduapi - Open Dofus Encyclopedia API" + DoduapiLong = "" + DoduapiVersionHelp = DoduapiShort + "\n" + DoduapiVersion + "\nhttps://github.com/dofusdude/doduapi" + httpDataServer *http.Server + httpMetricsServer *http.Server + UpdateChan chan GameVersion +) + func AutoUpdate(version *VersionT, updateHook chan GameVersion, updateDb chan *memdb.MemDB, updateSearchIndex chan map[string]SearchIndexes, almanaxBonusTicker *time.Ticker) { for { select { @@ -153,24 +164,18 @@ func isChannelClosed[T any](ch chan T) bool { return false } -var httpDataServer *http.Server -var httpMetricsServer *http.Server -var UpdateChan chan GameVersion - var ( rootCmd = &cobra.Command{ Use: "doduapi", - Short: "doduapi – The Dofus encyclopedia API.", - Long: ``, + Short: DoduapiShort, + Long: DoduapiLong, Run: rootCommand, } ) func main() { - ReadEnvs() - rootCmd.PersistentFlags().Bool("headless", false, "Run without a TUI.") - rootCmd.PersistentFlags().Bool("full-img", false, "Load images in prerendered resolutions (~2.5 GB).") + rootCmd.PersistentFlags().Bool("version", false, "Print API version.") rootCmd.PersistentFlags().Int32("alm-bonus-interval", 12, "Almanax bonuses search index interval in hours.") err := rootCmd.Execute() @@ -184,12 +189,18 @@ func rootCommand(ccmd *cobra.Command, args []string) { signal.Notify(sigint, os.Interrupt, syscall.SIGTERM) var err error - headless, err := ccmd.Flags().GetBool("headless") + + printVersion, err := ccmd.Flags().GetBool("version") if err != nil { log.Fatal(err) } - FullImg, err = ccmd.Flags().GetBool("full-img") + if printVersion { + fmt.Println(DoduapiVersionHelp) + return + } + + headless, err := ccmd.Flags().GetBool("headless") if err != nil { log.Fatal(err) } @@ -199,6 +210,8 @@ func rootCommand(ccmd *cobra.Command, args []string) { log.Fatal(err) } + ReadEnvs() + feedbackChan := make(chan string, 5) var wg sync.WaitGroup wg.Add(1) diff --git a/middleware.go b/middleware.go index e3d3c44..06d2b27 100644 --- a/middleware.go +++ b/middleware.go @@ -64,7 +64,7 @@ func languageChecker(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { lang := strings.ToLower(chi.URLParam(r, "lang")) switch lang { - case "en", "fr", "de", "es", "it", "pt": + case "en", "fr", "de", "es", "pt": ctx := context.WithValue(r.Context(), "lang", lang) next.ServeHTTP(w, r.WithContext(ctx)) default: diff --git a/routes.go b/routes.go index bd4fd1d..2c7a64a 100644 --- a/routes.go +++ b/routes.go @@ -43,18 +43,18 @@ func Router() chi.Router { r.Use(middleware.Recoverer) r.Use(middleware.Timeout(10 * time.Second)) - var routePrefix string + var gameRelease string if IsBeta { - routePrefix = "/dofus2beta" + gameRelease = "dofus3beta" } else { - routePrefix = "/dofus2" + gameRelease = "dofus3" } - r.With(useCors).With(languageChecker).Route("/dofus2/meta/{lang}/almanax/bonuses/search", func(r chi.Router) { + r.With(useCors).With(languageChecker).Route(fmt.Sprintf("/dofus3/v%d/meta/{lang}/almanax/bonuses/search", DoduapiMajor), func(r chi.Router) { r.Get("/", SearchAlmanaxBonuses) }) - r.With(useCors).Route(routePrefix, func(r chi.Router) { + r.With(useCors).Route(fmt.Sprintf("/%s/v%d", gameRelease, DoduapiMajor), func(r chi.Router) { if PublishFileServer { imagesDir := http.Dir(filepath.Join(DockerMountDataPath, "data", "img")) diff --git a/types.go b/types.go index 1792574..ba96c8c 100644 --- a/types.go +++ b/types.go @@ -41,11 +41,15 @@ type ApiAllSearchItem struct { ImageUrls *ApiImageUrls `json:"image_urls,omitempty"` } +type ApiAllSearchResultType struct { + NameId string `json:"name_id"` +} + type ApiAllSearchResult struct { - Name string `json:"name"` - Id int `json:"ankama_id"` - Type string `json:"type"` - ItemFields *ApiAllSearchItem `json:"item_fields,omitempty"` + Name string `json:"name"` + Id int `json:"ankama_id"` + Type ApiAllSearchResultType `json:"type"` + ItemFields *ApiAllSearchItem `json:"item_fields,omitempty"` } type ApiEffect struct { @@ -82,42 +86,6 @@ func RenderEffects(effects *[]mapping.MappedMultilangEffect, lang string) []ApiE return nil } -type ApiSetEffect struct { - MinInt int `json:"int_minimum"` - MaxInt int `json:"int_maximum"` - Type ApiEffectType `json:"type"` - IgnoreMinInt bool `json:"ignore_int_min"` - IgnoreMaxInt bool `json:"ignore_int_max"` - Formatted string `json:"formatted"` - ItemCombination uint `json:"item_combination"` -} - -func RenderSetEffects(effects *[]mapping.MappedMultilangSetEffect, lang string) []ApiSetEffect { - var retEffects []ApiSetEffect - for _, effect := range *effects { - retEffects = append(retEffects, ApiSetEffect{ - MinInt: effect.Min, - MaxInt: effect.Max, - IgnoreMinInt: effect.IsMeta || effect.MinMaxIrrelevant == -2, - IgnoreMaxInt: effect.IsMeta || effect.MinMaxIrrelevant <= -1, - Type: ApiEffectType{ - Name: effect.Type[lang], - Id: effect.ElementId, - IsMeta: effect.IsMeta, - IsActive: effect.Active, - }, - Formatted: effect.Templated[lang], - ItemCombination: effect.ItemCombination, - }) - } - - if len(retEffects) > 0 { - return retEffects - } - - return nil -} - type ApiCondition struct { Operator string `json:"operator"` IntValue int `json:"int_value"` @@ -132,20 +100,19 @@ type ApiConditionNode struct { } type APIResource struct { - Id int `json:"ankama_id"` - Name string `json:"name"` - Description string `json:"description"` - Type ApiType `json:"type"` - Level int `json:"level"` - Pods int `json:"pods"` - ImageUrls ApiImageUrls `json:"image_urls,omitempty"` - Effects []ApiEffect `json:"effects,omitempty"` - Conditions []ApiCondition `json:"conditions,omitempty"` - ConditionTree *ApiConditionNode `json:"condition_tree,omitempty"` - Recipe []APIRecipe `json:"recipe,omitempty"` -} - -func RenderResource(item *mapping.MappedMultilangItem, lang string) APIResource { + Id int `json:"ankama_id"` + Name string `json:"name"` + Description string `json:"description"` + Type ApiType `json:"type"` + Level int `json:"level"` + Pods int `json:"pods"` + ImageUrls ApiImageUrls `json:"image_urls,omitempty"` + Effects []ApiEffect `json:"effects,omitempty"` + Conditions *ApiConditionNode `json:"conditions,omitempty"` + Recipe []APIRecipe `json:"recipe,omitempty"` +} + +func RenderResource(item *mapping.MappedMultilangItemUnity, lang string) APIResource { resource := APIResource{ Id: item.AnkamaId, Name: item.Name[lang], @@ -156,17 +123,11 @@ func RenderResource(item *mapping.MappedMultilangItem, lang string) APIResource Description: item.Description[lang], Level: item.Level, Pods: item.Pods, - ImageUrls: RenderImageUrls(ImageUrls(item.IconId, "item")), + ImageUrls: RenderImageUrls(ImageUrls(item.IconId, "item", ItemImgResolutions)), Recipe: nil, } - resource.ConditionTree = RenderConditionTree(item.ConditionTree, lang) - conditions := RenderConditions(&item.Conditions, lang) - if len(conditions) == 0 { - resource.Conditions = nil - } else { - resource.Conditions = conditions - } + resource.Conditions = RenderConditionTree(item.Conditions, lang) effects := RenderEffects(&item.Effects, lang) if len(effects) == 0 { @@ -179,22 +140,21 @@ func RenderResource(item *mapping.MappedMultilangItem, lang string) APIResource } type APIEquipment struct { - Id int `json:"ankama_id"` - Name string `json:"name"` - Description string `json:"description"` - Type ApiType `json:"type"` - IsWeapon bool `json:"is_weapon"` - Level int `json:"level"` - Pods int `json:"pods"` - ImageUrls ApiImageUrls `json:"image_urls,omitempty"` - Effects []ApiEffect `json:"effects,omitempty"` - Conditions []ApiCondition `json:"conditions,omitempty"` - ConditionTree *ApiConditionNode `json:"condition_tree,omitempty"` - Recipe []APIRecipe `json:"recipe,omitempty"` - ParentSet *APISetReverseLink `json:"parent_set,omitempty"` -} - -func RenderEquipment(item *mapping.MappedMultilangItem, lang string) APIEquipment { + Id int `json:"ankama_id"` + Name string `json:"name"` + Description string `json:"description"` + Type ApiType `json:"type"` + IsWeapon bool `json:"is_weapon"` + Level int `json:"level"` + Pods int `json:"pods"` + ImageUrls ApiImageUrls `json:"image_urls,omitempty"` + Effects []ApiEffect `json:"effects,omitempty"` + Conditions *ApiConditionNode `json:"conditions,omitempty"` + Recipe []APIRecipe `json:"recipe,omitempty"` + ParentSet *APISetReverseLink `json:"parent_set,omitempty"` +} + +func RenderEquipment(item *mapping.MappedMultilangItemUnity, lang string) APIEquipment { var setLink *APISetReverseLink = nil if item.HasParentSet { setLink = &APISetReverseLink{ @@ -213,20 +173,13 @@ func RenderEquipment(item *mapping.MappedMultilangItem, lang string) APIEquipmen Description: item.Description[lang], Level: item.Level, Pods: item.Pods, - ImageUrls: RenderImageUrls(ImageUrls(item.IconId, "item")), + ImageUrls: RenderImageUrls(ImageUrls(item.IconId, "item", ItemImgResolutions)), IsWeapon: false, Recipe: nil, ParentSet: setLink, } - equip.ConditionTree = RenderConditionTree(item.ConditionTree, lang) - conditions := RenderConditions(&item.Conditions, lang) - if len(conditions) == 0 { - equip.Conditions = nil - } else { - equip.Conditions = conditions - } - + equip.Conditions = RenderConditionTree(item.Conditions, lang) effects := RenderEffects(&item.Effects, lang) if len(effects) == 0 { equip.Effects = nil @@ -257,11 +210,9 @@ type APIWeapon struct { Pods int `json:"pods"` ImageUrls ApiImageUrls `json:"image_urls,omitempty"` Effects []ApiEffect `json:"effects,omitempty"` - Conditions []ApiCondition `json:"conditions,omitempty"` - ConditionTree *ApiConditionNode `json:"condition_tree,omitempty"` + Conditions *ApiConditionNode `json:"conditions,omitempty"` CriticalHitProbability int `json:"critical_hit_probability"` CriticalHitBonus int `json:"critical_hit_bonus"` - TwoHanded bool `json:"is_two_handed"` MaxCastPerTurn int `json:"max_cast_per_turn"` ApCost int `json:"ap_cost"` Range APIRange `json:"range"` @@ -269,7 +220,7 @@ type APIWeapon struct { ParentSet *APISetReverseLink `json:"parent_set,omitempty"` } -func RenderWeapon(item *mapping.MappedMultilangItem, lang string) APIWeapon { +func RenderWeapon(item *mapping.MappedMultilangItemUnity, lang string) APIWeapon { var setLink *APISetReverseLink = nil if item.HasParentSet { setLink = &APISetReverseLink{ @@ -288,11 +239,10 @@ func RenderWeapon(item *mapping.MappedMultilangItem, lang string) APIWeapon { Description: item.Description[lang], Level: item.Level, Pods: item.Pods, - ImageUrls: RenderImageUrls(ImageUrls(item.IconId, "item")), + ImageUrls: RenderImageUrls(ImageUrls(item.IconId, "item", ItemImgResolutions)), Recipe: nil, CriticalHitBonus: item.CriticalHitBonus, CriticalHitProbability: item.CriticalHitProbability, - TwoHanded: item.TwoHanded, MaxCastPerTurn: item.MaxCastPerTurn, ApCost: item.ApCost, Range: APIRange{ @@ -303,14 +253,7 @@ func RenderWeapon(item *mapping.MappedMultilangItem, lang string) APIWeapon { ParentSet: setLink, } - weapon.ConditionTree = RenderConditionTree(item.ConditionTree, lang) - conditions := RenderConditions(&item.Conditions, lang) - if len(conditions) == 0 { - weapon.Conditions = nil - } else { - weapon.Conditions = conditions - } - + weapon.Conditions = RenderConditionTree(item.Conditions, lang) effects := RenderEffects(&item.Effects, lang) if len(effects) == 0 { weapon.Effects = nil @@ -412,11 +355,10 @@ type APIListItem struct { ImageUrls ApiImageUrls `json:"image_urls,omitempty"` // extra fields - Description *string `json:"description,omitempty"` - Recipe []APIRecipe `json:"recipe,omitempty"` - Conditions []ApiCondition `json:"conditions,omitempty"` - ConditionTree *ApiConditionNode `json:"condition_tree,omitempty"` - Effects []ApiEffect `json:"effects,omitempty"` + Description *string `json:"description,omitempty"` + Recipe []APIRecipe `json:"recipe,omitempty"` + Conditions *ApiConditionNode `json:"conditions,omitempty"` + Effects []ApiEffect `json:"effects,omitempty"` // extra equipment IsWeapon *bool `json:"is_weapon,omitempty"` @@ -426,13 +368,12 @@ type APIListItem struct { // extra weapon CriticalHitProbability *int `json:"critical_hit_probability,omitempty"` CriticalHitBonus *int `json:"critical_hit_bonus,omitempty"` - TwoHanded *bool `json:"is_two_handed,omitempty"` MaxCastPerTurn *int `json:"max_cast_per_turn,omitempty"` ApCost *int `json:"ap_cost,omitempty"` Range *APIRange `json:"range,omitempty"` } -func RenderItemListEntry(item *mapping.MappedMultilangItem, lang string) APIListItem { +func RenderItemListEntry(item *mapping.MappedMultilangItemUnity, lang string) APIListItem { return APIListItem{ Id: item.AnkamaId, Name: item.Name[lang], @@ -441,20 +382,25 @@ func RenderItemListEntry(item *mapping.MappedMultilangItem, lang string) APIList Id: item.Type.ItemTypeId, }, Level: item.Level, - ImageUrls: RenderImageUrls(ImageUrls(item.IconId, "item")), + ImageUrls: RenderImageUrls(ImageUrls(item.IconId, "item", ItemImgResolutions)), } } +type APIListItemType struct { + Id int `json:"ankama_id"` + NameId string `json:"name_id"` // not translated +} + type APIListTypedItem struct { - Id int `json:"ankama_id"` - Name string `json:"name"` - Type ApiType `json:"type"` - ItemSubtype string `json:"item_subtype"` - Level int `json:"level"` - ImageUrls ApiImageUrls `json:"image_urls,omitempty"` + Id int `json:"ankama_id"` + Name string `json:"name"` + Type ApiType `json:"type"` + ItemSubtype APIListItemType `json:"item_subtype"` + Level int `json:"level"` + ImageUrls ApiImageUrls `json:"image_urls,omitempty"` } -func RenderTypedItemListEntry(item *mapping.MappedMultilangItem, lang string) APIListTypedItem { +func RenderTypedItemListEntry(item *mapping.MappedMultilangItemUnity, lang string) APIListTypedItem { return APIListTypedItem{ Id: item.AnkamaId, Name: item.Name[lang], @@ -462,17 +408,20 @@ func RenderTypedItemListEntry(item *mapping.MappedMultilangItem, lang string) AP Name: item.Type.Name[lang], Id: item.Type.ItemTypeId, }, - ItemSubtype: CategoryIdApiMapping(item.Type.CategoryId), - Level: item.Level, - ImageUrls: RenderImageUrls(ImageUrls(item.IconId, "item")), + ItemSubtype: APIListItemType{ + Id: item.Type.CategoryId, + NameId: CategoryIdApiMapping(item.Type.CategoryId), + }, + Level: item.Level, + ImageUrls: RenderImageUrls(ImageUrls(item.IconId, "item", ItemImgResolutions)), } } type APIListMount struct { - Id int `json:"ankama_id"` - Name string `json:"name"` - FamilyName string `json:"family_name"` - ImageUrls ApiImageUrls `json:"image_urls,omitempty"` + Id int `json:"ankama_id"` + Name string `json:"name"` + Family APIMountFamily `json:"family"` + ImageUrls ApiImageUrls `json:"image_urls,omitempty"` // extra fields Effects []ApiEffect `json:"effects,omitempty"` @@ -480,10 +429,13 @@ type APIListMount struct { func RenderMountListEntry(mount *mapping.MappedMultilangMount, lang string) APIListMount { return APIListMount{ - Id: mount.AnkamaId, - Name: mount.Name[lang], - ImageUrls: RenderImageUrls(ImageUrls(mount.AnkamaId, "mount")), - FamilyName: mount.FamilyName[lang], + Id: mount.AnkamaId, + Name: mount.Name[lang], + ImageUrls: RenderImageUrls(ImageUrls(mount.AnkamaId, "mount", MountImgResolutions)), + Family: APIMountFamily{ + Id: mount.AnkamaId, + Name: mount.FamilyName[lang], + }, } } @@ -508,7 +460,7 @@ func RenderRecipe(recipe mapping.MappedMultilangRecipe, db *memdb.MemDB) []APIRe log.Error(err) return nil } - item := raw.(*mapping.MappedMultilangItem) + item := raw.(*mapping.MappedMultilangItemUnity) apiRecipes = append(apiRecipes, APIRecipe{ AnkamaId: entry.ItemId, @@ -534,20 +486,28 @@ type APIPageSet struct { Items []APIListSet `json:"sets"` } +type APIMountFamily struct { + Id int `json:"ankama_id"` + Name string `json:"name"` +} + type APIMount struct { - Id int `json:"ankama_id"` - Name string `json:"name"` - FamilyName string `json:"family_name"` - ImageUrls ApiImageUrls `json:"image_urls,omitempty"` - Effects []ApiEffect `json:"effects,omitempty"` + Id int `json:"ankama_id"` + Name string `json:"name"` + Family APIMountFamily `json:"family"` + ImageUrls ApiImageUrls `json:"image_urls,omitempty"` + Effects []ApiEffect `json:"effects,omitempty"` } func RenderMount(mount *mapping.MappedMultilangMount, lang string) APIMount { resMount := APIMount{ - Id: mount.AnkamaId, - Name: mount.Name[lang], - FamilyName: mount.FamilyName[lang], - ImageUrls: RenderImageUrls(ImageUrls(mount.AnkamaId, "mount")), + Id: mount.AnkamaId, + Name: mount.Name[lang], + Family: APIMountFamily{ + Id: mount.AnkamaId, + Name: mount.FamilyName[lang], + }, + ImageUrls: RenderImageUrls(ImageUrls(mount.AnkamaId, "mount", MountImgResolutions)), } effects := RenderEffects(&mount.Effects, lang) @@ -561,49 +521,53 @@ func RenderMount(mount *mapping.MappedMultilangMount, lang string) APIMount { } type APIListSet struct { - Id int `json:"ankama_id"` - Name string `json:"name"` - Items int `json:"items"` - Level int `json:"level"` - IsCosmetic bool `json:"is_cosmetic"` + Id int `json:"ankama_id"` + Name string `json:"name"` + Items int `json:"items"` + Level int `json:"level"` + ContainsCosmetics bool `json:"contains_cosmetics"` + ContainsCosmeticsOnly bool `json:"contains_cosmetics_only"` // extra fields - Effects [][]ApiSetEffect `json:"effects,omitempty"` - ItemIds []int `json:"equipment_ids,omitempty"` + Effects map[int][]ApiEffect `json:"effects,omitempty"` + ItemIds []int `json:"equipment_ids,omitempty"` } -func RenderSetListEntry(set *mapping.MappedMultilangSet, lang string) APIListSet { +func RenderSetListEntry(set *mapping.MappedMultilangSetUnity, lang string) APIListSet { return APIListSet{ - Id: set.AnkamaId, - Name: set.Name[lang], - Items: len(set.ItemIds), - Level: set.Level, - IsCosmetic: set.IsCosmetic, + Id: set.AnkamaId, + Name: set.Name[lang], + Items: len(set.ItemIds), + Level: set.Level, + ContainsCosmetics: set.ContainsCosmetics, + ContainsCosmeticsOnly: set.ContainsCosmeticsOnly, } } type APISet struct { - AnkamaId int `json:"ankama_id"` - Name string `json:"name"` - ItemIds []int `json:"equipment_ids"` - Effects [][]ApiSetEffect `json:"effects,omitempty"` - Level int `json:"highest_equipment_level"` - IsCosmetic bool `json:"is_cosmetic"` -} - -func RenderSet(set *mapping.MappedMultilangSet, lang string) APISet { - var effects [][]ApiSetEffect - for _, effect := range set.Effects { - effects = append(effects, RenderSetEffects(&effect, lang)) + AnkamaId int `json:"ankama_id"` + Name string `json:"name"` + ItemIds []int `json:"equipment_ids"` + Effects map[int][]ApiEffect `json:"effects,omitempty"` + Level int `json:"highest_equipment_level"` + ContainsCosmetics bool `json:"contains_cosmetics"` + ContainsCosmeticsOnly bool `json:"contains_cosmetics_only"` +} + +func RenderSet(set *mapping.MappedMultilangSetUnity, lang string) APISet { + effects := make(map[int][]ApiEffect) + for itemCombination, effect := range set.Effects { + effects[itemCombination] = RenderEffects(&effect, lang) } resSet := APISet{ - AnkamaId: set.AnkamaId, - Name: set.Name[lang], - ItemIds: set.ItemIds, - Effects: effects, - Level: set.Level, - IsCosmetic: set.IsCosmetic, + AnkamaId: set.AnkamaId, + Name: set.Name[lang], + ItemIds: set.ItemIds, + Effects: effects, + Level: set.Level, + ContainsCosmetics: set.ContainsCosmetics, + ContainsCosmeticsOnly: set.ContainsCosmeticsOnly, } if len(effects) == 0 { diff --git a/utils.go b/utils.go index ebc1a7e..78c06e6 100644 --- a/utils.go +++ b/utils.go @@ -23,8 +23,9 @@ import ( ) var ( - Languages = []string{"de", "en", "es", "fr", "it", "pt"} - ImgResolutions = []string{"200", "400", "800"} + Languages = []string{"de", "en", "es", "fr", "pt"} + ItemImgResolutions = []string{"64", "128"} + MountImgResolutions = []string{"64", "256"} ApiHostName string ApiPort string ApiScheme string @@ -43,8 +44,8 @@ var ( ReleaseUrl string UpdateHookToken string DofusVersion string - FullImg bool CurrentVersion GameVersion + ApiVersion string ) var currentWd string @@ -92,6 +93,10 @@ func ExtractTarGz(baseDir string, gzipStream io.Reader) error { return fmt.Errorf("ExtractTarGz: Mkdir() failed: %w", err) } case tar.TypeReg: + fileDir := filepath.Dir(header.Name) + if err := os.MkdirAll(filepath.Join(baseDir, fileDir), 0755); err != nil { + return fmt.Errorf("ExtractTarGz: Mkdir() failed: %w", err) + } outFile, err := os.Create(filepath.Join(baseDir, header.Name)) if err != nil { return fmt.Errorf("ExtractTarGz: Create() failed: %w", err) @@ -120,7 +125,7 @@ func DownloadExtract(filename string) error { if err != nil { return err } - err = ExtractTarGz("", response.Body) + err = ExtractTarGz(DockerMountDataPath, response.Body) if err != nil { return err } @@ -128,36 +133,124 @@ func DownloadExtract(filename string) error { return nil } -func DownloadImages() error { - var err error - resolutions := []string{"200", "400", "800"} +func copyFile(src, dst string) error { + sourceFile, err := os.Open(src) + if err != nil { + return err + } + defer sourceFile.Close() + + destinationFile, err := os.Create(dst) + if err != nil { + return err + } + defer destinationFile.Close() + + _, err = io.Copy(destinationFile, sourceFile) + return err +} - err = DownloadExtract("items_images") +func copyDir(src, dst string) error { + entries, err := os.ReadDir(src) if err != nil { - return fmt.Errorf("could not download items_images") + return err } - if FullImg { - for _, resolution := range resolutions { - err = DownloadExtract(fmt.Sprintf("items_images_%s", resolution)) - if err != nil { - return fmt.Errorf("could not download items_images %s", resolution) + for _, entry := range entries { + srcPath := filepath.Join(src, entry.Name()) + dstPath := filepath.Join(dst, entry.Name()) + + switch entry.Type() { + case os.ModeDir: + if _, err := os.Stat(dstPath); os.IsNotExist(err) { + if err := os.Mkdir(dstPath, 0755); err != nil { + return err + } + } + if err := copyDir(srcPath, dstPath); err != nil { + return err + } + default: + if err := copyFile(srcPath, dstPath); err != nil { + return err } } } - err = DownloadExtract("mounts_images") + return nil +} + +func DownloadImages() error { + var err error + + // -- items -- + err = DownloadExtract(fmt.Sprintf("items_images_64")) if err != nil { - return fmt.Errorf("could not download mount_images") + return fmt.Errorf("could not download items_images: %v", err) } - if FullImg { - for _, resolution := range resolutions { - err = DownloadExtract(fmt.Sprintf("mounts_images_%s", resolution)) - if err != nil { - return fmt.Errorf("could not download mount_images %s", resolution) - } - } + err = DownloadExtract(fmt.Sprintf("items_images_128")) + if err != nil { + return fmt.Errorf("could not download items_images: %v", err) + } + + oldPath1x := filepath.Join(DockerMountDataPath, "data", "img", "item", "1x") + oldPath2x := filepath.Join(DockerMountDataPath, "data", "img", "item", "2x") + newPath := filepath.Join(DockerMountDataPath, "data", "img", "item") + + err = copyDir(oldPath1x, newPath) + if err != nil { + return fmt.Errorf("could not copy images to path: %v", err) + } + + err = copyDir(oldPath2x, newPath) + if err != nil { + return fmt.Errorf("could not copy images to path: %v", err) + } + + err = os.RemoveAll(oldPath1x) + if err != nil { + return fmt.Errorf("could not remove old images dir: %v", err) + } + + err = os.RemoveAll(oldPath2x) + if err != nil { + return fmt.Errorf("could not remove old images dir: %v", err) + } + + // -- mounts -- + err = DownloadExtract(fmt.Sprintf("mounts_images_64")) + if err != nil { + return fmt.Errorf("could not download items_images: %v", err) + } + + err = DownloadExtract(fmt.Sprintf("mounts_images_256")) + if err != nil { + return fmt.Errorf("could not download items_images: %v", err) + } + + oldPathSmall := filepath.Join(DockerMountDataPath, "data", "img", "mount", "small") + oldPathBig := filepath.Join(DockerMountDataPath, "data", "img", "mount", "big") + newPathMounts := filepath.Join(DockerMountDataPath, "data", "img", "mount") + + err = copyDir(oldPathSmall, newPathMounts) + if err != nil { + return fmt.Errorf("could not copy images to path: %v", err) + } + + err = copyDir(oldPathBig, newPathMounts) + if err != nil { + return fmt.Errorf("could not copy images to path: %v", err) + } + + err = os.RemoveAll(oldPathSmall) + if err != nil { + return fmt.Errorf("could not remove old images dir: %v", err) + } + + err = os.RemoveAll(oldPathBig) + if err != nil { + return fmt.Errorf("could not remove old images dir: %v", err) } return nil @@ -197,7 +290,7 @@ func ReadEnvs() { dofusVersion := viper.GetString("DOFUS_VERSION") if dofusVersion == "" { - releaseApiResponse, err := http.Get(fmt.Sprintf("https://api.github.com/repos/dofusdude/dofus2-%s/releases/latest", betaStr)) + releaseApiResponse, err := http.Get(fmt.Sprintf("https://api.github.com/repos/dofusdude/dofus3-%s/releases/latest", betaStr)) if err != nil { log.Fatal(err) } @@ -225,7 +318,7 @@ func ReadEnvs() { ElementsUrl = fmt.Sprintf("https://raw.githubusercontent.com/dofusdude/doduda/main/persistent/elements.%s.json", betaStr) TypesUrl = fmt.Sprintf("https://raw.githubusercontent.com/dofusdude/doduda/main/persistent/item_types.%s.json", betaStr) - ReleaseUrl = fmt.Sprintf("https://github.com/dofusdude/dofus2-%s/releases/download/%s", betaStr, DofusVersion) + ReleaseUrl = fmt.Sprintf("https://github.com/dofusdude/dofus3-%s/releases/download/%s", betaStr, DofusVersion) ApiScheme = viper.GetString("API_SCHEME") ApiHostName = viper.GetString("API_HOSTNAME") @@ -238,20 +331,15 @@ func ReadEnvs() { DockerMountDataPath = viper.GetString("DIR") } -func ImageUrls(iconId int, apiType string) []string { +func ImageUrls(iconId int, apiType string, resolutions []string) []string { betaImage := "" if IsBeta { betaImage = "beta" } - baseUrl := fmt.Sprintf("%s://%s/dofus2%s/img/%s", ApiScheme, ApiHostName, betaImage, apiType) + baseUrl := fmt.Sprintf("%s://%s/dofus3%s/v%d/img/%s", ApiScheme, ApiHostName, betaImage, DoduapiMajor, apiType) var urls []string - urls = append(urls, fmt.Sprintf("%s/%d.png", baseUrl, iconId)) - - if ImgResolutions == nil { - return urls - } - for _, resolution := range ImgResolutions { + for _, resolution := range resolutions { resolutionUrl := fmt.Sprintf("%s/%d-%s.png", baseUrl, iconId, resolution) urls = append(urls, resolutionUrl) }