diff --git a/.gitignore b/.gitignore index aa83554..aacf7dc 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ node_modules/ *.rdb coverage/ modules/config.js +undefined*.png \ No newline at end of file diff --git a/README.md b/README.md index 34a950f..9c8e084 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Crafatar [![travis](https://img.shields.io/travis/Jake0oo0/crafatar.svg?style=flat)](https://travis-ci.org/Jake0oo0/crafatar/) [![Coverage Status](https://img.shields.io/coveralls/Jake0oo0/crafatar.svg?style=flat)](https://coveralls.io/r/Jake0oo0/crafatar) +# Crafatar [![travis](https://img.shields.io/travis/Jake0oo0/crafatar.svg?style=flat)](https://travis-ci.org/Jake0oo0/crafatar/) [![Coverage Status](https://img.shields.io/coveralls/Jake0oo0/crafatar.svg?style=flat)](https://coveralls.io/r/Jake0oo0/crafatar) [![Code Climate](https://codeclimate.com/github/Jake0oo0/crafatar/badges/gpa.svg)](https://codeclimate.com/github/Jake0oo0/crafatar) https://crafatar.com diff --git a/app.js b/app.js deleted file mode 100644 index 8fcd215..0000000 --- a/app.js +++ /dev/null @@ -1,63 +0,0 @@ -var express = require("express"); -var path = require("path"); -var logger = require("morgan"); -var cookieParser = require("cookie-parser"); -var bodyParser = require("body-parser"); - -var routes = require("./routes/index"); -var avatars = require("./routes/avatars"); -var skins = require("./routes/skins"); -var renders = require('./routes/renders'); -var capes = require("./routes/capes"); - -var app = express(); - -// view engine setup -app.set("views", path.join(__dirname, "views")); -app.set("view engine", "jade"); - -app.use(logger("dev")); -app.use(bodyParser.json()); -app.use(bodyParser.urlencoded({ extended: false })); -app.use(cookieParser()); -app.use(express.static(path.join(__dirname, "public"))); - -app.use('/', routes); -app.use('/avatars', avatars); -app.use('/skins', skins); -app.use('/renders', renders); -app.use("/capes", capes); - - -// catch 404 and forward to error handler -app.use(function(req, res, next) { - var err = new Error("Not Found"); - err.status = 404; - next(err); -}); - -// error handlers - -// development error handler -// will print stacktrace -if (app.get("env") === "development") { - app.use(function(err, req, res, next) { - res.status(err.status || 500); - res.render("error", { - message: err.message, - error: err - }); - }); -} - -// production error handler -// no stacktraces leaked to user -app.use(function(err, req, res, next) { - res.status(err.status || 500); - res.render("error", { - message: err.message, - error: {} - }); -}); - -module.exports = app; diff --git a/skins/capes/.gitkeep b/images/capes/.gitkeep similarity index 100% rename from skins/capes/.gitkeep rename to images/capes/.gitkeep diff --git a/modules/cache.js b/modules/cache.js index fd96122..323e9f2 100644 --- a/modules/cache.js +++ b/modules/cache.js @@ -103,14 +103,16 @@ exp.update_timestamp = function(uuid, hash) { }; // create the key +uuid+, store +hash+ and time -exp.save_hash = function(uuid, hash) { +exp.save_hash = function(uuid, skin, cape) { logging.log(uuid + " cache: saving hash"); + logging.log("skin:" + skin + " cape:" + cape); var time = new Date().getTime(); // store shorter null byte instead of "null" - hash = hash || "."; + skin = skin || "."; + cape = cape || "."; // store uuid in lower case if not null uuid = uuid && uuid.toLowerCase(); - redis.hmset(uuid, "h", hash, "t", time); + redis.hmset(uuid, "s", skin, "c", cape, "t", time); }; exp.remove_hash = function(uuid) { @@ -119,7 +121,7 @@ exp.remove_hash = function(uuid) { }; // get a details object for +uuid+ -// {hash: "0123456789abcdef", time: 1414881524512} +// {skin: "0123456789abcdef", cape: "gs1gds1g5d1g5ds1", time: 1414881524512} // null when uuid unkown exp.get_details = function(uuid, callback) { // get uuid in lower case if not null @@ -128,7 +130,8 @@ exp.get_details = function(uuid, callback) { var details = null; if (data) { details = { - hash: (data.h == "." ? null : data.h), + skin: (data.s === "." ? null : data.s), + cape: (data.c === "." ? null : data.c), time: Number(data.t) }; } diff --git a/modules/cleaner.js b/modules/cleaner.js index 00a863d..f014591 100644 --- a/modules/cleaner.js +++ b/modules/cleaner.js @@ -15,7 +15,7 @@ function should_clean_redis(callback) { callback(err, false); } else { try { - logging.debug(info); + //logging.debug(info.toString()); logging.debug("used mem:" + info.used_memory); var used = parseInt(info.used_memory) / 1024; logging.log("RedisCleaner: " + used + "KB used"); @@ -71,7 +71,6 @@ exp.run = function() { var helmdir = __dirname + "/../" + config.helms_dir; var renderdir = __dirname + "/../" + config.renders_dir; var skindir = __dirname + "/../" + config.skins_dir; - fs.readdir(facesdir, function (err, files) { for (var i = 0, l = Math.min(files.length, config.cleaning_amount); i < l; i++) { var filename = files[i]; @@ -98,4 +97,4 @@ exp.run = function() { function nil () {} -module.exports = exp; \ No newline at end of file +module.exports = exp; diff --git a/modules/config.example.js b/modules/config.example.js index 651d371..40ca59b 100644 --- a/modules/config.example.js +++ b/modules/config.example.js @@ -2,6 +2,9 @@ var config = { min_size: 1, // for avatars max_size: 512, // for avatars; too big values might lead to slow response time or DoS default_size: 160, // for avatars; size to be used when no size given + min_scale: 1, // for renders + max_scale: 10, // for renders; too big values might lead to slow response time or DoS + default_scale: 6, // for renders; scale to be used when no scale given local_cache_time: 1200, // seconds until we will check if the image changed. should be > 60 to prevent mojang 429 response browser_cache_time: 3600, // seconds until browser will request image again cleaning_interval: 1800, // seconds interval: deleting images if disk size at limit @@ -9,15 +12,12 @@ var config = { cleaning_redis_limit: 24576, // max allowed used KB on redis to trigger redis flush cleaning_amount: 50000, // amount of avatar (and their helm) files to clean http_timeout: 1000, // ms until connection to mojang is dropped + debug_enabled: false, // enables logging.debug faces_dir: "images/faces/", // directory where faces are kept. should have trailing "/" helms_dir: "images/helms/", // directory where helms are kept. should have trailing "/" skins_dir: "images/skins/", // directory where skins are kept. should have trailing "/" renders_dir: "images/renders/",// Directory where rendered skins are kept. should have trailing "/" - capes_dir: "images/capes/", // directory where capes are kept. should have trailing "/" - debug_enabled: false, // enables logging.debug - min_scale: 1, // for renders - max_scale: 10, // for renders; too big values might lead to slow response time or DoS - default_scale: 6 // for renders; scale to be used when no scale given + capes_dir: "images/capes/", // directory where capes are kept. should have trailing "/" }; module.exports = config; diff --git a/modules/helpers.js b/modules/helpers.js index fe2f3ba..d72b0d5 100644 --- a/modules/helpers.js +++ b/modules/helpers.js @@ -10,112 +10,145 @@ var fs = require("fs"); var valid_uuid = /^([0-9a-f-A-F-]{32,36}|[a-zA-Z0-9_]{1,16})$/; // uuid|username var hash_pattern = /[0-9a-f]+$/; +// gets the hash from the textures.minecraft.net +url+ function get_hash(url) { return hash_pattern.exec(url)[0].toLowerCase(); } -// requests skin for +uuid+ and extracts face/helm if image hash in +details+ changed -// callback contains error, image hash -function store_images(uuid, details, callback) { - // get skin_url for +uuid+ - networking.get_skin_url(uuid, function(err, skin_url) { - if (err) { - callback(err, null); - } else { - if (skin_url) { - logging.log(uuid + " " + skin_url); - // set file paths - var hash = get_hash(skin_url); - if (details && details.hash == hash) { - // hash hasn't changed - logging.log(uuid + " hash has not changed"); - cache.update_timestamp(uuid, hash); - callback(null, hash); - } else { - // hash has changed - logging.log(uuid + " new hash: " + hash); - var facepath = __dirname + "/../" + config.faces_dir + hash + ".png"; - var helmpath = __dirname + "/../" + config.helms_dir + hash + ".png"; - - fs.exists(facepath, function (exists) { - if (exists) { - logging.log(uuid + " Avatar already exists, not downloading"); - cache.save_hash(uuid, hash); - callback(null, hash); - } else { - // download skin - networking.get_skin(skin_url, uuid, function(err, img) { - if (err || !img) { - callback(err, null); - } else { - // extract face / helm - skins.extract_face(img, facepath, function(err) { - if (err) { - callback(err); - } else { - logging.log(uuid + " face extracted"); - logging.debug(uuid + " " + facepath); - skins.extract_helm(uuid, facepath, img, helmpath, function(err) { - logging.log(uuid + " helm extracted"); - logging.debug(uuid + " " + helmpath); - cache.save_hash(uuid, hash); - callback(err, hash); - }); - } - }); - } - }); - } - }); - } +function store_skin(uuid, profile, details, callback) { + networking.get_skin_url(uuid, profile, function(url) { + if (url) { + var hash = get_hash(url); + if (details && details.skin === hash) { + cache.update_timestamp(uuid, hash); + callback(null, hash); } else { - // profile found, but has no skin - cache.save_hash(uuid, null); - callback(null, null); - } - } - }); - - networking.get_cape_url(uuid, function(err, cape_url) { - if (err) { - callback(err, null); - } else { - if (cape_url) { - logging.log(uuid + " " + cape_url); - // set file paths - var hash = get_hash(cape_url); - if (details && details.hash == hash) { - // hash hasn't changed - logging.log(uuid + " hash has not changed"); - cache.update_timestamp(uuid, hash); - callback(null, hash); - } else { - // hash has changed - logging.log(uuid + " new hash: " + hash); - var capepath = __dirname + "/../" + config.capes_dir + hash + ".png"; - - if (fs.existsSync(capepath)) { - logging.log(uuid + " Cape already exists, not downloading"); - cache.save_hash(uuid, hash); + logging.log(uuid + " new skin hash: " + hash); + var facepath = __dirname + "/../" + config.faces_dir + hash + ".png"; + var helmpath = __dirname + "/../" + config.helms_dir + hash + ".png" + fs.exists(facepath, function(exists) { + if (exists) { + logging.log(uuid + " skin already exists, not downloading"); callback(null, hash); } else { - // download cape - networking.get_cape(cape_url, function(err, img) { + networking.get_from(url, function(img, response, err) { if (err || !img) { callback(err, null); + } else { + skins.extract_face(img, facepath, function(err) { + if (err) { + logging.error(err); + callback(err, null); + } else { + logging.log(uuid + " face extracted"); + skins.extract_helm(facepath, img, helmpath, function(err) { + logging.log(uuid + " helm extracted"); + logging.debug(helmpath); + callback(err, hash); + }); + } + }); } }); } - } - } else { - // profile found, but has no cape - cache.save_hash(uuid, null); - callback(null, null); + }); } + } else { + callback(null, null); } }); } +function store_cape(uuid, profile, details, callback) { + networking.get_cape_url(uuid, profile, function(url) { + if (url) { + var hash = get_hash(url); + if (details && details.cape === hash) { + cache.update_timestamp(uuid, hash); + callback(null, hash); + } else { + logging.log(uuid + " new cape hash: " + hash); + var capepath = __dirname + "/../" + config.capes_dir + hash + ".png"; + fs.exists(capepath, function(exists) { + if (exists) { + logging.log(uuid + " cape already exists, not downloading"); + callback(null, hash); + } else { + networking.get_from(url, function(img, response, err) { + if (err || !img) { + logging.error(err); + callback(err, null); + } else { + skins.save_image(img, capepath, function(err) { + logging.log(uuid + " cape saved"); + callback(err, hash); + }); + } + }); + } + }); + } + } else { + callback(null, null); + } + }); +} + +function remove_from_array(arr, item) { + var i; + while((i = arr.indexOf(item)) !== -1) { + arr.splice(i, 1); + } +} + +// downloads the images for +uuid+ while checking the cache +// status based on +details+. +whichhash+ specifies which +// image is more important, and should be called back on +// +callback+ contains the error buffer and image hash +var currently_running = []; +function callback_for(uuid, which, err, cape_hash, skin_hash) { + for (var i = 0; i < currently_running.length; i++) { + if (currently_running[i] && currently_running[i].uuid === uuid && (currently_running[i].which === which || which === null)) { + var will_call = currently_running[i]; + will_call.callback(err, will_call.which === 'skin' ? skin_hash : cape_hash); + //remove_from_array(currently_running, i); + delete(currently_running[i]); + } + } +} + +function array_has_hash(arr, property, value) { + for (var i = 0; i < arr.length; i++) { + if (arr[i] && arr[i][property] === value) { + return true; + } + } + return false; +} + +function store_images(uuid, details, whichhash, callback) { + var isUUID = uuid.length > 16; + var new_hash = { 'uuid': uuid, 'which': whichhash, 'callback': callback }; + if (!array_has_hash(currently_running, 'uuid', uuid)) { + currently_running.push(new_hash); + networking.get_profile((isUUID ? uuid : null), function(err, profile) { + if (err || (isUUID && !profile)) { + callback_for(uuid, err, null, null); + } else { + store_skin(uuid, profile, details, function(err, skin_hash) { + cache.save_hash(uuid, skin_hash, null); + callback_for(uuid, 'skin', err, null, skin_hash); + store_cape(uuid, profile, details, function(err, cape_hash) { + cache.save_hash(uuid, skin_hash, cape_hash); + callback_for(uuid, 'cape', err, cape_hash, skin_hash); + }); + }); + } + }); + } else { + currently_running.push(new_hash); + } +} var exp = {}; @@ -125,7 +158,6 @@ exp.uuid_valid = function(uuid) { return valid_uuid.test(uuid); }; - // decides whether to get an image from disk or to download it // callback contains error, status, hash // the status gives information about how the image was received @@ -134,49 +166,33 @@ exp.uuid_valid = function(uuid) { // 1: "cached" - found on disk // 2: "downloaded" - profile downloaded, skin downloaded from mojang servers // 3: "checked" - profile re-downloaded (was too old), but it has either not changed or has no skin -exp.get_image_hash = function(uuid, callback) { +exp.get_image_hash = function(uuid, raw_type, callback) { cache.get_details(uuid, function(err, details) { + var type = (details !== null ? (raw_type === "skin" ? details.skin : details.cape) : null); if (err) { callback(err, -1, null); } else { - if (details && details.time + config.local_cache_time * 1000 >= new Date().getTime()) { - // uuid known + recently updated - logging.log(uuid + " uuid cached & recently updated"); - callback(null, (details.hash ? 1 : 0), details.hash); + if (details && details.time + config.local_cache_time * 1000 >= new Date().getTime()) {logging.log(uuid + " uuid cached & recently updated"); + callback(null, (type ? 1 : 0), type); + } else { + if (details) { + logging.log(uuid + " uuid cached, but too old"); } else { - if (details) { - logging.log(uuid + " uuid cached, but too old"); + logging.log(uuid + " uuid not cached"); + } + store_images(uuid, details, raw_type, function(err, hash) { + if (err) { + callback(err, -1, details && type); } else { - logging.log(uuid + " uuid not cached"); + var status = details && (type === hash) ? 3 : 2; + logging.debug(uuid + " old hash: " + (details && type)); + logging.log(uuid + " hash: " + hash); + callback(null, status, hash); } - store_images(uuid, details, function(err, hash) { - if (err) { - callback(err, -1, details && details.hash); - } else { - // skin is only checked (3) when uuid known AND hash didn't change - // in all other cases the skin is downloaded (2) - var status = details && (details.hash == hash) ? 3 : 2; - logging.debug(uuid + " old hash: " + (details && details.hash)); - logging.log(uuid + " hash: " + hash); - callback(null, status, hash); - } - }); - } + }); } - }); -}; - -exp.get_cape_hash = function(uuid, callback) { - cache.get_details(uuid, function(err, details) { - if (err) { - callback(err, -1, null); - } else { - if (details && details.time + config.local_cache_time * 1000 >= new Date().getTime()) { - logging.log(uuid + " uuid cached & recently updated"); - - } - } - }) + } +}); }; @@ -185,17 +201,16 @@ exp.get_cape_hash = function(uuid, callback) { // image is the user's face+helm when helm is true, or the face otherwise // for status, see get_image_hash exp.get_avatar = function(uuid, helm, size, callback) { - exp.get_image_hash(uuid, function(err, status, hash) { + logging.log("request: " + uuid); + exp.get_image_hash(uuid, "skin", function(err, status, hash) { if (hash) { var facepath = __dirname + "/../" + config.faces_dir + hash + ".png"; var helmpath = __dirname + "/../" + config.helms_dir + hash + ".png"; var filepath = facepath; - fs.exists(helmpath, function (exists) { if (helm && exists) { filepath = helmpath; } - skins.resize_img(filepath, size, function(img_err, result) { if (img_err) { callback(img_err, -1, null, hash); @@ -216,7 +231,8 @@ exp.get_avatar = function(uuid, helm, size, callback) { // handles requests for +uuid+ skins // callback contains error, hash, image buffer exp.get_skin = function(uuid, callback) { - exp.get_image_hash(uuid, function(err, status, hash) { + logging.log(uuid + " skin request"); + exp.get_image_hash(uuid, 'skin', function(err, status, hash) { var skinpath = __dirname + "/../" + config.skins_dir + hash + ".png"; fs.exists(skinpath, function (exists) { if (exists) { @@ -235,7 +251,7 @@ exp.get_skin = function(uuid, callback) { function get_type(helm, body) { var text = body ? "body" : "head"; - return helm ? text+"helm" : text; + return helm ? text + "helm" : text; } // handles creations of skin renders @@ -247,51 +263,84 @@ exp.get_render = function(uuid, scale, helm, body, callback) { return; } var renderpath = __dirname + "/../" + config.renders_dir + hash + "-" + scale + "-" + get_type(helm, body) + ".png"; - fs.exists(renderpath, function (exists) { + fs.exists(renderpath, function(exists) { if (exists) { renders.open_render(uuid, renderpath, function(err, img) { callback(err, 1, hash, img); }); return; - } - if (!img) { - callback(err, 0, hash, null); - return; - } - renders.draw_model(uuid, img, scale, helm, body, function(err, img) { - if (err) { - callback(err, -1, hash, null); - } else if (!img) { - callback(null, 0, hash, null); - } else { - fs.writeFile(renderpath, img, "binary", function(err){ - if (err) { - logging.log(uuid + " error: " + err); - } - callback(null, 2, hash, img); - }); + } else { + if (!img) { + callback(err, 0, hash, null); + return; } - }); + renders.draw_model(uuid, img, scale, helm, body, function(err, img) { + if (err) { + callback(err, -1, hash, null); + } else if (!img) { + callback(null, 0, hash, null); + } else { + fs.writeFile(renderpath, img, "binary", function(err) { + if (err) { + logging.log(err); + } + callback(null, 2, hash, img); + }); + } + }); + } }); }); }; + +// handles requests for +uuid+ skins +// callback contains error, hash, image buffer +exp.get_skin = function(uuid, callback) { + logging.log(uuid + " skin request"); + exp.get_image_hash(uuid, "skin", function(err, status, hash) { + var skinpath = __dirname + "/../" + config.skins_dir + hash + ".png"; + fs.exists(skinpath, function(exists) { + if (exists) { + logging.log("skin already exists, not downloading"); + skins.open_skin(skinpath, function(err, img) { + callback(err, hash, img); + }); + } else { + networking.save_texture(uuid, hash, skinpath, function(err, response, img) { + callback(err, hash, img); + }); + } + }); + }); +}; + +// handles requests for +uuid+ capes +// callback contains error, hash, image buffer exp.get_cape = function(uuid, callback) { logging.log(uuid + " cape request"); - exp.get_image_hash(uuid, function(err, status, hash) { - if (hash) { - var capeurl = "http://textures.minecraft.net/texture/" + hash; - networking.get_cape(capeurl, function(err, img) { - if (err) { - logging.error("error while downloading cape"); - callback(err, hash, null); - } else { - callback(null, hash, img); - } - }); - } else { + exp.get_image_hash(uuid, "cape", function(err, status, hash) { + if (!hash) { callback(err, null, null); + return; } + var capepath = __dirname + "/../" + config.capes_path + hash + ".png"; + fs.exists(capepath, function(exists) { + if (exists) { + logging.log("cape already exists, not downloading"); + skins.open_skin(capepath, function(err, img) { + callback(err, hash, img); + }); + } else { + networking.save_texture(uuid, hash, capepath, function(err, response, img) { + if (response && response.statusCode === 404) { + callback(err, hash, null); + } else { + callback(err, hash, img); + } + }); + } + }); }); }; diff --git a/modules/networking.js b/modules/networking.js index 9f36e92..9354f00 100644 --- a/modules/networking.js +++ b/modules/networking.js @@ -1,224 +1,195 @@ var logging = require("./logging"); -var request = require("request"); +var request = require("requestretry"); var config = require("./config"); -var skins = require("./skins"); var fs = require("fs"); var session_url = "https://sessionserver.mojang.com/session/minecraft/profile/"; var skins_url = "https://skins.minecraft.net/MinecraftSkins/"; var capes_url = "https://skins.minecraft.net/MinecraftCloaks/"; -// exracts the skin url of a +profile+ object -// returns null when no url found (user has no skin) -function extract_skin_url(profile) { - var url = null; - if (profile && profile.properties) { - profile.properties.forEach(function(prop) { - if (prop.name == "textures") { - var json = Buffer(prop.value, "base64").toString(); - var props = JSON.parse(json); - url = props && props.textures && props.textures.SKIN && props.textures.SKIN.url || null; - } - }); - } - return url; -} - -function extract_cape_url(profile) { - var url = null; - if (profile && profile.properties) { - profile.properties.forEach(function(prop) { - if (prop.name == "textures") { - var json = Buffer(prop.value, "base64").toString(); - var props = JSON.parse(json); - url = props && props.textures && props.textures.CAPE && props.textures.CAPE.url || null; - } - }); - } - return url; -} - -// make a request to skins.miencraft.net -// the skin url is taken from the HTTP redirect -var get_username_url = function(name, callback) { - request.get({ - url: skins_url + name + ".png", - headers: { - "User-Agent": "https://crafatar.com" - }, - timeout: config.http_timeout, - followRedirect: false - }, function(error, response, body) { - if (!error && response.statusCode == 301) { - // skin_url received successfully - logging.log(name + " skin url received"); - callback(null, response.headers.location); - } else if (error) { - callback(error, null); - } else if (response.statusCode == 404) { - // skin (or user) doesn't exist - logging.log(name + " has no skin"); - callback(null, null); - } else if (response.statusCode == 429) { - // Too Many Requests - // Never got this, seems like skins aren't limited - logging.warn(name + body || "Too many requests"); - callback(null, null); - } else { - logging.error(name + " Unknown error:"); - logging.error(name + " " + response); - callback(body || "Unknown error", null); - } - }); -}; - -// make a request to sessionserver -// the skin_url is taken from the profile -var get_uuid_url = function(uuid, callback) { - request.get({ - url: session_url + uuid, - headers: { - "User-Agent": "https://crafatar.com" - }, - timeout: config.http_timeout // ms - }, function (error, response, body) { - if (!error && response.statusCode == 200) { - // profile downloaded successfully - logging.log(uuid + " profile downloaded"); - callback(null, extract_skin_url(JSON.parse(body))); - } else if (error) { - callback(error, null); - } else if (response.statusCode == 204 || response.statusCode == 404) { - // we get 204 No Content when UUID doesn't exist (including 404 in case they change that) - logging.log(uuid + " uuid does not exist"); - callback(null, null); - } else if (response.statusCode == 429) { - // Too Many Requests - callback(body || "Too many requests", null); - } else { - logging.error(uuid + " Unknown error:"); - logging.error(uuid + " " + response); - callback(body || "Unknown error", null); - } - }); -}; - var exp = {}; -// download skin_url for +uuid+ (name or uuid) -// callback contains error, skin_url -exp.get_skin_url = function(uuid, callback) { - if (uuid.length <= 16) { - get_username_url(uuid, function(err, url) { - callback(err, url); - }); - } else { - get_uuid_url(uuid, function(err, url) { - callback(err, url); +function extract_url(profile, property) { + var url = null; + if (profile && profile.properties) { + profile.properties.forEach(function(prop) { + if (prop.name === "textures") { + var json = new Buffer(prop.value, "base64").toString(); + var props = JSON.parse(json); + url = props && props.textures && props.textures[property] && props.textures[property].url || null; + } }); } + return url; }; -exp.get_cape_url = function(uuid, callback) { - if (uuid.length <= 16) { - get_username_url(uuid, function(err, url) { - callback(err, url); - }); - } else { - get_uuid_url(uuid, function(err, url) { - callback(err, url); - }); - } +// exracts the skin url of a +profile+ object +// returns null when no url found (user has no skin) +exp.extract_skin_url = function(profile) { + return extract_url(profile, 'SKIN'); }; -// downloads skin file from +url+ -// callback contains error, image -exp.get_skin = function(url, uuid, callback) { +// exracts the cape url of a +profile+ object +// returns null when no url found (user has no cape) +exp.extract_cape_url = function(profile) { + return extract_url(profile, 'CAPE'); +}; + +// makes a GET request to the +url+ +// +options+ hash includes various options for +// encoding and timeouts, defaults are already +// specified. +callback+ contains the body, response, +// and error buffer. get_from helper method is available +exp.get_from_options = function(url, options, callback) { request.get({ url: url, headers: { "User-Agent": "https://crafatar.com" }, - encoding: null, // encoding must be null so we get a buffer - timeout: config.http_timeout // ms - }, function (error, response, body) { - if (!error && response.statusCode == 200) { - // skin downloaded successfully - logging.log(uuid + " downloaded skin"); - logging.debug(uuid + " " + url); - callback(null, body); + timeout: (options.timeout || config.http_timeout), + encoding: (options.encoding || null), + followRedirect: (options.folow_redirect || false), + maxAttempts: 2, + retryDelay: 2000, + retryStrategy: request.RetryStrategies.NetworkError + }, function(error, response, body) { + if (!error && (response.statusCode === 200 || response.statusCode === 301)) { + // skin_url received successfully + logging.log(url + " url received"); + callback(body, response, null); + } else if (error) { + callback(body || null, response, error); + } else if (response.statusCode === 404) { + // page doesn't exist + logging.log(url + " url does not exist"); + callback(null, response, null); + } else if (response.statusCode === 429) { + // Too Many Requests exception - code 429 + logging.warn(body || "Too many requests"); + callback(body || null, response, error); } else { - if (error) { - logging.error(uuid + " error downloading '" + url + "': " + error); - } else if (response.statusCode == 404) { - logging.warn(uuid + " texture not found (404): " + url); - } else if (response.statusCode == 429) { - // Too Many Requests - // Never got this, seems like textures aren't limited - logging.warn(uuid + " too many requests for " + url); - logging.warn(uuid + " " + body); - } else { - logging.error(uuid + " unknown error for " + url); - logging.error(uuid + " " + response); - logging.error(uuid + " " + body); - error = "unknown error"; // Error needs to be set, otherwise null in callback - } - callback(error, null); + logging.error(url + " Unknown error:"); + //logging.error(response); + callback(body || null, response, error); } }); }; -exp.save_skin = function(uuid, hash, outpath, callback) { +// helper method for get_from_options, no options required +exp.get_from = function(url, callback) { + exp.get_from_options(url, {}, function(body, response, err) { + callback(body, response, err); + }); +}; + +// specifies which numbers identify what url +var mojang_url_types = { + 1: skins_url, + 2: capes_url +}; + +// make a request to skins.miencraft.net +// the skin url is taken from the HTTP redirect +// type reference is above +exp.get_username_url = function(name, type, callback) { + exp.get_from(mojang_url_types[type] + name + ".png", function(body, response, err) { + if (!err) { + callback(err, response ? (response.statusCode === 404 ? null : response.headers.location) : null); + } else { + callback(err, null); + } + }); +}; + +// gets the URL for a skin/cape from the profile +// +type+ specifies which to retrieve +exp.get_uuid_url = function(profile, type, callback) { + var url = null; + if (type === 1) { + url = exp.extract_skin_url(profile); + } else if (type === 2) { + url = exp.extract_cape_url(profile); + } + callback(url || null); +}; + +// make a request to sessionserver +// profile is returned as json +exp.get_profile = function(uuid, callback) { + if (!uuid) { + callback(null, null); + } else { + exp.get_from_options(session_url + uuid, {encoding: "utf8"} ,function(body, response, err) { + callback(err !== null ? err : null, (body !== null ? JSON.parse(body) : null)); + }); + } +}; + +// todo remove middleman + +// +uuid+ is likely a username and if so +// +uuid+ is used to get the url, otherwise +// +profile+ will be used to get the url +exp.get_skin_url = function(uuid, profile, callback) { + getUrl(uuid, profile, 1, function(url) { + callback(url); + }); +}; + +// +uuid+ is likely a username and if so +// +uuid+ is used to get the url, otherwise +// +profile+ will be used to get the url +exp.get_cape_url = function(uuid, profile, callback) { + getUrl(uuid, profile, 2, function(url) { + callback(url); + }); +}; + +function getUrl(uuid, profile, type, callback) { + if (uuid.length <= 16) { + //username + exp.get_username_url(uuid, type, function(err, url) { + callback(url || null); + }); + } else { + exp.get_uuid_url(profile, type, function(url) { + callback(url || null); + }); + } +} + +// downloads skin file from +url+ +// callback contains error, image +exp.get_skin = function(url, callback) { + exp.get_from(url, function(body, response, err) { + callback(body, err); + }); +}; + +exp.save_texture = function(uuid, hash, outpath, callback) { if (hash) { - var skinurl = "http://textures.minecraft.net/texture/" + hash; - exp.get_skin(skinurl, uuid, function(err, img) { + var textureurl = "http://textures.minecraft.net/texture/" + hash; + exp.get_from(textureurl, function(img, response, err) { if (err) { - logging.error(uuid + " error while downloading skin"); - callback(err, null); + logging.error(uuid + "error while downloading texture"); + callback(err, response, null); } else { - fs.writeFile(outpath, img, "binary", function(err){ + fs.writeFile(outpath, img, "binary", function(err) { if (err) { logging.log(uuid + " error: " + err); } - callback(null, img); + callback(err, response, img); }); } }); } else { - callback(null, null); + callback(null, null, null); } }; exp.get_cape = function(url, callback) { - request.get({ - url: url, - headers: { - "User-Agent": "https://crafatar.com" - }, - encoding: null, // encoding must be null so we get a buffer - timeout: config.http_timeout // ms - }, function (error, response, body) { - if (!error && response.statusCode == 200) { - // cape downloaded successfully - logging.log("downloaded cape"); - logging.debug(url); - callback(null, body); - } else { - if (error) { - logging.error("Error downloading '" + url + "': " + error); - } else if (response.statusCode == 404) { - logging.warn("texture not found (404): " + url); - } else if (response.statusCode == 429) { - logging.warn("too many requests for " + url); - logging.warn(body); - } else { - logging.error("unknown error for " + url); - logging.error(response); - logging.error(body); - error = "unknown error"; // Error needs to be set, otherwise null in callback - } - callback(error, null); - } + exp.get_from(url, function(body, response, err) { + callback(err, body); }); }; diff --git a/modules/renders.js b/modules/renders.js index 4df51c7..5d922e6 100644 --- a/modules/renders.js +++ b/modules/renders.js @@ -140,7 +140,7 @@ exp.draw_model = function(uuid, img, scale, helm, body, callback) { image.onload = function() { var width = 64 * scale; - var original_height = (image.height == 32 ? 32 : 64); + var original_height = (image.height === 32 ? 32 : 64); var height = original_height * scale; var model_canvas = new Canvas(20 * scale, (body ? 44.8 : 17.6) * scale); var skin_canvas = new Canvas(width, height); @@ -199,4 +199,4 @@ function scale_image(imageData, context, d_x, d_y, scale) { } } -module.exports = exp; \ No newline at end of file +module.exports = exp; diff --git a/modules/skins.js b/modules/skins.js index 669d9cf..664e7e7 100644 --- a/modules/skins.js +++ b/modules/skins.js @@ -104,4 +104,21 @@ exp.open_skin = function(uuid, skinpath, callback) { }); }; +exp.save_image = function(buffer, outpath, callback) { + lwip.open(buffer, "png", function(err, image) { + if (err) { + callback(err); + } else { + image.batch() + .writeFile(outpath, function(err) { + if (err) { + callback(err); + } else { + callback(null); + } + }); + } + }); +}; + module.exports = exp; \ No newline at end of file diff --git a/package.json b/package.json index af3969f..1eb1102 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "jade": "~1.8.2", "lwip": "0.0.6", "redis": "0.12.1", - "request": "2.51.0", + "requestretry": "1.2.2", "node-df": "0.1.1", "mime": "1.2.11" }, diff --git a/public/stylesheets/style.css b/public/stylesheets/style.css index b5f657d..03b1d03 100644 --- a/public/stylesheets/style.css +++ b/public/stylesheets/style.css @@ -195,6 +195,13 @@ h4 { background-image: url("/skins/0?default=alex"); } +#cape-example-1:hover .preview { + background-image: url("/capes/Dinnerbone"); +} +#cape-example-2:hover .preview { + background-image: url("/capes/md_5"); +} + img.preload { /* preload hover images @@ -284,4 +291,4 @@ img.preload { .avatar.flipped { -webkit-transform: rotate(180deg); transform: rotate(180deg); -} \ No newline at end of file +} diff --git a/routes/avatars.js b/routes/avatars.js index ec1a3ca..50619ad 100644 --- a/routes/avatars.js +++ b/routes/avatars.js @@ -54,7 +54,7 @@ module.exports = function(req, res) { } } etag = image && hash && hash.substr(0, 32) || "none"; - var matches = req.headers["if-none-match"] == '"' + etag + '"'; + var matches = req.headers["if-none-match"] === '"' + etag + '"'; if (image) { var http_status = 200; if (matches) { @@ -103,6 +103,6 @@ module.exports = function(req, res) { "Access-Control-Allow-Origin": "*", "Etag": '"' + etag + '"' }); - res.end(http_status == 304 ? null : image); + res.end(http_status === 304 ? null : image); } -}; \ No newline at end of file +}; diff --git a/routes/capes.js b/routes/capes.js index 80e47d5..efbb8fd 100644 --- a/routes/capes.js +++ b/routes/capes.js @@ -1,18 +1,27 @@ var logging = require("../modules/logging"); var helpers = require("../modules/helpers"); var config = require("../modules/config"); -var router = require("express").Router(); -var lwip = require("lwip"); -/* GET skin request. */ -router.get("/:uuid.:ext?", function (req, res) { - var uuid = (req.params.uuid || ""); - var def = req.query.default; +var human_status = { + 0: "none", + 1: "cached", + 2: "downloaded", + 3: "checked", + "-1": "error" +}; + +// GET cape request +module.exports = function(req, res) { var start = new Date(); + var uuid = (req.url.pathname.split("/")[2] || "").split(".")[0]; var etag = null; if (!helpers.uuid_valid(uuid)) { - res.status(422).send("422 Invalid UUID"); + res.writeHead(422, { + "Content-Type": "text/plain", + "Response-Time": new Date() - start + }); + res.end("Invalid ID"); return; } @@ -20,13 +29,13 @@ router.get("/:uuid.:ext?", function (req, res) { uuid = uuid.replace(/-/g, ""); try { - helpers.get_cape(uuid, function (err, hash, image) { - logging.log(uuid); + helpers.get_cape(uuid, function(err, status, image, hash) { + logging.log(uuid + " - " + human_status[status]); if (err) { - logging.error(err); + logging.error(uuid + " " + err); } etag = hash && hash.substr(0, 32) || "none"; - var matches = req.get("If-None-Match") == "\"" + etag + "\""; + var matches = req.headers["if-none-match"] === '"' + etag + '"'; if (image) { var http_status = 200; if (matches) { @@ -34,33 +43,37 @@ router.get("/:uuid.:ext?", function (req, res) { } else if (err) { http_status = 503; } - logging.debug("Etag: " + req.get("If-None-Match")); + logging.debug("Etag: " + req.headers["if-none-match"]); logging.debug("matches: " + matches); logging.log("status: " + http_status); - sendimage(http_status, image); + sendimage(http_status, status, image); } else { - res.status(404).send("404 not found"); + res.writeHead(404, { + "Content-Type": "text/plain", + "Response-Time": new Date() - start + }); + res.end("404 not found"); } }); - } catch (e) { - logging.error("Error!"); + } catch(e) { + logging.error(uuid + " error:"); logging.error(e); - res.status(500).send("500 error while retrieving cape"); + res.writeHead(500, { + "Content-Type": "text/plain", + "Response-Time": new Date() - start + }); + res.end("500 server error"); } - - function sendimage(http_status, image) { + function sendimage(http_status, img_status, image) { res.writeHead(http_status, { "Content-Type": "image/png", "Cache-Control": "max-age=" + config.browser_cache_time + ", public", "Response-Time": new Date() - start, - "X-Storage-Type": "downloaded", + "X-Storage-Type": human_status[img_status], "Access-Control-Allow-Origin": "*", - "Etag": "\"" + etag + "\"" + "Etag": '"' + etag + '"' }); - res.end(http_status == 304 ? null : image); + res.end(http_status === 304 ? null : image); } -}); - - -module.exports = router; +}; diff --git a/routes/renders.js b/routes/renders.js index 1a1b4ff..7a1ae9d 100644 --- a/routes/renders.js +++ b/routes/renders.js @@ -23,7 +23,7 @@ module.exports = function(req, res) { var raw_type = (req.url.path_list[2] || ""); // validate type - if (raw_type != "body" && raw_type != "head") { + if (raw_type !== "body" && raw_type !== "head") { res.writeHead(422, { "Content-Type": "text/plain", "Response-Time": new Date() - start @@ -32,7 +32,7 @@ module.exports = function(req, res) { return; } - var body = raw_type == "body"; + var body = raw_type === "body"; var uuid = (req.url.path_list[3] || "").split(".")[0]; var def = req.url.query.default; var scale = parseInt(req.url.query.scale) || config.default_scale; @@ -65,7 +65,7 @@ module.exports = function(req, res) { logging.error(uuid + " " + err); } etag = hash && hash.substr(0, 32) || "none"; - var matches = req.headers["if-none-match"] == '"' + etag + '"'; + var matches = req.headers["if-none-match"] === '"' + etag + '"'; if (image) { var http_status = 200; if (matches) { @@ -128,6 +128,6 @@ module.exports = function(req, res) { "Access-Control-Allow-Origin": "*", "Etag": '"' + etag + '"' }); - res.end(http_status == 304 ? null : image); + res.end(http_status === 304 ? null : image); } }; \ No newline at end of file diff --git a/routes/skins.js b/routes/skins.js index bcff7e2..2f2e638 100644 --- a/routes/skins.js +++ b/routes/skins.js @@ -31,7 +31,7 @@ module.exports = function(req, res) { logging.error(uuid + " " + err); } etag = hash && hash.substr(0, 32) || "none"; - var matches = req.headers["if-none-match"] == '"' + etag + '"'; + var matches = req.headers["if-none-match"] === '"' + etag + '"'; if (image) { var http_status = 200; if (matches) { @@ -82,6 +82,6 @@ module.exports = function(req, res) { "Access-Control-Allow-Origin": "*", "Etag": '"' + etag + '"' }); - res.end(http_status == 304 ? null : image); + res.end(http_status === 304 ? null : image); } }; \ No newline at end of file diff --git a/server.js b/server.js index fdc8f00..5c9db96 100644 --- a/server.js +++ b/server.js @@ -5,7 +5,6 @@ var config = require("./modules/config"); var clean = require("./modules/cleaner"); var http = require("http"); var mime = require("mime"); -var path = require("path"); var url = require("url"); var fs = require("fs"); @@ -13,7 +12,8 @@ var routes = { index: require("./routes/index"), avatars: require("./routes/avatars"), skins: require("./routes/skins"), - renders: require("./routes/renders") + renders: require("./routes/renders"), + capes: require("./routes/capes") }; function asset_request(req, res) { @@ -37,7 +37,7 @@ function requestHandler(req, res) { request.url.query = request.url.query || {}; // remove trailing and double slashes + other junk - var path_list = path.resolve(request.url.pathname).split("/"); + var path_list = request.url.pathname.split("/"); for (var i = 0; i < path_list.length; i++) { // URL decode path_list[i] = querystring.unescape(path_list[i]); @@ -61,6 +61,9 @@ function requestHandler(req, res) { case "renders": routes.renders(request, res); break; + case "capes": + routes.capes(request, res); + break; default: asset_request(request, res); } diff --git a/skins/capes/3f688e0e699b3d9fe448b5bb50a3a288f9c589762b3dae8308842122dcb81.png b/skins/capes/3f688e0e699b3d9fe448b5bb50a3a288f9c589762b3dae8308842122dcb81.png deleted file mode 100644 index a6b7b0b..0000000 Binary files a/skins/capes/3f688e0e699b3d9fe448b5bb50a3a288f9c589762b3dae8308842122dcb81.png and /dev/null differ diff --git a/skins/capes/95a2d2d94942966f743b84e4c262631978253979db673c2fbcc27dc3d2dcc7a7.png b/skins/capes/95a2d2d94942966f743b84e4c262631978253979db673c2fbcc27dc3d2dcc7a7.png deleted file mode 100644 index 8933fdd..0000000 Binary files a/skins/capes/95a2d2d94942966f743b84e4c262631978253979db673c2fbcc27dc3d2dcc7a7.png and /dev/null differ diff --git a/test/bulk.sh b/test/bulk.sh index bbbac09..0076254 100755 --- a/test/bulk.sh +++ b/test/bulk.sh @@ -14,4 +14,8 @@ for uuid in `cat "$dir/uuids.txt"`; do helm="&helm" fi curl -sSL -o /dev/null -w "%{url_effective} %{http_code} %{time_total}s\\n" "http://$host/avatars/$uuid?size=$size$helm" -done \ No newline at end of file +<<<<<<< HEAD +done +======= +done +>>>>>>> Network rewrite/major cleanup, major caching changes, etc diff --git a/test/test.js b/test/test.js index 5a69fe3..8268b96 100644 --- a/test/test.js +++ b/test/test.js @@ -8,6 +8,7 @@ var config = require("../modules/config"); var skins = require("../modules/skins"); var cache = require("../modules/cache"); var renders = require("../modules/renders"); +var cleaner = require("../modules/cleaner") // we don't want tests to fail because of slow internet config.http_timeout *= 3; @@ -22,6 +23,10 @@ var names = fs.readFileSync("test/usernames.txt").toString().split(/\r?\n/); var uuid = uuids[Math.round(Math.random() * (uuids.length - 1))]; var name = names[Math.round(Math.random() * (names.length - 1))]; +function getRandomInt(min, max) { + return Math.floor(Math.random() * (max - min + 1)) + min; +} + var ids = [ uuid.toLowerCase(), uuid.toUpperCase(), @@ -35,6 +40,7 @@ describe("Crafatar", function() { before(function() { cache.get_redis().flushall(); + cleaner.run(); }); describe("UUID/username", function() { @@ -79,13 +85,14 @@ describe("Crafatar", function() { done(); }); it("should not exist (uuid)", function(done) { - networking.get_skin_url("00000000000000000000000000000000", function(err, profile) { - assert.strictEqual(err, null); + var number = getRandomInt(0, 9).toString(); + networking.get_profile(Array(33).join(number), function(err, profile) { + assert.strictEqual(profile, null); done(); }); }); it("should not exist (username)", function(done) { - networking.get_skin_url("Steve", function(err, profile) { + networking.get_username_url("Steve", 1, function(err, profile) { assert.strictEqual(err, null); done(); }); @@ -99,10 +106,13 @@ describe("Crafatar", function() { var steven_uuid = "b8ffc3d37dbf48278f69475f6690aabd"; it("uuid's account should exist, but skin should not", function(done) { - helpers.get_avatar(alex_uuid, false, 160, function(err, status, image) { - assert.strictEqual(status, 2); - done(); - }); + networking.get_profile(alex_uuid, function(err, profile) { + assert.notStrictEqual(profile, null); + networking.get_uuid_url(profile, 1, function(url) { + assert.strictEqual(url, null); + done(); + }); + }) }); it("odd UUID should default to Alex", function(done) { assert.strictEqual(skins.default_skin(alex_uuid), "alex"); @@ -117,7 +127,7 @@ describe("Crafatar", function() { it("should time out on uuid info download", function(done) { var original_timeout = config.http_timeout; config.http_timeout = 1; - networking.get_skin_url("069a79f444e94726a5befca90e38aaf5", function(err, skin_url) { + networking.get_profile("069a79f444e94726a5befca90e38aaf5", function(err, profile) { assert.strictEqual(err.code, "ETIMEDOUT"); config.http_timeout = original_timeout; done(); @@ -126,7 +136,7 @@ describe("Crafatar", function() { it("should time out on username info download", function(done) { var original_timeout = config.http_timeout; config.http_timeout = 1; - networking.get_skin_url("redstone_sheep", function(err, skin_url) { + networking.get_username_url("redstone_sheep", 1, function(err, url) { assert.strictEqual(err.code, "ETIMEDOUT"); config.http_timeout = original_timeout; done(); @@ -135,7 +145,11 @@ describe("Crafatar", function() { it("should time out on skin download", function(done) { var original_timeout = config.http_timeout; config.http_timeout = 1; +<<<<<<< HEAD networking.get_skin("http://textures.minecraft.net/texture/477be35554684c28bdeee4cf11c591d3c88afb77e0b98da893fd7bc318c65184", uuid, function(err, img) { +======= + networking.get_from("http://textures.minecraft.net/texture/477be35554684c28bdeee4cf11c591d3c88afb77e0b98da893fd7bc318c65184", function(img, response, err) { +>>>>>>> Network rewrite/major cleanup, major caching changes, etc assert.strictEqual(err.code, "ETIMEDOUT"); config.http_timeout = original_timeout; done(); @@ -143,7 +157,11 @@ describe("Crafatar", function() { }); it("should not find the skin", function(done) { assert.doesNotThrow(function() { +<<<<<<< HEAD networking.get_skin("http://textures.minecraft.net/texture/this-does-not-exist", uuid, function(err, img) { +======= + networking.get_from("http://textures.minecraft.net/texture/this-does-not-exist", function(img, response, err) { +>>>>>>> Network rewrite/major cleanup, major caching changes, etc assert.strictEqual(err, null); // no error here, but it shouldn't throw exceptions done(); }); @@ -157,6 +175,32 @@ describe("Crafatar", function() { }); }); + // we have to make sure that we test both a 32x64 and 64x64 skin + describe("Networking: Render", function() { + it("should not fail (username, 32x64 skin)", function(done) { + helpers.get_render("md_5", 6, true, true, function(err, hash, img) { + assert.strictEqual(err, null); + done(); + }); + }); + it("should not fail (username, 64x64 skin)", function(done) { + helpers.get_render("Jake0oo0", 6, true, true, function(err, hash, img) { + assert.strictEqual(err, null); + done(); + }); + }); + }); + + describe("Networking: Cape", function() { + it("should not fail (guaranteed cape)", function(done) { + helpers.get_cape("Dinnerbone", function(err, hash, img) { + assert.strictEqual(err, null); + done(); + }); + }); + }); + + // DRY with uuid and username tests for (var i in ids) { var id = ids[i]; @@ -177,6 +221,7 @@ describe("Crafatar", function() { }); it("should be cached", function(done) { helpers.get_avatar(id, false, 160, function(err, status, image) { + console.log("STATUS: " + status) assert.strictEqual(status === 0 || status === 1, true); done(); }); @@ -206,17 +251,23 @@ describe("Crafatar", function() { }); describe("Networking: Render", function() { - it("should not fail (username, 64x64 skin)", function(done) { - helpers.get_render("Jake0oo0", 6, true, true, function(err, hash, img) { + it("should not fail (full body)", function(done) { + helpers.get_render(id, 6, true, true, function(err, hash, img) { + assert.strictEqual(err, null); + done(); + }); + }); + it("should not fail (only head)", function(done) { + helpers.get_render(id, 6, true, false, function(err, hash, img) { assert.strictEqual(err, null); done(); }); }); }); - describe("Networking: Render", function() { - it("should not fail (username, 32x64 skin)", function(done) { - helpers.get_render("md_5", 6, true, true, function(err, hash, img) { + describe("Networking: Cape", function() { + it("should not fail (possible cape)", function(done) { + helpers.get_cape(id, function(err, hash, img) { assert.strictEqual(err, null); done(); }); @@ -231,8 +282,9 @@ describe("Crafatar", function() { if (id_type == "uuid") { it("uuid should be rate limited", function(done) { - helpers.get_avatar(id, false, 160, function(err, status, image) { - assert.strictEqual(JSON.parse(err).error, "TooManyRequestsException"); + networking.get_profile(id, function(err, profile) { + console.log("THIS THING:: " + err) + assert.strictEqual(profile.error, "TooManyRequestsException"); done(); }); }); diff --git a/views/index.jade b/views/index.jade index b0baa02..e876773 100644 --- a/views/index.jade +++ b/views/index.jade @@ -220,6 +220,34 @@ block content | Hover over the examples for a preview! .preview-background + section + a(id="capes" class="anchor") + a(href="#capes") + h3 Capes + p + | A cape endpoint is also available to get the active cape of a user.
+ | Replace + mark.green id + | with a Mojang UUID or username to get the related cape.
+ | The user's cape is returned, otherwise a 404 is thrown.
+ .code + | #{domain}/skins/ + mark.green id + + section + a(id="cape-examples", class="anchor") + a(href="#cape-examples") + h4 Cape Examples + .code + #cape-example-1.example-wrapper + .example #{domain}/capes/Dinnerbone + p.preview Dinnerbone's Cape Mojang capes are not transparent... + #cape-example-2.example-wrapper + .example #{domain}/capes/md_5 + p.preview md_5's Cape + p.preview-placeholder + | Hover over the examples for a preview! + .preview-background section a(id="meta" class="anchor") @@ -343,4 +371,8 @@ block content img.preload(src="/avatars/020242a17b9441799eff511eea1221da?size=64&helm", alt="preloaded image") img.preload(src="/avatars/9769ecf6331448f3ace67ae06cec64a3?size=64&helm", alt="preloaded image") img.preload(src="/avatars/f8cdb6839e9043eea81939f85d9c5d69?size=64&helm", alt="preloaded image") +<<<<<<< HEAD img.preload(src="/skins/jeb_", alt="preloaded image") +======= + img.preload(src="/skins/jeb_", alt="preloaded image") +>>>>>>> Network rewrite/major cleanup, major caching changes, etc