From e6481e3c73af3640b97f66204cafe59c0ebd3d16 Mon Sep 17 00:00:00 2001 From: jomo Date: Sun, 23 Nov 2014 21:53:42 +0100 Subject: [PATCH 01/17] bug fixes, improvements, fix #13, fix #14 --- modules/config.js | 4 +-- modules/helpers.js | 62 +++++++++++----------------------- modules/networking.js | 78 ++++++++++++++++++++++++++++++++++++++----- routes/avatars.js | 4 +-- test/test.js | 34 ++++++++++++------- 5 files changed, 114 insertions(+), 68 deletions(-) diff --git a/modules/config.js b/modules/config.js index 9b15725..e5501f7 100644 --- a/modules/config.js +++ b/modules/config.js @@ -4,9 +4,9 @@ var config = { default_size: 160, // size to be used when no size given local_cache_time: 3600, // 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 - http_timeout: 1000, // ms until connection to mojang is dropped + http_timeout: 3000, // ms until connection to mojang is dropped faces_dir: 'skins/faces/', // directory where faces are kept. should have trailing '/' helms_dir: 'skins/helms/' // directory where helms are kept. should have trailing '/' }; -module.exports = config; \ No newline at end of file +module.exports = config; diff --git a/modules/helpers.js b/modules/helpers.js index 2a2183a..7d2d70e 100644 --- a/modules/helpers.js +++ b/modules/helpers.js @@ -5,7 +5,7 @@ var skins = require('./skins'); // 0098cb60-fa8e-427c-b299-793cbd302c9a var valid_uuid = /^([0-9a-f-]{32,36}|[a-zA-Z0-9_]{1,16})$/; // uuid|username -var hash_pattern = /([^\/]+)(?=\.\w{0,16}$)|((?:[a-z][a-z]*[0-9]+[a-z0-9]*))/; +var hash_pattern = /[0-9a-f]+$/; function get_hash(url) { return hash_pattern.exec(url)[0].toLowerCase(); @@ -14,28 +14,15 @@ function get_hash(url) { // 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 profile for +uuid+ - networking.get_profile(uuid, function(err, profile) { - if (err === 0) { - // uuid does not exist - cache.save_hash(uuid, null); - callback(null, null); - } else if (err) { + // get skin_url for +uuid+ + networking.get_skin_url(uuid, function(err, skin_url) { + if (err) { callback(err, null); } else { - var skinurl = null; - - // Username handling - if (uuid.length <= 16) { - skinurl = "https://skins.minecraft.net/MinecraftSkins/" + uuid + ".png"; - console.log(uuid + " is a username"); - } else { - skinurl = skin_url(profile); - } - if (skinurl) { - console.log(uuid + " " + skinurl); + if (skin_url) { + console.log(uuid + " " + skin_url); // set file paths - var hash = get_hash(skinurl); + var hash = get_hash(skin_url); if (details && details.hash == hash) { // hash hasn't changed console.log(uuid + " hash has not changed"); @@ -47,7 +34,7 @@ function store_images(uuid, details, callback) { var facepath = __dirname + '/../' + config.faces_dir + hash + ".png"; var helmpath = __dirname + '/../' + config.helms_dir + hash + ".png"; // download skin, extract face/helm - networking.skin_file(skinurl, facepath, helmpath, function(err) { + networking.skin_file(skin_url, facepath, helmpath, function(err) { if (err) { callback(err, null); } else { @@ -65,30 +52,14 @@ function store_images(uuid, details, callback) { }); } -// exracts the skin url of a +profile+ object -// returns null when no url found (user has no skin) -function 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; -} - // 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 -// -1: error -// 0: cached as null -// 1: found on disk -// 2: profile requested/found, skin downloaded from mojang servers -// 3: profile requested/found, but it has not changed or no skin +// -1: "error" +// 0: "none" - cached as null +// 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 function get_image_hash(uuid, callback) { cache.get_details(uuid, function(err, details) { if (err) { @@ -100,12 +71,17 @@ function get_image_hash(uuid, callback) { callback(null, (details.hash ? 1 : 0), details.hash); } else { console.log(uuid + " uuid not known or too old"); + console.log("details:"); + console.log(details); + console.log("/details"); store_images(uuid, details, function(err, hash) { if (err) { callback(err, -1, details && details.hash); } else { console.log(uuid + " hash: " + hash); - callback(null, (hash != (details && details.hash) ? 2 : 3), hash); + var oldhash = details && details.hash; + var status = hash !== oldhash ? 2 : 3; + callback(null, status, hash); } }); } diff --git a/modules/networking.js b/modules/networking.js index 270c541..b6e488d 100644 --- a/modules/networking.js +++ b/modules/networking.js @@ -4,16 +4,60 @@ 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 exp = {}; - -// download the Mojang profile for +uuid+ -// callback contains error, profile object -exp.get_profile = function(uuid, callback) { - if (uuid.length <= 16) { - callback(null, null); - return; +// 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; +} + +// 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", + timeout: config.http_timeout, + followRedirect: false + }, function(error, response, body) { + if (!error && response.statusCode == 301) { + // skin_url received successfully + console.log(name + " skin url received"); + callback(null, response.headers.location); + } else if (error) { + callback(error, null); + } else if (response.statusCode == 404) { + // skin doesn't exist + console.log(name + " has no skin"); + callback(0, null); + } else if (response.statusCode == 429) { + // Too Many Requests + // Never got this, seems like skins aren't limited + console.warn(name + " Too many requests"); + console.warn(body); + callback(null, null); + } else { + console.error(name + " Unknown error:"); + console.error(response); + console.error(body); + callback(null, 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, timeout: config.http_timeout // ms @@ -21,7 +65,7 @@ exp.get_profile = function(uuid, callback) { if (!error && response.statusCode == 200) { // profile downloaded successfully console.log(uuid + " profile downloaded"); - callback(null, JSON.parse(body)); + callback(null, extract_skin_url(JSON.parse(body))); } else if (error) { callback(error, null); } else if (response.statusCode == 204 || response.statusCode == 404) { @@ -42,6 +86,22 @@ exp.get_profile = function(uuid, callback) { }); }; +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); + }); + } +}; + // downloads skin file from +url+ // stores face image as +facename+ // stores helm image as +helmname+ diff --git a/routes/avatars.js b/routes/avatars.js index 24d0bc3..888357f 100644 --- a/routes/avatars.js +++ b/routes/avatars.js @@ -6,8 +6,8 @@ var skins = require('../modules/skins'); var human_status = { 0: "none", 1: "cached", - 2: "checked", - 3: "downloaded", + 2: "downloaded", + 3: "checked", "-1": "error" }; diff --git a/test/test.js b/test/test.js index 949d1e7..032f42a 100644 --- a/test/test.js +++ b/test/test.js @@ -16,12 +16,12 @@ var usernames = fs.readFileSync('test/usernames.txt').toString().split("\n"); var uuid = uuids[Math.round(Math.random() * (uuids.length - 1))]; var username = usernames[Math.round(Math.random() * (usernames.length - 1))]; -describe('UUID/username', function() { +describe('Crafatar', function() { before(function() { cache.get_redis().flushall(); }); - describe('UUID', function() { + describe('UUID/username', function() { it("should be an invalid uuid", function(done) { assert.strictEqual(helpers.uuid_valid("g098cb60fa8e427cb299793cbd302c9a"), false); done(); @@ -58,43 +58,51 @@ describe('UUID/username', function() { assert.strictEqual(helpers.uuid_valid("a"), true); done(); }); - it("should not exist", function(done) { - networking.get_profile("00000000000000000000000000000000", function(err, profile) { + it("should not exist (uuid)", function(done) { + networking.get_skin_url("00000000000000000000000000000000", function(err, profile) { + assert.strictEqual(err, 0); + done(); + }); + }); + it("should not exist (username)", function(done) { + networking.get_skin_url("Steve", function(err, profile) { assert.strictEqual(err, 0); done(); }); }); }); - describe('Avatar', function() { + describe('Networking: Avatar', function() { it("should be downloaded (uuid)", function(done) { helpers.get_avatar(uuid, false, 160, function(err, status, image) { assert.strictEqual(status, 2); done(); }); }); - it("should be local (uuid)", function(done) { + it("should be cached (uuid)", function(done) { helpers.get_avatar(uuid, false, 160, function(err, status, image) { assert.strictEqual(status, 1); done(); }); }); + /* We can't test this because of mojang's rate limits :( it("should be checked (uuid)", function(done) { var original_cache_time = config.local_cache_time; config.local_cache_time = 0; helpers.get_avatar(uuid, false, 160, function(err, status, image) { - assert.strictEqual(status, 2); + assert.strictEqual(status, 3); config.local_cache_time = original_cache_time; done(); }); }); + */ it("should be downloaded (username)", function(done) { helpers.get_avatar(username, false, 160, function(err, status, image) { assert.strictEqual(status, 2); done(); }); }); - it("should be local (username)", function(done) { + it("should be cached (username)", function(done) { helpers.get_avatar(username, false, 160, function(err, status, image) { assert.strictEqual(status, 1); done(); @@ -104,7 +112,7 @@ describe('UUID/username', function() { var original_cache_time = config.local_cache_time; config.local_cache_time = 0; helpers.get_avatar(username, false, 160, function(err, status, image) { - assert.strictEqual(status, 2); + assert.strictEqual(status, 3); config.local_cache_time = original_cache_time; done(); }); @@ -137,18 +145,20 @@ describe('UUID/username', function() { }); }); it("should time out on profile download", function(done) { + var original_timeout = config.http_timeout; config.http_timeout = 1; - networking.get_profile("069a79f444e94726a5befca90e38aaf5", function(err, profile) { + networking.get_skin_url("069a79f444e94726a5befca90e38aaf5", function(err, profile) { assert.strictEqual(err.code, "ETIMEDOUT"); - config.http_timeout = 3000; + config.http_timeout = original_timeout; done(); }); }); it("should time out on skin download", function(done) { + var original_timeout = config.http_timeout; config.http_timeout = 1; networking.skin_file("http://textures.minecraft.net/texture/477be35554684c28bdeee4cf11c591d3c88afb77e0b98da893fd7bc318c65184", "face.png", "helm.png", function(err) { assert.strictEqual(err.code, "ETIMEDOUT"); - config.http_timeout = 3000; + config.http_timeout = original_timeout; done(); }); }); From 464c1eab98326f39b5747e13f3b702fac71ad145 Mon Sep 17 00:00:00 2001 From: jomo Date: Sun, 23 Nov 2014 21:56:46 +0100 Subject: [PATCH 02/17] add IRC info to readme --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 0c42981..c7fc96b 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,10 @@ Image manipulation is done by [lwip](https://github.com/EyalAr/lwip) See the [API Usage](https://crafatar.com) +## Contact + +You can [join us](https://webchat.esper.net/?channels=spongy) in #spongy on irc.esper.net. + ## Install * Clone the repository From 148ea4563108febe5856744a341d691ab9b09848 Mon Sep 17 00:00:00 2001 From: jomo Date: Sun, 23 Nov 2014 22:01:11 +0100 Subject: [PATCH 03/17] add test for timing out on username info (#14) --- test/test.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/test/test.js b/test/test.js index 032f42a..23dcfc8 100644 --- a/test/test.js +++ b/test/test.js @@ -144,10 +144,19 @@ describe('Crafatar', function() { done(); }); }); - it("should time out on profile download", function(done) { + 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, profile) { + networking.get_skin_url("069a79f444e94726a5befca90e38aaf5", function(err, skin_url) { + assert.strictEqual(err.code, "ETIMEDOUT"); + config.http_timeout = original_timeout; + done(); + }); + }); + 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) { assert.strictEqual(err.code, "ETIMEDOUT"); config.http_timeout = original_timeout; done(); From 06172a8aaf8ce32e194d27dd53d54f7b2881e7cb Mon Sep 17 00:00:00 2001 From: jomo Date: Sun, 23 Nov 2014 22:10:51 +0100 Subject: [PATCH 04/17] add test for textures 404 (#14) --- test/test.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/test.js b/test/test.js index 23dcfc8..e25ea07 100644 --- a/test/test.js +++ b/test/test.js @@ -171,5 +171,11 @@ describe('Crafatar', function() { done(); }); }); + it("should not find the skin", function(done) { + networking.skin_file("http://textures.minecraft.net/texture/this-does-not-exist", "face.png", "helm.png", function(err) { + assert.strictEqual(err, null); // no error here, but it shouldn't throw exceptions + done(); + }); + }); }); }); From 3b7acbd28d7bbaa09311d04ced7886a809f2ea77 Mon Sep 17 00:00:00 2001 From: jomo Date: Sun, 23 Nov 2014 22:17:40 +0100 Subject: [PATCH 05/17] add test for existing files / not re-downloading (#14) --- test/test.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/test.js b/test/test.js index e25ea07..ceac967 100644 --- a/test/test.js +++ b/test/test.js @@ -124,6 +124,16 @@ describe('Crafatar', function() { done(); }); }); + it("should already have the files / not download", function(done) { + fs.openSync("face.png", "w"); + fs.openSync("helm.png", "w"); + networking.skin_file("http://textures.minecraft.net/texture/477be35554684c28bdeee4cf11c591d3c88afb77e0b98da893fd7bc318c65184", "face.png", "helm.png", function(err) { + assert.strictEqual(err, null); // no error here, but it shouldn't throw exceptions + fs.unlinkSync("face.png"); + fs.unlinkSync("helm.png"); + done(); + }); + }); it("should default to Alex", function(done) { assert.strictEqual(skins.default_skin("ec561538f3fd461daff5086b22154bce"), "alex"); done(); From 8ee862596bebb11bf45f7085588a120dfbcb09c3 Mon Sep 17 00:00:00 2001 From: jomo Date: Sun, 23 Nov 2014 22:39:02 +0100 Subject: [PATCH 06/17] add test for invalid file updates (#14) --- test/test.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/test/test.js b/test/test.js index ceac967..41c85f6 100644 --- a/test/test.js +++ b/test/test.js @@ -144,7 +144,7 @@ describe('Crafatar', function() { }); }); - describe('Mojang Errors', function() { + describe('Errors', function() { before(function() { cache.get_redis().flushall(); }); @@ -187,5 +187,11 @@ describe('Crafatar', function() { done(); }); }); + it("should handle file updates on invalid files", function(done) { + assert.doesNotThrow(function() { + cache.update_timestamp("0123456789abcdef0123456789abcdef", "invalid-file.png"); + }); + done(); + }); }); }); From ea3f322c737201b6e856aacde2b5d706c8376d15 Mon Sep 17 00:00:00 2001 From: jomo Date: Sun, 23 Nov 2014 22:41:27 +0100 Subject: [PATCH 07/17] test improvements, fix #14 --- test/test.js | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/test/test.js b/test/test.js index 41c85f6..7fbc265 100644 --- a/test/test.js +++ b/test/test.js @@ -125,13 +125,15 @@ describe('Crafatar', function() { }); }); it("should already have the files / not download", function(done) { - fs.openSync("face.png", "w"); - fs.openSync("helm.png", "w"); - networking.skin_file("http://textures.minecraft.net/texture/477be35554684c28bdeee4cf11c591d3c88afb77e0b98da893fd7bc318c65184", "face.png", "helm.png", function(err) { - assert.strictEqual(err, null); // no error here, but it shouldn't throw exceptions - fs.unlinkSync("face.png"); - fs.unlinkSync("helm.png"); - done(); + assert.doesNotThrow(function() { + fs.openSync("face.png", "w"); + fs.openSync("helm.png", "w"); + networking.skin_file("http://textures.minecraft.net/texture/477be35554684c28bdeee4cf11c591d3c88afb77e0b98da893fd7bc318c65184", "face.png", "helm.png", function(err) { + assert.strictEqual(err, null); // no error here, but it shouldn't throw exceptions + fs.unlinkSync("face.png"); + fs.unlinkSync("helm.png"); + done(); + }); }); }); it("should default to Alex", function(done) { @@ -182,9 +184,11 @@ describe('Crafatar', function() { }); }); it("should not find the skin", function(done) { - networking.skin_file("http://textures.minecraft.net/texture/this-does-not-exist", "face.png", "helm.png", function(err) { - assert.strictEqual(err, null); // no error here, but it shouldn't throw exceptions - done(); + assert.doesNotThrow(function() { + networking.skin_file("http://textures.minecraft.net/texture/this-does-not-exist", "face.png", "helm.png", function(err) { + assert.strictEqual(err, null); // no error here, but it shouldn't throw exceptions + done(); + }); }); }); it("should handle file updates on invalid files", function(done) { From dd8a3b91e2e9315dbdedf206f6ad39a3aa35be0b Mon Sep 17 00:00:00 2001 From: jomo Date: Sun, 23 Nov 2014 22:56:47 +0100 Subject: [PATCH 08/17] using our own logging, allows us to do some eventual modifications --- modules/cache.js | 19 ++++++++-------- modules/config.js | 3 ++- modules/helpers.js | 21 +++++++++--------- modules/logging.js | 16 ++++++++++++++ modules/networking.js | 51 ++++++++++++++++++++++--------------------- 5 files changed, 65 insertions(+), 45 deletions(-) create mode 100644 modules/logging.js diff --git a/modules/cache.js b/modules/cache.js index af0ad70..ed1420d 100644 --- a/modules/cache.js +++ b/modules/cache.js @@ -1,3 +1,4 @@ +var logging = require('./logging'); var config = require("./config"); var redis = null; var fs = require("fs"); @@ -5,7 +6,7 @@ var fs = require("fs"); // sets up redis connection // flushes redis when running on heroku (files aren't kept between pushes) function connect_redis() { - console.log("connecting to redis..."); + logging.log("connecting to redis..."); if (process.env.REDISCLOUD_URL) { var redisURL = require("url").parse(process.env.REDISCLOUD_URL); redis = require("redis").createClient(redisURL.port, redisURL.hostname); @@ -14,17 +15,17 @@ function connect_redis() { redis = require("redis").createClient(); } redis.on("ready", function() { - console.log("Redis connection established."); + logging.log("Redis connection established."); if(process.env.HEROKU) { - console.log("Running on heroku, flushing redis"); + logging.log("Running on heroku, flushing redis"); redis.flushall(); } }); redis.on("error", function (err) { - console.error(err); + logging.error(err); }); redis.on("end", function () { - console.warn("Redis connection lost!"); + logging.warn("Redis connection lost!"); }); } @@ -38,11 +39,11 @@ function update_file_date(hash) { var date = new Date(); fs.utimes(path, date, date, function(err){ if (err) { - console.error(err); + logging.error(err); } }); } else { - console.error("Tried to update " + path + " date, but it doesn't exist"); + logging.error("Tried to update " + path + " date, but it doesn't exist"); } }); } @@ -56,7 +57,7 @@ exp.get_redis = function() { // sets the timestamp for +uuid+ and its face file's date to now exp.update_timestamp = function(uuid, hash) { - console.log(uuid + " cache: updating timestamp"); + logging.log(uuid + " cache: updating timestamp"); var time = new Date().getTime(); redis.hmset(uuid, "t", time); update_file_date(hash); @@ -64,7 +65,7 @@ exp.update_timestamp = function(uuid, hash) { // create the key +uuid+, store +hash+ and time exp.save_hash = function(uuid, hash) { - console.log(uuid + " cache: saving hash"); + logging.log(uuid + " cache: saving hash"); var time = new Date().getTime(); redis.hmset(uuid, "h", hash, "t", time); }; diff --git a/modules/config.js b/modules/config.js index e5501f7..0934c0d 100644 --- a/modules/config.js +++ b/modules/config.js @@ -6,7 +6,8 @@ var config = { browser_cache_time: 3600, // seconds until browser will request image again http_timeout: 3000, // ms until connection to mojang is dropped faces_dir: 'skins/faces/', // directory where faces are kept. should have trailing '/' - helms_dir: 'skins/helms/' // directory where helms are kept. should have trailing '/' + helms_dir: 'skins/helms/', // directory where helms are kept. should have trailing '/' + debug_enabled: true // enables logging.debug }; module.exports = config; diff --git a/modules/helpers.js b/modules/helpers.js index 7d2d70e..025927c 100644 --- a/modules/helpers.js +++ b/modules/helpers.js @@ -1,4 +1,5 @@ var networking = require('./networking'); +var logging = require('./logging'); var config = require('./config'); var cache = require('./cache'); var skins = require('./skins'); @@ -20,17 +21,17 @@ function store_images(uuid, details, callback) { callback(err, null); } else { if (skin_url) { - console.log(uuid + " " + 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 - console.log(uuid + " hash has not changed"); + logging.log(uuid + " hash has not changed"); cache.update_timestamp(uuid, hash); callback(null, hash); } else { // hash has changed - console.log(uuid + " new hash: " + hash); + logging.log(uuid + " new hash: " + hash); var facepath = __dirname + '/../' + config.faces_dir + hash + ".png"; var helmpath = __dirname + '/../' + config.helms_dir + hash + ".png"; // download skin, extract face/helm @@ -67,18 +68,18 @@ function get_image_hash(uuid, callback) { } else { if (details && details.time + config.local_cache_time * 1000 >= new Date().getTime()) { // uuid known + recently updated - console.log(uuid + " uuid known & recently updated"); + logging.log(uuid + " uuid known & recently updated"); callback(null, (details.hash ? 1 : 0), details.hash); } else { - console.log(uuid + " uuid not known or too old"); - console.log("details:"); - console.log(details); - console.log("/details"); + logging.log(uuid + " uuid not known or too old"); + logging.log("details:"); + logging.log(details); + logging.log("/details"); store_images(uuid, details, function(err, hash) { if (err) { callback(err, -1, details && details.hash); } else { - console.log(uuid + " hash: " + hash); + logging.log(uuid + " hash: " + hash); var oldhash = details && details.hash; var status = hash !== oldhash ? 2 : 3; callback(null, status, hash); @@ -102,7 +103,7 @@ exp.uuid_valid = function(uuid) { // 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) { - console.log("\nrequest: " + uuid); + logging.log("\nrequest: " + uuid); get_image_hash(uuid, function(err, status, hash) { if (hash) { var filepath = __dirname + '/../' + (helm ? config.helms_dir : config.faces_dir) + hash + ".png"; diff --git a/modules/logging.js b/modules/logging.js new file mode 100644 index 0000000..66f1f2f --- /dev/null +++ b/modules/logging.js @@ -0,0 +1,16 @@ +var config = require("./config"); + +var exp = {}; + +function debug() { + if (config.debug_enabled) { + console.log(Array.prototype.slice.call(arguments).join(" ")); + } +} + +exp.log = console.log; +exp.warn = console.warn; +exp.error = console.error; +exp.debug = debug; + +module.exports = exp; \ No newline at end of file diff --git a/modules/networking.js b/modules/networking.js index b6e488d..098d85e 100644 --- a/modules/networking.js +++ b/modules/networking.js @@ -1,3 +1,4 @@ +var logging = require('./logging'); var request = require('request'); var config = require('./config'); var skins = require('./skins'); @@ -32,24 +33,24 @@ var get_username_url = function(name, callback) { }, function(error, response, body) { if (!error && response.statusCode == 301) { // skin_url received successfully - console.log(name + " skin url received"); + logging.log(name + " skin url received"); callback(null, response.headers.location); } else if (error) { callback(error, null); } else if (response.statusCode == 404) { // skin doesn't exist - console.log(name + " has no skin"); + logging.log(name + " has no skin"); callback(0, null); } else if (response.statusCode == 429) { // Too Many Requests // Never got this, seems like skins aren't limited - console.warn(name + " Too many requests"); - console.warn(body); + logging.warn(name + " Too many requests"); + logging.warn(body); callback(null, null); } else { - console.error(name + " Unknown error:"); - console.error(response); - console.error(body); + logging.error(name + " Unknown error:"); + logging.error(response); + logging.error(body); callback(null, null); } }); @@ -64,23 +65,23 @@ var get_uuid_url = function(uuid, callback) { }, function (error, response, body) { if (!error && response.statusCode == 200) { // profile downloaded successfully - console.log(uuid + " profile downloaded"); + 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) - console.log(uuid + " uuid does not exist"); + logging.log(uuid + " uuid does not exist"); callback(0, null); } else if (response.statusCode == 429) { // Too Many Requests - console.warn(uuid + " Too many requests"); - console.warn(body); + logging.warn(uuid + " Too many requests"); + logging.warn(body); callback(null, null); } else { - console.error(uuid + " Unknown error:"); - console.error(response); - console.error(body); + logging.error(uuid + " Unknown error:"); + logging.error(response); + logging.error(body); callback(null, null); } }); @@ -108,7 +109,7 @@ exp.get_skin_url = function(uuid, callback) { // callback contains error exp.skin_file = function(url, facename, helmname, callback) { if (fs.existsSync(facename) && fs.existsSync(facename)) { - console.log("Images already exist, not downloading."); + logging.log("Images already exist, not downloading."); callback(null); return; } @@ -119,32 +120,32 @@ exp.skin_file = function(url, facename, helmname, callback) { }, function (error, response, body) { if (!error && response.statusCode == 200) { // skin downloaded successfully - console.log(url + " skin downloaded"); + logging.log(url + " skin downloaded"); skins.extract_face(body, facename, function(err) { if (err) { callback(err); } else { - console.log(facename + " face extracted"); + logging.log(facename + " face extracted"); skins.extract_helm(facename, body, helmname, function(err) { - console.log(helmname + " helm extracted."); + logging.log(helmname + " helm extracted."); callback(err); }); } }); } else { if (error) { - console.error("Error downloading '" + url + "': " + error); + logging.error("Error downloading '" + url + "': " + error); } else if (response.statusCode == 404) { - console.warn("texture not found (404): " + url); + logging.warn("texture not found (404): " + url); } else if (response.statusCode == 429) { // Too Many Requests // Never got this, seems like textures aren't limited - console.warn("too many requests for " + url); - console.warn(body); + logging.warn("too many requests for " + url); + logging.warn(body); } else { - console.error("unknown error for " + url); - console.error(response); - console.error(body); + 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); From bac6a0dadde718ef632f1c6f2b130a9ee2c756b9 Mon Sep 17 00:00:00 2001 From: jomo Date: Sun, 23 Nov 2014 23:05:43 +0100 Subject: [PATCH 09/17] less spammy tests :) --- test/test.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/test.js b/test/test.js index 7fbc265..0a8f7dc 100644 --- a/test/test.js +++ b/test/test.js @@ -3,6 +3,7 @@ var fs = require('fs'); var networking = require('../modules/networking'); var helpers = require('../modules/helpers'); +var logging = require('../modules/logging'); var config = require('../modules/config'); var skins = require('../modules/skins'); var cache = require("../modules/cache"); @@ -10,6 +11,9 @@ var cache = require("../modules/cache"); // we don't want tests to fail because of slow internet config.http_timeout = 3000; +// no spam +logging.log = function(){}; + var uuids = fs.readFileSync('test/uuids.txt').toString().split("\n"); var usernames = fs.readFileSync('test/usernames.txt').toString().split("\n"); // Get a random UUID + username in order to prevent rate limiting From 796f410248eded615ac14bb3fabeb8c78f59214a Mon Sep 17 00:00:00 2001 From: jomo Date: Sun, 23 Nov 2014 23:13:25 +0100 Subject: [PATCH 10/17] allow custom config --- .gitignore | 1 + .travis.yml | 5 +++-- Procfile | 2 +- README.md | 1 + modules/config.example.js | 13 +++++++++++++ modules/config.js | 4 ++-- 6 files changed, 21 insertions(+), 5 deletions(-) create mode 100644 modules/config.example.js diff --git a/.gitignore b/.gitignore index aed9c83..4e398b0 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ node_modules/ .DS_Store *.rdb coverage/ +modules/config.js diff --git a/.travis.yml b/.travis.yml index 5a0bfe9..58e6785 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,12 @@ language: node_js node_js: - "0.10" +before_script: + - cp "modules/config.example.js" "modules/config.js" notifications: irc: channels: - "irc.esper.net#spongy" skip_join: true services: - - redis-server -skip_join: true \ No newline at end of file + - redis-server \ No newline at end of file diff --git a/Procfile b/Procfile index e8f79ea..7778d31 100644 --- a/Procfile +++ b/Procfile @@ -1 +1 @@ -web: npm start \ No newline at end of file +web: cp "modules/config.example.js" "modules/config.js" && npm start \ No newline at end of file diff --git a/README.md b/README.md index c7fc96b..1001ab0 100644 --- a/README.md +++ b/README.md @@ -20,5 +20,6 @@ You can [join us](https://webchat.esper.net/?channels=spongy) in #spongy on irc. * Clone the repository * `npm install` * `redis-server` +* `cp "modules/config.example.js" "modules/config.js"` * `npm start` * Access [http://localhost:3000](http://localhost:3000) \ No newline at end of file diff --git a/modules/config.example.js b/modules/config.example.js new file mode 100644 index 0000000..52698ad --- /dev/null +++ b/modules/config.example.js @@ -0,0 +1,13 @@ +var config = { + min_size: 1, // < 1 will (obviously) cause crash + max_size: 512, // too big values might lead to slow response time or DoS + default_size: 160, // size to be used when no size given + local_cache_time: 3600, // 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 + http_timeout: 1000, // ms until connection to mojang is dropped + faces_dir: 'skins/faces/', // directory where faces are kept. should have trailing '/' + helms_dir: 'skins/helms/', // directory where helms are kept. should have trailing '/' + debug_enabled: false // enables logging.debug +}; + +module.exports = config; \ No newline at end of file diff --git a/modules/config.js b/modules/config.js index 0934c0d..4b77193 100644 --- a/modules/config.js +++ b/modules/config.js @@ -7,7 +7,7 @@ var config = { http_timeout: 3000, // ms until connection to mojang is dropped faces_dir: 'skins/faces/', // directory where faces are kept. should have trailing '/' helms_dir: 'skins/helms/', // directory where helms are kept. should have trailing '/' - debug_enabled: true // enables logging.debug + debug_enabled: true // enables logging.debug }; -module.exports = config; +module.exports = config; \ No newline at end of file From 55ed86a3f6fdc21fbc7ee7e2c8c0fa5ae7381dd3 Mon Sep 17 00:00:00 2001 From: jomo Date: Mon, 24 Nov 2014 00:53:39 +0100 Subject: [PATCH 11/17] skin support, fix #15 --- app.js | 2 +- modules/helpers.js | 26 ++++++++++++------------ routes/avatars.js | 50 +++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 59 insertions(+), 19 deletions(-) diff --git a/app.js b/app.js index 327947c..cc2e7b6 100644 --- a/app.js +++ b/app.js @@ -20,7 +20,7 @@ app.use(cookieParser()); app.use(express.static(path.join(__dirname, 'public'))); app.use('/', routes); -app.use('/avatars', avatars); +app.use('/', avatars); // catch 404 and forward to error handler diff --git a/modules/helpers.js b/modules/helpers.js index 025927c..01d792c 100644 --- a/modules/helpers.js +++ b/modules/helpers.js @@ -53,6 +53,16 @@ function store_images(uuid, details, callback) { }); } + +var exp = {}; + +// returns true if the +uuid+ is a valid uuid or username +// the uuid may be not exist, however +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 @@ -61,7 +71,7 @@ function store_images(uuid, details, callback) { // 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 -function get_image_hash(uuid, callback) { +exp.get_image_hash = function(uuid, callback) { cache.get_details(uuid, function(err, details) { if (err) { callback(err, -1, null); @@ -72,9 +82,6 @@ function get_image_hash(uuid, callback) { callback(null, (details.hash ? 1 : 0), details.hash); } else { logging.log(uuid + " uuid not known or too old"); - logging.log("details:"); - logging.log(details); - logging.log("/details"); store_images(uuid, details, function(err, hash) { if (err) { callback(err, -1, details && details.hash); @@ -88,23 +95,16 @@ function get_image_hash(uuid, callback) { } } }); -} - -var exp = {}; - -// returns true if the +uuid+ is a valid uuid or username -// the uuid may be not exist, however -exp.uuid_valid = function(uuid) { - return valid_uuid.test(uuid); }; + // handles requests for +uuid+ images with +size+ // callback contains error, status, image buffer // 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) { logging.log("\nrequest: " + uuid); - get_image_hash(uuid, function(err, status, hash) { + exp.get_image_hash(uuid, function(err, status, hash) { if (hash) { var filepath = __dirname + '/../' + (helm ? config.helms_dir : config.faces_dir) + hash + ".png"; skins.resize_img(filepath, size, function(img_err, result) { diff --git a/routes/avatars.js b/routes/avatars.js index 888357f..111296e 100644 --- a/routes/avatars.js +++ b/routes/avatars.js @@ -1,3 +1,5 @@ +var networking = require('../modules/networking'); +var logging = require('../modules/logging'); var helpers = require('../modules/helpers'); var router = require('express').Router(); var config = require('../modules/config'); @@ -11,8 +13,46 @@ var human_status = { "-1": "error" }; +router.get('/skins/:uuid.:ext?', function(req, res) { + var uuid = req.params.uuid; + var start = new Date(); + + if (!helpers.uuid_valid(uuid)) { + res.status(422).send("422 Invalid UUID"); + return; + } + // strip dashes + uuid = uuid.replace(/-/g, ""); + try { + helpers.get_image_hash(uuid, function(err, status, hash) { + if (hash) { + res.writeHead(301, { + 'Location': "http://textures.minecraft.net/texture/" + hash, + 'Cache-Control': 'max-age=' + config.browser_cache_time + ', public', + 'Response-Time': new Date() - start, + 'X-Storage-Type': human_status[status] + }); + res.end(); + } else if (!err) { + res.writeHead(404, { + 'Cache-Control': 'max-age=' + config.browser_cache_time + ', public', + 'Response-Time': new Date() - start, + 'X-Storage-Type': human_status[status] + }); + res.end("404 Not found"); + } else { + res.status(500).send("500 Internal server error"); + } + }); + } catch(e) { + logging.error("Error!"); + logging.error(e); + res.status(500).send("500 Internal server error"); + } +}); + /* GET avatar request. */ -router.get('/:uuid.:ext?', function(req, res) { +router.get('/avatars/:uuid.:ext?', function(req, res) { var uuid = req.params.uuid; var size = req.query.size || config.default_size; var def = req.query.default; @@ -35,9 +75,9 @@ router.get('/:uuid.:ext?', function(req, res) { try { helpers.get_avatar(uuid, helm, size, function(err, status, image) { - console.log(uuid + " - " + human_status[status]); + logging.log(uuid + " - " + human_status[status]); if (err) { - console.error(err); + logging.error(err); } if (image) { sendimage(err ? 503 : 200, status, image); @@ -46,8 +86,8 @@ router.get('/:uuid.:ext?', function(req, res) { } }); } catch(e) { - console.error("Error!"); - console.error(e); + logging.error("Error!"); + logging.error(e); handle_default(500, status); } From 509bd78c8e014e382c8bd54891bbc80a5598974b Mon Sep 17 00:00:00 2001 From: jomo Date: Mon, 24 Nov 2014 01:12:39 +0100 Subject: [PATCH 12/17] add docs for usernames + skins --- views/index.jade | 45 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/views/index.jade b/views/index.jade index f26e0cd..64a0223 100644 --- a/views/index.jade +++ b/views/index.jade @@ -11,30 +11,41 @@ block content hr h2 Documentation - h3 Endpoint + h3 Avatars p | Replace - mark.green uuid - | with a Mojang UUID to get the related head. All images are PNGs. + mark.green id + | with a Mojang UUID or username to get the related head. All images are PNGs. .code | <img src="#{domain}/avatars/ - mark.green uuid + mark.green id | "> h3 Parameters h4 size p The size of the image in pixels, 1 - 512.
Default is 160. h4 default - p The image to be returned when the uuid has no skin (404).
Valid options are + p The image to be returned when the id has no skin (404).
Valid options are a(href="/avatars/00000000000000000000000000000000?default=steve") steve | or a(href="/avatars/00000000000000000000000000000000?default=alex") alex - | .
The standard value is calculated based on the UUID (even = alex, odd = steve) + | .
The standard value is calculated based on the id (even = alex, odd = steve) h4 helm p Get an avatar with the second (helmet) layer applied.
The content of this parameter is ignored + h3 Skins + p You can also get the full skin file from name or id.
+ | Replace + mark.green id + | with a Mojang UUID or username to get the related skin. + | You are redirected to the textures URL, or a 404 is returned. + .code + | <img src="#{domain}/skins/ + mark.green id + | "> + h3 HTTP headers - p Images will come with these HTTP headers, useful for debugging. + p Responses come with these HTTP headers, useful for debugging. h4 Response-Time p The time, in milliseconds, it took Crafatar to process the request. h4 X-Storage-Type @@ -42,9 +53,19 @@ block content li none: No external requests. Cached: User has no skin. li cached: No external requests. Skin cached and stored locally. li checked: 1 external request. Skin cached, checked for updates, no skin downloaded.
- | This happens either when the user has no skin or it didn't change. + | This happens either when the user removed their skin or when it didn't change. li downloaded: 2 external requests. Skin changed or unknown, downloaded. - li error: This can happen, for example, when Mojang's servers are down. If possible, an outdated image will be served instead. + li error: This can happen, for example, when Mojang's servers are down.
+ | If possible, an outdated image is be served instead. + + h3 About usernames + p We strongly advise you to use UUIDs instead of usernames in production. + | Usernames are deprecated by Mojang and you should only use usernames for testing.
+ | Invalid usernames are rejected and a 422 is returned. + + h3 About UUIDs + p UUIDs may use the raw or dashed format.
+ | Invalid UUIDs are rejected and a 422 is returned. h3 Examples p Get jeb_'s avatar, 160 × 160 pixels @@ -53,6 +74,12 @@ block content .code <img src="#{domain}/avatars/853c80ef3c3749fdaa49938b674adae6?size=64"> p Get jeb_'s helmet avatar, 64 × 64 pixels .code <img src="#{domain}/avatars/853c80ef3c3749fdaa49938b674adae6?size=64&helm"> + p Get jeb_'s avatar by username, 160 x 160 pixels + .code <img src="#{domain}/avatars/jeb_"> + p Get jeb_'s skin + .code <img src="#{domain}/skins/853c80ef3c3749fdaa49938b674adae6"> + p Get jeb_'s skin by username + .code <img src="#{domain}/skins/jeb_"> .col-md-2.center .sideface.redstone_sheep(title="redstone_sheep") .sideface.Jake0oo0(title="Jake0oo0") From d658a84668ea3379455299d1c96d7b968a0e5fdf Mon Sep 17 00:00:00 2001 From: jomo Date: Mon, 24 Nov 2014 01:49:00 +0100 Subject: [PATCH 13/17] docs improvements --- public/stylesheets/style.css | 5 +++ routes/index.js | 4 ++- views/index.jade | 64 +++++++++++++++++++++++++++--------- 3 files changed, 57 insertions(+), 16 deletions(-) diff --git a/public/stylesheets/style.css b/public/stylesheets/style.css index a2a79d4..2c3bb75 100644 --- a/public/stylesheets/style.css +++ b/public/stylesheets/style.css @@ -13,6 +13,11 @@ a { color: #00B7FF; } +a.anchor { + position: relative; + top: -50px; +} + a.forkme { top: 0; right: 0; diff --git a/routes/index.js b/routes/index.js index ee6e85e..549b698 100644 --- a/routes/index.js +++ b/routes/index.js @@ -1,11 +1,13 @@ var express = require('express'); +var config = require('../modules/config'); var router = express.Router(); /* GET home page. */ router.get('/', function(req, res) { res.render('index', { title: 'Crafatar', - domain: "https://" + req.headers.host + domain: "https://" + req.headers.host, + config: config }); }); diff --git a/views/index.jade b/views/index.jade index 64a0223..84e34cc 100644 --- a/views/index.jade +++ b/views/index.jade @@ -4,14 +4,20 @@ block content .container(style= "margin-top: 70px;") .row .col-md-10 - h1 Crafatar + a(name="crafatar", class="anchor") + a(href="#crafatar") + h1 Crafatar hr p Welcome to Crafatar, an API for Minecraft's faces! hr - h2 Documentation + a(name="documentation", class="anchor") + a(href="#documentation") + h2 Documentation - h3 Avatars + a(name="avatars", class="anchor") + a(href="#avatars") + h3 Avatars p | Replace mark.green id @@ -21,19 +27,29 @@ block content mark.green id | "> - h3 Parameters - h4 size - p The size of the image in pixels, 1 - 512.
Default is 160. - h4 default + a(name="parameters", class="anchor") + a(href="#parameters") + h3 Parameters + a(name="size", class="anchor") + a(href="#size") + h4 size + p The size of the image in pixels, #{config.min_size} - #{config.max_size}.
Default is #{config.default_size}. + a(name="default", class="anchor") + a(href="#default") + h4 default p The image to be returned when the id has no skin (404).
Valid options are a(href="/avatars/00000000000000000000000000000000?default=steve") steve | or a(href="/avatars/00000000000000000000000000000000?default=alex") alex | .
The standard value is calculated based on the id (even = alex, odd = steve) - h4 helm + a(name="helm", class="anchor") + a(href="#helm") + h4 helm p Get an avatar with the second (helmet) layer applied.
The content of this parameter is ignored - h3 Skins + a(name="skins", class="anchor") + a(href="#skins") + h3 Skins p You can also get the full skin file from name or id.
| Replace mark.green id @@ -44,11 +60,17 @@ block content mark.green id | "> - h3 HTTP headers + a(name="http-headers", class="anchor") + a(href="#http-headers") + h3 HTTP headers p Responses come with these HTTP headers, useful for debugging. - h4 Response-Time + a(name="response-time", class="anchor") + a(href="#response-time") + h4 Response-Time p The time, in milliseconds, it took Crafatar to process the request. - h4 X-Storage-Type + a(name="x-storage-type", class="anchor") + a(href="#x-storage-type") + h4 X-Storage-Type ul li none: No external requests. Cached: User has no skin. li cached: No external requests. Skin cached and stored locally. @@ -58,16 +80,28 @@ block content li error: This can happen, for example, when Mojang's servers are down.
| If possible, an outdated image is be served instead. - h3 About usernames + a(name="about-usernames", class="anchor") + a(href="#about-usernames") + h3 About usernames p We strongly advise you to use UUIDs instead of usernames in production. | Usernames are deprecated by Mojang and you should only use usernames for testing.
| Invalid usernames are rejected and a 422 is returned. - h3 About UUIDs + a(name="about-uuids", class="anchor") + a(href="#about-uuids") + h3 About UUIDs p UUIDs may use the raw or dashed format.
| Invalid UUIDs are rejected and a 422 is returned. - h3 Examples + a(name="about-caching", class="anchor") + a(href="#about-caching") + h3 About caching + p Crafatar caches keeps skins for #{config.local_cache_time} seconds until they are checked for changes.
+ | Images should be cached in browsers for #{config.browser_cache_time} seconds until a new request to Crafatar is made. + + a(name="examples", class="anchor") + a(href="#examples") + h3 Examples p Get jeb_'s avatar, 160 × 160 pixels .code <img src="#{domain}/avatars/853c80ef3c3749fdaa49938b674adae6"> p Get jeb_'s avatar, 64 × 64 pixels From 18bec6a19641adfc21d607e08a2166a0a8f8a880 Mon Sep 17 00:00:00 2001 From: jomo Date: Mon, 24 Nov 2014 02:00:01 +0100 Subject: [PATCH 14/17] make html valid --- views/index.jade | 63 +++++++++++++++++++++++++++--------------------- 1 file changed, 35 insertions(+), 28 deletions(-) diff --git a/views/index.jade b/views/index.jade index 84e34cc..8936493 100644 --- a/views/index.jade +++ b/views/index.jade @@ -4,18 +4,18 @@ block content .container(style= "margin-top: 70px;") .row .col-md-10 - a(name="crafatar", class="anchor") + a(id="crafatar", class="anchor") a(href="#crafatar") h1 Crafatar hr p Welcome to Crafatar, an API for Minecraft's faces! hr - a(name="documentation", class="anchor") + a(id="documentation", class="anchor") a(href="#documentation") h2 Documentation - a(name="avatars", class="anchor") + a(id="avatars", class="anchor") a(href="#avatars") h3 Avatars p @@ -27,30 +27,32 @@ block content mark.green id | "> - a(name="parameters", class="anchor") + a(id="parameters", class="anchor") a(href="#parameters") h3 Parameters - a(name="size", class="anchor") + a(id="size", class="anchor") a(href="#size") h4 size p The size of the image in pixels, #{config.min_size} - #{config.max_size}.
Default is #{config.default_size}. - a(name="default", class="anchor") + a(id="default", class="anchor") a(href="#default") h4 default - p The image to be returned when the id has no skin (404).
Valid options are + p + | The image to be returned when the id has no skin (404).
Valid options are a(href="/avatars/00000000000000000000000000000000?default=steve") steve | or a(href="/avatars/00000000000000000000000000000000?default=alex") alex | .
The standard value is calculated based on the id (even = alex, odd = steve) - a(name="helm", class="anchor") + a(id="helm", class="anchor") a(href="#helm") h4 helm p Get an avatar with the second (helmet) layer applied.
The content of this parameter is ignored - a(name="skins", class="anchor") + a(id="skins", class="anchor") a(href="#skins") h3 Skins - p You can also get the full skin file from name or id.
+ p + | You can also get the full skin file from name or id.
| Replace mark.green id | with a Mojang UUID or username to get the related skin. @@ -60,46 +62,51 @@ block content mark.green id | "> - a(name="http-headers", class="anchor") + a(id="http-headers", class="anchor") a(href="#http-headers") h3 HTTP headers p Responses come with these HTTP headers, useful for debugging. - a(name="response-time", class="anchor") + a(id="response-time", class="anchor") a(href="#response-time") h4 Response-Time p The time, in milliseconds, it took Crafatar to process the request. - a(name="x-storage-type", class="anchor") + a(id="x-storage-type", class="anchor") a(href="#x-storage-type") h4 X-Storage-Type ul li none: No external requests. Cached: User has no skin. li cached: No external requests. Skin cached and stored locally. - li checked: 1 external request. Skin cached, checked for updates, no skin downloaded.
- | This happens either when the user removed their skin or when it didn't change. + li + | checked: 1 external request. Skin cached, checked for updates, no skin downloaded.
+ | This happens either when the user removed their skin or when it didn't change. li downloaded: 2 external requests. Skin changed or unknown, downloaded. - li error: This can happen, for example, when Mojang's servers are down.
- | If possible, an outdated image is be served instead. + li + | error: This can happen, for example, when Mojang's servers are down.
+ | If possible, an outdated image is be served instead. - a(name="about-usernames", class="anchor") + a(id="about-usernames", class="anchor") a(href="#about-usernames") h3 About usernames - p We strongly advise you to use UUIDs instead of usernames in production. - | Usernames are deprecated by Mojang and you should only use usernames for testing.
- | Invalid usernames are rejected and a 422 is returned. + p + | We strongly advise you to use UUIDs instead of usernames in production. + | Usernames are deprecated by Mojang and you should only use usernames for testing.
+ | Invalid usernames are rejected and a 422 is returned. - a(name="about-uuids", class="anchor") + a(id="about-uuids", class="anchor") a(href="#about-uuids") h3 About UUIDs - p UUIDs may use the raw or dashed format.
- | Invalid UUIDs are rejected and a 422 is returned. + p + | UUIDs may use the raw or dashed format.
+ | Invalid UUIDs are rejected and a 422 is returned. - a(name="about-caching", class="anchor") + a(id="about-caching", class="anchor") a(href="#about-caching") h3 About caching - p Crafatar caches keeps skins for #{config.local_cache_time} seconds until they are checked for changes.
- | Images should be cached in browsers for #{config.browser_cache_time} seconds until a new request to Crafatar is made. + p + | Crafatar caches keeps skins for #{config.local_cache_time} seconds until they are checked for changes.
+ | Images should be cached in browsers for #{config.browser_cache_time} seconds until a new request to Crafatar is made. - a(name="examples", class="anchor") + a(id="examples", class="anchor") a(href="#examples") h3 Examples p Get jeb_'s avatar, 160 × 160 pixels From 1dd1ee4d34bb53cbf50f363658d019ea71fb2d70 Mon Sep 17 00:00:00 2001 From: jomo Date: Mon, 24 Nov 2014 02:12:56 +0100 Subject: [PATCH 15/17] allow custom default image, fix #17 --- routes/avatars.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/routes/avatars.js b/routes/avatars.js index 111296e..eecf9f1 100644 --- a/routes/avatars.js +++ b/routes/avatars.js @@ -93,11 +93,19 @@ router.get('/avatars/:uuid.:ext?', function(req, res) { function handle_default(http_status, img_status) { if (def != "steve" && def != "alex") { - def = skins.default_skin(uuid); + res.writeHead(301, { + 'Cache-Control': 'max-age=' + config.browser_cache_time + ', public', + 'Response-Time': new Date() - start, + 'X-Storage-Type': human_status[img_status], + 'Location': def + }); + res.end(); + } else { + def = def || skins.default_skin; + skins.resize_img("public/images/" + def + ".png", size, function(err, image) { + sendimage(http_status, img_status, image); + }); } - skins.resize_img("public/images/" + def + ".png", size, function(err, image) { - sendimage(http_status, img_status, image); - }); } function sendimage(http_status, img_status, image) { From fdb52d7d0f94909ae3e503c9dfffd86ac234fde3 Mon Sep 17 00:00:00 2001 From: jomo Date: Mon, 24 Nov 2014 02:21:59 +0100 Subject: [PATCH 16/17] add docs for #17 --- views/index.jade | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/views/index.jade b/views/index.jade index 8936493..d29d712 100644 --- a/views/index.jade +++ b/views/index.jade @@ -42,7 +42,8 @@ block content a(href="/avatars/00000000000000000000000000000000?default=steve") steve | or a(href="/avatars/00000000000000000000000000000000?default=alex") alex - | .
The standard value is calculated based on the id (even = alex, odd = steve) + | .
A URL is also accepted.
+ | The standard value is calculated based on the id (even = alex, odd = steve) a(id="helm", class="anchor") a(href="#helm") h4 helm @@ -115,6 +116,10 @@ block content .code <img src="#{domain}/avatars/853c80ef3c3749fdaa49938b674adae6?size=64"> p Get jeb_'s helmet avatar, 64 × 64 pixels .code <img src="#{domain}/avatars/853c80ef3c3749fdaa49938b674adae6?size=64&helm"> + p Get jeb_'s avatar or fall back to steve + .code <img src="#{domain}/avatars/853c80ef3c3749fdaa49938b674adae6?default=steve"> + p Get jeb_'s avatar or fall back to a custom image + .code <img src="#{domain}/avatars/853c80ef3c3749fdaa49938b674adae6?default=https%3A%2F%2Fi.imgur.com%2FozszMZV.png"> p Get jeb_'s avatar by username, 160 x 160 pixels .code <img src="#{domain}/avatars/jeb_"> p Get jeb_'s skin From 0734060d96770c99389b15d9546f7bd8c6e4e940 Mon Sep 17 00:00:00 2001 From: jomo Date: Mon, 24 Nov 2014 02:31:21 +0100 Subject: [PATCH 17/17] oops :poop: --- routes/avatars.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/routes/avatars.js b/routes/avatars.js index eecf9f1..3d30496 100644 --- a/routes/avatars.js +++ b/routes/avatars.js @@ -92,7 +92,7 @@ router.get('/avatars/:uuid.:ext?', function(req, res) { } function handle_default(http_status, img_status) { - if (def != "steve" && def != "alex") { + if (def && def != "steve" && def != "alex") { res.writeHead(301, { 'Cache-Control': 'max-age=' + config.browser_cache_time + ', public', 'Response-Time': new Date() - start, @@ -101,7 +101,7 @@ router.get('/avatars/:uuid.:ext?', function(req, res) { }); res.end(); } else { - def = def || skins.default_skin; + def = def || skins.default_skin(uuid); skins.resize_img("public/images/" + def + ".png", size, function(err, image) { sendimage(http_status, img_status, image); });