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 0c42981..1001ab0 100644
--- a/README.md
+++ b/README.md
@@ -11,10 +11,15 @@ 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
* `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/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/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.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 9b15725..4b77193 100644
--- a/modules/config.js
+++ b/modules/config.js
@@ -4,9 +4,10 @@ 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 '/'
+ helms_dir: 'skins/helms/', // directory where helms are kept. should have trailing '/'
+ debug_enabled: true // enables logging.debug
};
module.exports = config;
\ No newline at end of file
diff --git a/modules/helpers.js b/modules/helpers.js
index 2a2183a..01d792c 100644
--- a/modules/helpers.js
+++ b/modules/helpers.js
@@ -1,11 +1,12 @@
var networking = require('./networking');
+var logging = require('./logging');
var config = require('./config');
var cache = require('./cache');
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,40 +15,27 @@ 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) {
+ logging.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");
+ 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
- networking.skin_file(skinurl, facepath, helmpath, function(err) {
+ networking.skin_file(skin_url, facepath, helmpath, function(err) {
if (err) {
callback(err, null);
} else {
@@ -65,53 +53,6 @@ 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
-function get_image_hash(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()) {
- // uuid known + recently updated
- console.log(uuid + " uuid known & recently updated");
- callback(null, (details.hash ? 1 : 0), details.hash);
- } else {
- console.log(uuid + " uuid not known or too old");
- 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 exp = {};
@@ -121,13 +62,49 @@ 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
+// -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
+exp.get_image_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()) {
+ // uuid known + recently updated
+ logging.log(uuid + " uuid known & recently updated");
+ callback(null, (details.hash ? 1 : 0), details.hash);
+ } else {
+ logging.log(uuid + " uuid not known or too old");
+ store_images(uuid, details, function(err, hash) {
+ if (err) {
+ callback(err, -1, details && details.hash);
+ } else {
+ logging.log(uuid + " hash: " + hash);
+ var oldhash = details && details.hash;
+ var status = hash !== oldhash ? 2 : 3;
+ callback(null, status, hash);
+ }
+ });
+ }
+ }
+ });
+};
+
+
// 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) {
- console.log("\nrequest: " + uuid);
- get_image_hash(uuid, function(err, status, hash) {
+ logging.log("\nrequest: " + uuid);
+ 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/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 270c541..098d85e 100644
--- a/modules/networking.js
+++ b/modules/networking.js
@@ -1,54 +1,115 @@
+var logging = require('./logging');
var request = require('request');
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 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
+ 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
+ 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
+ logging.warn(name + " Too many requests");
+ logging.warn(body);
+ callback(null, null);
+ } else {
+ logging.error(name + " Unknown error:");
+ logging.error(response);
+ logging.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
}, function (error, response, body) {
if (!error && response.statusCode == 200) {
// profile downloaded successfully
- console.log(uuid + " profile downloaded");
- callback(null, JSON.parse(body));
+ 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);
}
});
};
+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+
// 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;
}
@@ -59,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);
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/avatars.js b/routes/avatars.js
index 9ea50a0..24def5e 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');
@@ -6,13 +8,51 @@ var skins = require('../modules/skins');
var human_status = {
0: "none",
1: "cached",
- 2: "checked",
- 3: "downloaded",
+ 2: "downloaded",
+ 3: "checked",
"-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,18 +86,26 @@ 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);
}
function handle_default(http_status, img_status) {
- if (def != "steve" && def != "alex") {
- def = skins.default_skin(uuid);
+ if (def && def != "steve" && def != "alex") {
+ 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(uuid);
+ 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) {
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/test/test.js b/test/test.js
index 949d1e7..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,18 +11,21 @@ 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
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 +62,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 +116,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();
});
@@ -116,6 +128,18 @@ describe('UUID/username', function() {
done();
});
});
+ it("should already have the files / not download", function(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) {
assert.strictEqual(skins.default_skin("ec561538f3fd461daff5086b22154bce"), "alex");
done();
@@ -126,7 +150,7 @@ describe('UUID/username', function() {
});
});
- describe('Mojang Errors', function() {
+ describe('Errors', function() {
before(function() {
cache.get_redis().flushall();
});
@@ -136,21 +160,46 @@ describe('UUID/username', 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_profile("069a79f444e94726a5befca90e38aaf5", function(err, profile) {
+ networking.get_skin_url("069a79f444e94726a5befca90e38aaf5", function(err, skin_url) {
assert.strictEqual(err.code, "ETIMEDOUT");
- config.http_timeout = 3000;
+ 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();
});
});
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();
});
});
+ it("should not find the skin", function(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) {
+ assert.doesNotThrow(function() {
+ cache.update_timestamp("0123456789abcdef0123456789abcdef", "invalid-file.png");
+ });
+ done();
+ });
});
});
diff --git a/views/index.jade b/views/index.jade
index f26e0cd..d29d712 100644
--- a/views/index.jade
+++ b/views/index.jade
@@ -4,55 +4,128 @@ block content
.container(style= "margin-top: 70px;")
.row
.col-md-10
- h1 Crafatar
+ a(id="crafatar", class="anchor")
+ a(href="#crafatar")
+ h1 Crafatar
hr
p Welcome to Crafatar, an API for Minecraft's faces!
hr
- h2 Documentation
+ a(id="documentation", class="anchor")
+ a(href="#documentation")
+ h2 Documentation
- h3 Endpoint
+ a(id="avatars", class="anchor")
+ a(href="#avatars")
+ 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
+ a(id="parameters", class="anchor")
+ a(href="#parameters")
+ h3 Parameters
+ 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(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
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)
- h4 helm
+ | .
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
p Get an avatar with the second (helmet) layer applied.
The content of this parameter is ignored
- h3 HTTP headers
- p Images will come with these HTTP headers, useful for debugging.
- h4 Response-Time
+ a(id="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
+ | 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
+ | ">
+
+ a(id="http-headers", class="anchor")
+ a(href="#http-headers")
+ h3 HTTP headers
+ p Responses come with these HTTP headers, useful for debugging.
+ 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.
- h4 X-Storage-Type
+ 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 has no skin or 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 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 Examples
+ 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.
+
+ 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.
+
+ 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.
+
+ a(id="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
.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
+ .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")