From 442dee02804d01fea26aec0e60ff36823a7a91bb Mon Sep 17 00:00:00 2001 From: jomo Date: Mon, 27 Jul 2015 20:06:16 +0200 Subject: [PATCH 01/86] don't print all this shit when we receive a 500 --- lib/networking.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/networking.js b/lib/networking.js index 5e387cc..8c3f662 100644 --- a/lib/networking.js +++ b/lib/networking.js @@ -94,8 +94,8 @@ exp.get_from_options = function(rid, url, options, callback) { // cause error so the image will not be cached callback(body || null, response, (error || "TooManyRequests")); } else { - logging.error(rid, " Unknown reply:"); - logging.error(rid, JSON.stringify(response)); + // Probably 500 or the likes + logging.error(rid, "Unexpected response:", code, body); callback(body || null, response, error); } }); From d6a9f7c71ae5690278c5290e060aa1f61095a077 Mon Sep 17 00:00:00 2001 From: jomo Date: Tue, 28 Jul 2015 23:58:48 +0200 Subject: [PATCH 02/86] add JS to check mojang's server status does CORS request to status.mojang.com/check and figures out if 'uuid', 'name' or 'both' are affected not doing anything yet --- lib/public/javascript/mojang.js | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 lib/public/javascript/mojang.js diff --git a/lib/public/javascript/mojang.js b/lib/public/javascript/mojang.js new file mode 100644 index 0000000..2cad271 --- /dev/null +++ b/lib/public/javascript/mojang.js @@ -0,0 +1,28 @@ +var xhr = new XMLHttpRequest(); + +xhr.onload = function() { + var response = JSON.parse(xhr.responseText); + var status = {}; + response.map(function(elem) { + var key = Object.keys(elem)[0]; + status[key] = elem[key]; + }); + + var textures = status["textures.minecraft.net"] !== "green"; + var session = status["sessionserver.mojang.com"] !== "green"; + var skins = status["skins.minecraft.net"] !== "green"; + var error = null; + + if (textures || session && skins) { + error = "both"; + } else if (skins) { + error = "name"; + } else if (session) { + error = "uuid"; + } + + console.log(error); +}; + +xhr.open("GET", "https://status.mojang.com/check", true); +xhr.send(); \ No newline at end of file From 49b4ae1a6e0c17ea8e50306f4fef9c97d1b3c0cd Mon Sep 17 00:00:00 2001 From: jomo Date: Sun, 16 Aug 2015 06:17:38 +0200 Subject: [PATCH 03/86] exit on process error From the iojs docs: > An unhandled exception means your application - and by extension io.js itself - > is in an undefined state. Blindly resuming means anything could happen. > > Think of resuming as pulling the power cord when you are upgrading your system. > Nine out of ten times nothing happens - but the 10th time, your system is bust. > > uncaughtException should be used to perform synchronous cleanup before shutting > down the process. It is not safe to resume normal operation after > uncaughtException. If you do use it, restart your application after every > unhandled exception! --- www.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/www.js b/www.js index 981c14b..0b649ff 100644 --- a/www.js +++ b/www.js @@ -3,8 +3,9 @@ var cleaner = require("./lib/cleaner"); var config = require("./config"); var cluster = require("cluster"); -process.on("uncaughtException", function (err) { +process.on("uncaughtException", function(err) { logging.error("uncaughtException", err.stack || err.toString()); + process.exit(1); }); if (cluster.isMaster) { From 79da225b9f63aa9b9370c8abc21bae9ee1f0d04a Mon Sep 17 00:00:00 2001 From: jomo Date: Sun, 16 Aug 2015 22:11:08 +0200 Subject: [PATCH 04/86] gracefully shut down on SIGTERM this will close the server, i.e. all new connections will be dropped while existing connections are able to complete within 30 seconds otherwise they are dropped and the server force quits --- lib/server.js | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/lib/server.js b/lib/server.js index b480a3e..d9380a0 100644 --- a/lib/server.js +++ b/lib/server.js @@ -131,18 +131,33 @@ var exp = {}; exp.boot = function(callback) { var port = process.env.PORT || 3000; var bind_ip = process.env.BIND || "0.0.0.0"; - logging.log("Server running on http://" + bind_ip + ":" + port + "/"); server = http.createServer(requestHandler).listen(port, bind_ip, function() { + logging.log("Server running on http://" + bind_ip + ":" + port + "/"); if (callback) { callback(); } }); + + // stop accepting new connections, + // wait for established connections to finish (30s max), + // then exit + process.on("SIGTERM", function() { + logging.warn("Got SIGTERM, no longer accepting connections!"); + + setTimeout(function() { + logging.error("Dropping connections after 30s. Force quit."); + process.exit(1); + }, 30000); + + server.close(function() { + // all connections have been closed + process.exit(); + }); + }); }; exp.close = function(callback) { - server.close(function() { - callback(); - }); + server.close(callback); }; module.exports = exp; From 72708ca590030d24537bb0b0c6e942aa52c04ff3 Mon Sep 17 00:00:00 2001 From: jomo Date: Sun, 16 Aug 2015 22:14:26 +0200 Subject: [PATCH 05/86] remove forever, update dependencies forever should be used externally to start crafatar. it's not an internal dependency --- package.json | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 6428b81..4d7d2fc 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ ], "scripts": { "postinstall": "cp 'config.example.js' 'config.js'", - "start": "forever -l logs/log.log -o logs/out.log -e logs/error.log -p ./ -a --minUptime 8000 --spinSleepTime 1500 www.js", + "start": "node www.js", "test": "mocha", "test-travis": "istanbul cover ./node_modules/mocha/bin/_mocha --report lcovonly -- -R spec && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage" }, @@ -30,19 +30,18 @@ "iojs": "2.0.x" }, "dependencies": { - "canvas": "^1.2.3", + "canvas": "^1.2.7", "crc": "~3.3.0", - "forever": "~0.14.2", "jade": "~1.11.0", "lwip": "~0.0.7", "mime": "~1.3.4", "node-df": "~0.1.1", "redis": "~0.12.1", - "request": "~2.58.0", + "request": "~2.60.0", "toobusy-js": "~0.4.2" }, "devDependencies": { - "coveralls": "~2.11.2", + "coveralls": "~2.11.4", "istanbul": "~0.3.17", "mocha": "~2.2.5", "mocha-lcov-reporter": "~0.0.2" From 85e7b4b5713355f9ab95ddac997982ce3cea14d7 Mon Sep 17 00:00:00 2001 From: jomo Date: Sun, 16 Aug 2015 22:18:17 +0200 Subject: [PATCH 06/86] remove clusters clusters aren't supported, see #80 until we actually use clusters, having a main AND a single worker cluster just makes things more difficult --- config.example.js | 1 - lib/logging.js | 4 +--- www.js | 18 ++---------------- 3 files changed, 3 insertions(+), 20 deletions(-) diff --git a/config.example.js b/config.example.js index be165d5..3ce042d 100644 --- a/config.example.js +++ b/config.example.js @@ -29,7 +29,6 @@ var config = { server: { http_timeout: 1000, // ms until connection to Mojang is dropped debug_enabled: false, // enables logging.debug - clusters: 1, // we recommend not using multiple clusters YET, see issue #80 log_time: true // set to false if you use an external logger that provides timestamps } }; diff --git a/lib/logging.js b/lib/logging.js index b39844f..668bd89 100644 --- a/lib/logging.js +++ b/lib/logging.js @@ -1,4 +1,3 @@ -var cluster = require("cluster"); var config = require("../config"); var exp = {}; @@ -18,10 +17,9 @@ function join_args(args) { function log(level, args, logger) { logger = logger || console.log; var time = config.server.log_time ? new Date().toISOString() + " " : ""; - var clid = (cluster.worker && cluster.worker.id || "M"); var lines = join_args(args).split("\n"); for (var i = 0, l = lines.length; i < l; i++) { - logger(time + clid, level + ":", lines[i]); + logger(time, level + ":", lines[i]); } } diff --git a/www.js b/www.js index 0b649ff..2a52daf 100644 --- a/www.js +++ b/www.js @@ -1,26 +1,12 @@ var logging = require("./lib/logging"); var cleaner = require("./lib/cleaner"); var config = require("./config"); -var cluster = require("cluster"); process.on("uncaughtException", function(err) { logging.error("uncaughtException", err.stack || err.toString()); process.exit(1); }); -if (cluster.isMaster) { - var cores = config.server.clusters || require("os").cpus().length; - logging.log("Starting", cores + " worker" + (cores > 1 ? "s" : "")); - for (var i = 0; i < cores; i++) { - cluster.fork(); - } +setInterval(cleaner.run, config.cleaner.interval * 1000); - cluster.on("exit", function (worker) { - logging.error("Worker #" + worker.id + " died. Rebooting a new one."); - setTimeout(cluster.fork, 100); - }); - - setInterval(cleaner.run, config.cleaner.interval * 1000); -} else { - require("./lib/server.js").boot(); -} \ No newline at end of file +require("./lib/server.js").boot(); \ No newline at end of file From 0b58b3a4d10c4e0a3ad3904533e6e3831b841fce Mon Sep 17 00:00:00 2001 From: jomo Date: Sun, 16 Aug 2015 22:27:22 +0200 Subject: [PATCH 07/86] add final log before shutting down on SIGTERM --- lib/server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/server.js b/lib/server.js index d9380a0..3d2d6be 100644 --- a/lib/server.js +++ b/lib/server.js @@ -150,7 +150,7 @@ exp.boot = function(callback) { }, 30000); server.close(function() { - // all connections have been closed + logging.log("All connections closed, shutting down."); process.exit(); }); }); From b460260bae93dbd3ec0eb648c19f494e534cf1d2 Mon Sep 17 00:00:00 2001 From: jomo Date: Tue, 18 Aug 2015 01:08:01 +0200 Subject: [PATCH 08/86] remove logs/ should be up to the user where to put log files and to make sure that directory actually exists currently nothing in this code base expects logs/ to be present. --- logs/.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 logs/.gitkeep diff --git a/logs/.gitkeep b/logs/.gitkeep deleted file mode 100644 index e69de29..0000000 From 4d949362beeaf432201f576d83518dd0bb0351a8 Mon Sep 17 00:00:00 2001 From: jomo Date: Tue, 18 Aug 2015 01:11:38 +0200 Subject: [PATCH 09/86] change default config to use /var/lib/crafatr/images I think this makes more sense, especially when you run multiple instances so they can use the same image cache instead of each version relying on their own --- config.example.js | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/config.example.js b/config.example.js index 3ce042d..265f17c 100644 --- a/config.example.js +++ b/config.example.js @@ -1,35 +1,35 @@ var config = { avatars: { - 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_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 }, renders: { - min_scale: 1, // for 3D rendered skins - max_scale: 10, // for 3D rendered skins; too big values might lead to slow response time or DoS - default_scale: 6 // for 3D rendered skins; scale to be used when no scale given + min_scale: 1, // for 3D rendered skins + max_scale: 10, // for 3D rendered skins; too big values might lead to slow response time or DoS + default_scale: 6 // for 3D rendered skins; scale to be used when no scale given }, cleaner: { - interval: 1800, // interval seconds to check limits - disk_limit: 10240, // min allowed free KB on disk to trigger image deletion - redis_limit: 24576, // max allowed used KB on redis to trigger redis flush - amount: 50000 // amount of skins for which all iamge types are deleted + interval: 1800, // interval seconds to check limits + disk_limit: 10240, // min allowed free KB on disk to trigger image deletion + redis_limit: 24576, // max allowed used KB on redis to trigger redis flush + amount: 50000 // amount of skins for which all iamge types are deleted }, directories: { - faces: "images/faces/", // directory where faces are kept. should have trailing "/" - helms: "images/helms/", // directory where helms are kept. should have trailing "/" - skins: "images/skins/", // directory where skins are kept. should have trailing "/" - renders: "images/renders/", // directory where rendered skins are kept. should have trailing "/" - capes: "images/capes/" // directory where capes are kept. should have trailing "/" + faces: "/var/lib/crafatar/images/faces/", // directory where faces are kept. must have trailing "/" + helms: "/var/lib/crafatar/images/helms/", // directory where helms are kept. must have trailing "/" + skins: "/var/lib/crafatar/images/skins/", // directory where skins are kept. must have trailing "/" + renders: "/var/lib/crafatar/images/renders/", // directory where rendered skins are kept. must have trailing "/" + capes: "/var/lib/crafatar/images/capes/" // directory where capes are kept. must have trailing "/" }, caching: { - local: 1200, // seconds until we will check if user's skin changed. should be > 60 to comply with Mojang's rate limit - browser: 3600 // seconds until browser will request image again + local: 1200, // seconds until we will check if user's skin changed. should be > 60 to comply with Mojang's rate limit + browser: 3600 // seconds until browser will request image again }, server: { - http_timeout: 1000, // ms until connection to Mojang is dropped - debug_enabled: false, // enables logging.debug - log_time: true // set to false if you use an external logger that provides timestamps + http_timeout: 1000, // ms until connection to Mojang is dropped + debug_enabled: false, // enables logging.debug + log_time: true // set to false if you use an external logger that provides timestamps } }; From 9ed431d7ad4a37dd7353ac40a79a01b8e32da9c3 Mon Sep 17 00:00:00 2001 From: jomo Date: Tue, 18 Aug 2015 01:33:00 +0200 Subject: [PATCH 10/86] remove images/ (see @4d94936) --- .gitignore | 10 +++++----- images/capes/.gitkeep | 0 images/faces/.gitkeep | 0 images/helms/.gitkeep | 0 images/renders/.gitkeep | 0 images/skins/.gitkeep | 0 6 files changed, 5 insertions(+), 5 deletions(-) delete mode 100644 images/capes/.gitkeep delete mode 100644 images/faces/.gitkeep delete mode 100644 images/helms/.gitkeep delete mode 100644 images/renders/.gitkeep delete mode 100644 images/skins/.gitkeep diff --git a/.gitignore b/.gitignore index 79ec252..1c01a67 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,8 @@ -images/*/*.png -*.log +images/ node_modules/ -.DS_Store -*.rdb coverage/ -config.js +.DS_Store +*.log +*.rdb *.sublime-* +config.js diff --git a/images/capes/.gitkeep b/images/capes/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/images/faces/.gitkeep b/images/faces/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/images/helms/.gitkeep b/images/helms/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/images/renders/.gitkeep b/images/renders/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/images/skins/.gitkeep b/images/skins/.gitkeep deleted file mode 100644 index e69de29..0000000 From a928952dc4289626a4348d3e6cb0bad34b4cf025 Mon Sep 17 00:00:00 2001 From: jomo Date: Thu, 20 Aug 2015 01:52:01 +0200 Subject: [PATCH 11/86] don't force image directories to be relative --- lib/cache.js | 2 +- lib/cleaner.js | 10 +++++----- lib/helpers.js | 18 +++++++++--------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/lib/cache.js b/lib/cache.js index 1f7a568..8527300 100644 --- a/lib/cache.js +++ b/lib/cache.js @@ -40,7 +40,7 @@ function connect_redis() { // the helms file is ignored because we only need 1 file to read/write from function update_file_date(rid, skin_hash) { if (skin_hash) { - var face_path = path.join(__dirname, "..", config.directories.faces, skin_hash + ".png"); + var face_path = path.join(config.directories.faces, skin_hash + ".png"); fs.exists(face_path, function(exists) { if (exists) { var date = new Date(); diff --git a/lib/cleaner.js b/lib/cleaner.js index 53924c5..443f2ba 100644 --- a/lib/cleaner.js +++ b/lib/cleaner.js @@ -35,7 +35,7 @@ function should_clean_redis(callback) { // callback: error, true|false function should_clean_disk(callback) { df({ - file: path.join(__dirname, "..", config.directories.faces), + file: config.directories.faces, prefixMultiplier: "KiB", isDisplayPrefixMultiplier: false, precision: 2 @@ -71,10 +71,10 @@ exp.run = function() { logging.error(err); } else if (clean) { logging.warn("DiskCleaner: Disk limit reached! Cleaning images now"); - var facesdir = path.join(__dirname, "..", config.directories.faces); - var helmdir = path.join(__dirname, "..", config.directories.helms); - var renderdir = path.join(__dirname, "..", config.directories.renders); - var skindir = path.join(__dirname, "..", config.directories.skins); + var facesdir = config.directories.faces; + var helmdir = config.directories.helms; + var renderdir = config.directories.renders; + var skindir = config.directories.skins; fs.readdir(facesdir, function (readerr, files) { if (!readerr) { for (var i = 0, l = Math.min(files.length, config.cleaner.amount); i < l; i++) { diff --git a/lib/helpers.js b/lib/helpers.js index 549e35a..c76a319 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -30,9 +30,9 @@ function store_skin(rid, userId, profile, cache_details, callback) { }); } else { logging.debug(rid, "new skin hash:", skin_hash); - var facepath = path.join(__dirname, "..", config.directories.faces, skin_hash + ".png"); - var helmpath = path.join(__dirname, "..", config.directories.helms, skin_hash + ".png"); - var skinpath = path.join(__dirname, "..", config.directories.skins, skin_hash + ".png"); + var facepath = path.join(config.directories.faces, skin_hash + ".png"); + var helmpath = path.join(config.directories.helms, skin_hash + ".png"); + var skinpath = path.join(config.directories.skins, skin_hash + ".png"); fs.exists(facepath, function(exists) { if (exists) { logging.debug(rid, "skin already exists, not downloading"); @@ -87,7 +87,7 @@ function store_cape(rid, userId, profile, cache_details, callback) { }); } else { logging.debug(rid, "new cape hash:", cape_hash); - var capepath = path.join(__dirname, "..", config.directories.capes, cape_hash + ".png"); + var capepath = path.join(config.directories.capes, cape_hash + ".png"); fs.exists(capepath, function(exists) { if (exists) { logging.debug(rid, "cape already exists, not downloading"); @@ -261,8 +261,8 @@ exp.get_image_hash = function(rid, userId, type, callback) { exp.get_avatar = function(rid, userId, helm, size, callback) { exp.get_image_hash(rid, userId, "skin", function(err, status, skin_hash) { if (skin_hash) { - var facepath = path.join(__dirname, "..", config.directories.faces, skin_hash + ".png"); - var helmpath = path.join(__dirname, "..", config.directories.helms, skin_hash + ".png"); + var facepath = path.join(config.directories.faces, skin_hash + ".png"); + var helmpath = path.join(config.directories.helms, skin_hash + ".png"); var filepath = facepath; fs.exists(helmpath, function(exists) { if (helm && exists) { @@ -288,7 +288,7 @@ exp.get_avatar = function(rid, userId, helm, size, callback) { exp.get_skin = function(rid, userId, callback) { exp.get_image_hash(rid, userId, "skin", function(err, status, skin_hash) { if (skin_hash) { - var skinpath = path.join(__dirname, "..", config.directories.skins, skin_hash + ".png"); + var skinpath = path.join(config.directories.skins, skin_hash + ".png"); fs.exists(skinpath, function(exists) { if (exists) { logging.debug(rid, "skin already exists, not downloading"); @@ -323,7 +323,7 @@ exp.get_render = function(rid, userId, scale, helm, body, callback) { callback(err, status, skin_hash, null); return; } - var renderpath = path.join(__dirname, "..", config.directories.renders, [skin_hash, scale, get_type(helm, body)].join("-") + ".png"); + var renderpath = path.join(config.directories.renders, [skin_hash, scale, get_type(helm, body)].join("-") + ".png"); fs.exists(renderpath, function(exists) { if (exists) { renders.open_render(rid, renderpath, function(render_err, rendered_img) { @@ -362,7 +362,7 @@ exp.get_cape = function(rid, userId, callback) { callback(err, null, status, null); return; } - var capepath = path.join(__dirname, "..", config.directories.capes, cape_hash + ".png"); + var capepath = path.join(config.directories.capes, cape_hash + ".png"); fs.exists(capepath, function(exists) { if (exists) { logging.debug(rid, "cape already exists, not downloading"); From 3156423209cca3b369ff780eeb4b72f6b67e4442 Mon Sep 17 00:00:00 2001 From: jomo Date: Sat, 22 Aug 2015 18:30:29 +0200 Subject: [PATCH 12/86] fix typo --- config.example.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.example.js b/config.example.js index 265f17c..356cead 100644 --- a/config.example.js +++ b/config.example.js @@ -13,7 +13,7 @@ var config = { interval: 1800, // interval seconds to check limits disk_limit: 10240, // min allowed free KB on disk to trigger image deletion redis_limit: 24576, // max allowed used KB on redis to trigger redis flush - amount: 50000 // amount of skins for which all iamge types are deleted + amount: 50000 // amount of skins for which all image types are deleted }, directories: { faces: "/var/lib/crafatar/images/faces/", // directory where faces are kept. must have trailing "/" From ac9cd93c8e33599ad36e42ca6f0193be7752f276 Mon Sep 17 00:00:00 2001 From: jomo Date: Sat, 29 Aug 2015 22:42:02 +0200 Subject: [PATCH 13/86] 512MB free disk space is better than 10MB --- config.example.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.example.js b/config.example.js index 356cead..8b04a8e 100644 --- a/config.example.js +++ b/config.example.js @@ -11,7 +11,7 @@ var config = { }, cleaner: { interval: 1800, // interval seconds to check limits - disk_limit: 10240, // min allowed free KB on disk to trigger image deletion + disk_limit: 524288, // min allowed free KB on disk to trigger image deletion redis_limit: 24576, // max allowed used KB on redis to trigger redis flush amount: 50000 // amount of skins for which all image types are deleted }, From e56f300d3e2a6442e3f52068839b5bcce1ac5e35 Mon Sep 17 00:00:00 2001 From: jomo Date: Sun, 30 Aug 2015 02:11:35 +0200 Subject: [PATCH 14/86] use EPHEMERAL_STORAGE instead of HEROKU env afaik you can have persistent storage on heroku, at least via addons and heroku is probably not the only environment where one has a temporary file system --- app.json | 2 +- lib/cache.js | 23 +++++++++++------------ 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/app.json b/app.json index 40bd9ac..f1cde27 100644 --- a/app.json +++ b/app.json @@ -10,7 +10,7 @@ ], "website": "https://crafatar.com/", "env": { - "HEROKU": "true", + "EPHEMERAL_STORAGE": "true", "BUILDPACK_URL": "https://github.com/mojodna/heroku-buildpack-multi.git#build-env" }, "addons": [ diff --git a/lib/cache.js b/lib/cache.js index 8527300..e8b7d72 100644 --- a/lib/cache.js +++ b/lib/cache.js @@ -8,11 +8,11 @@ var fs = require("fs"); var redis = null; // sets up redis connection -// flushes redis when running on heroku (files aren't kept between pushes) +// flushes redis when using ephemeral storage (e.g. Heroku) function connect_redis() { logging.log("connecting to redis..."); // parse redis env - var redis_env = (process.env.REDISCLOUD_URL || process.env.REDIS_URL); + var redis_env = process.env.REDISCLOUD_URL || process.env.REDIS_URL; var redis_url = redis_env ? url.parse(redis_env) : {}; redis_url.port = redis_url.port || 6379; redis_url.hostname = redis_url.hostname || "localhost"; @@ -23,15 +23,15 @@ function connect_redis() { } redis.on("ready", function() { logging.log("Redis connection established."); - if (process.env.HEROKU) { - logging.log("Running on heroku, flushing redis"); + if (process.env.EPHEMERAL_STORAGE) { + logging.log("Storage is ephemeral, flushing redis"); redis.flushall(); } }); - redis.on("error", function (err) { + redis.on("error", function(err) { logging.error(err); }); - redis.on("end", function () { + redis.on("end", function() { logging.warn("Redis connection lost!"); }); } @@ -67,15 +67,14 @@ exp.get_redis = function() { // updates the redis instance's server_info object // callback: error, info object exp.info = function(callback) { - redis.info(function (err, res) { - + redis.info(function(err, res) { // parse the info command and store it in redis.server_info // this code block was taken from mranney/node_redis#on_info_cmd // http://git.io/LBUNbg var lines = res.toString().split("\r\n"); var obj = {}; - lines.forEach(function (line) { + lines.forEach(function(line) { var parts = line.split(":"); if (parts[1]) { obj[parts[0]] = parts[1]; @@ -99,7 +98,7 @@ exp.info = function(callback) { // callback: error exp.update_timestamp = function(rid, userId, hash, temp, callback) { logging.debug(rid, "updating cache timestamp"); - var sub = temp ? (config.caching.local - 60) : 0; + var sub = temp ? config.caching.local - 60 : 0; var time = Date.now() - sub; // store userId in lower case if not null userId = userId && userId.toLowerCase(); @@ -117,8 +116,8 @@ exp.save_hash = function(rid, userId, skin_hash, cape_hash, callback) { logging.debug(rid, "caching skin:" + skin_hash + " cape:" + cape_hash); var time = Date.now(); // store shorter null byte instead of "null" - skin_hash = (skin_hash === null ? "" : skin_hash); - cape_hash = (cape_hash === null ? "" : cape_hash); + skin_hash = skin_hash === null ? "" : skin_hash; + cape_hash = cape_hash === null ? "" : cape_hash; // store userId in lower case if not null userId = userId && userId.toLowerCase(); if (skin_hash === undefined) { From 6746459c8d3525246daa46c74161e131f5cb4b55 Mon Sep 17 00:00:00 2001 From: jomo Date: Sun, 30 Aug 2015 03:57:29 +0200 Subject: [PATCH 15/86] delete cape images when cleaning --- lib/cleaner.js | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/lib/cleaner.js b/lib/cleaner.js index 443f2ba..26f0122 100644 --- a/lib/cleaner.js +++ b/lib/cleaner.js @@ -39,7 +39,7 @@ function should_clean_disk(callback) { prefixMultiplier: "KiB", isDisplayPrefixMultiplier: false, precision: 2 - }, function (err, response) { + }, function(err, response) { if (err) { callback(err, false); } else { @@ -71,28 +71,39 @@ exp.run = function() { logging.error(err); } else if (clean) { logging.warn("DiskCleaner: Disk limit reached! Cleaning images now"); + var skinsdir = config.directories.skins; + var capesdir = config.directories.capes; var facesdir = config.directories.faces; - var helmdir = config.directories.helms; - var renderdir = config.directories.renders; - var skindir = config.directories.skins; - fs.readdir(facesdir, function (readerr, files) { + var helmsdir = config.directories.helms; + var rendersdir = config.directories.renders; + fs.readdir(skinsdir, function(readerr, files) { if (!readerr) { for (var i = 0, l = Math.min(files.length, config.cleaner.amount); i < l; i++) { var filename = files[i]; if (filename[0] !== ".") { fs.unlink(path.join(facesdir, filename), nil); - fs.unlink(path.join(helmdir, filename), nil); - fs.unlink(path.join(skindir, filename), nil); + fs.unlink(path.join(helmsdir, filename), nil); + fs.unlink(path.join(skinsdir, filename), nil); } } } }); - fs.readdir(renderdir, function (readerr, files) { + fs.readdir(rendersdir, function(readerr, files) { if (!readerr) { for (var j = 0, l = Math.min(files.length, config.cleaner.amount); j < l; j++) { var filename = files[j]; if (filename[0] !== ".") { - fs.unlink(renderdir + filename, nil); + fs.unlink(rendersdir + filename, nil); + } + } + } + }); + fs.readdir(capesdir, function(readerr, files) { + if (!readerr) { + for (var j = 0, l = Math.min(files.length, config.cleaner.amount); j < l; j++) { + var filename = files[j]; + if (filename[0] !== ".") { + fs.unlink(capesdir + filename, nil); } } } From 5f703eda709da8397383907176edf1c95a4a1837 Mon Sep 17 00:00:00 2001 From: jomo Date: Sun, 30 Aug 2015 03:58:57 +0200 Subject: [PATCH 16/86] check disk/redis every 10 minutes --- config.example.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.example.js b/config.example.js index 8b04a8e..770bdec 100644 --- a/config.example.js +++ b/config.example.js @@ -10,7 +10,7 @@ var config = { default_scale: 6 // for 3D rendered skins; scale to be used when no scale given }, cleaner: { - interval: 1800, // interval seconds to check limits + interval: 600, // interval seconds to check limits disk_limit: 524288, // min allowed free KB on disk to trigger image deletion redis_limit: 24576, // max allowed used KB on redis to trigger redis flush amount: 50000 // amount of skins for which all image types are deleted From 78f2f2027f2c53291bcc9ae461fa2a261260ef08 Mon Sep 17 00:00:00 2001 From: jomo Date: Sun, 30 Aug 2015 04:29:56 +0200 Subject: [PATCH 17/86] cleaner: be less verbose --- lib/cleaner.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/lib/cleaner.js b/lib/cleaner.js index 26f0122..67bc741 100644 --- a/lib/cleaner.js +++ b/lib/cleaner.js @@ -20,10 +20,11 @@ function should_clean_redis(callback) { } else { try { // logging.debug(info.toString()); - logging.debug("used mem:" + info.used_memory); var used = parseInt(info.used_memory) / 1024; - logging.log("RedisCleaner:", used + "KB used"); - callback(err, used >= config.cleaner.redis_limit); + var result = used >= config.cleaner.redis_limit; + var msg = "RedisCleaner: " + used + "KB used"; + (result ? logging.log : logging.debug)(msg); + callback(err, result); } catch(e) { callback(e, false); } @@ -44,8 +45,10 @@ function should_clean_disk(callback) { callback(err, false); } else { var available = response[0].available; - logging.log("DiskCleaner:", available + "KB available"); - callback(err, available < config.cleaner.disk_limit); + var result = available < config.cleaner.disk_limit; + var msg = "DiskCleaner: " + available + "KB available"; + (result ? logging.log : logging.debug)(msg); + callback(err, result); } }); } @@ -61,7 +64,7 @@ exp.run = function() { logging.warn("RedisCleaner: Redis limit reached! flushing now"); redis.flushall(); } else { - logging.log("RedisCleaner: Nothing to clean"); + logging.debug("RedisCleaner: Nothing to clean"); } }); @@ -109,7 +112,7 @@ exp.run = function() { } }); } else { - logging.log("DiskCleaner: Nothing to clean"); + logging.debug("DiskCleaner: Nothing to clean"); } }); }; From 755cc74170d3640f4004cb2be582856a9e3936fd Mon Sep 17 00:00:00 2001 From: jomo Date: Sun, 30 Aug 2015 04:48:50 +0200 Subject: [PATCH 18/86] don't update file dates this was originally implemented because we wanted to delete the oldest images on disk where 'oldest' means not *used* for the longest time that's not useful and was never actually implemented, so we don't need this --- lib/cache.js | 25 ++----------------------- lib/helpers.js | 6 +++--- test/test.js | 8 -------- 3 files changed, 5 insertions(+), 34 deletions(-) diff --git a/lib/cache.js b/lib/cache.js index e8b7d72..0c04c67 100644 --- a/lib/cache.js +++ b/lib/cache.js @@ -36,26 +36,6 @@ function connect_redis() { }); } -// sets the date of the face file belonging to +skin_hash+ to now -// the helms file is ignored because we only need 1 file to read/write from -function update_file_date(rid, skin_hash) { - if (skin_hash) { - var face_path = path.join(config.directories.faces, skin_hash + ".png"); - fs.exists(face_path, function(exists) { - if (exists) { - var date = new Date(); - fs.utimes(face_path, date, date, function(err) { - if (err) { - logging.error(rid, "Error:", err.stack); - } - }); - } else { - logging.error(rid, "tried to update", face_path + " date, but it does not exist"); - } - }); - } -} - var exp = {}; // returns the redis instance @@ -92,11 +72,11 @@ exp.info = function(callback) { }); }; -// sets the timestamp for +userId+ and its face file's (+hash+) date to the current time +// sets the timestamp for +userId+ // if +temp+ is true, the timestamp is set so that the record will be outdated after 60 seconds // these 60 seconds match the duration of Mojang's rate limit ban // callback: error -exp.update_timestamp = function(rid, userId, hash, temp, callback) { +exp.update_timestamp = function(rid, userId, temp, callback) { logging.debug(rid, "updating cache timestamp"); var sub = temp ? config.caching.local - 60 : 0; var time = Date.now() - sub; @@ -105,7 +85,6 @@ exp.update_timestamp = function(rid, userId, hash, temp, callback) { redis.hmset(userId, "t", time, function(err) { callback(err); }); - update_file_date(rid, hash); }; // create the key +userId+, store +skin_hash+, +cape_hash+ and time diff --git a/lib/helpers.js b/lib/helpers.js index c76a319..04f9d07 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -25,7 +25,7 @@ function store_skin(rid, userId, profile, cache_details, callback) { if (!err && url) { var skin_hash = get_hash(url); if (cache_details && cache_details.skin === skin_hash) { - cache.update_timestamp(rid, userId, skin_hash, false, function(cache_err) { + cache.update_timestamp(rid, userId, false, function(cache_err) { callback(cache_err, skin_hash); }); } else { @@ -82,7 +82,7 @@ function store_cape(rid, userId, profile, cache_details, callback) { if (!err && url) { var cape_hash = get_hash(url); if (cache_details && cache_details.cape === cape_hash) { - cache.update_timestamp(rid, userId, cape_hash, false, function(cache_err) { + cache.update_timestamp(rid, userId, false, function(cache_err) { callback(cache_err, cape_hash); }); } else { @@ -238,7 +238,7 @@ exp.get_image_hash = function(rid, userId, type, callback) { if (store_err) { // we might have a cached hash although an error occured // (e.g. Mojang servers not reachable, using outdated hash) - cache.update_timestamp(rid, userId, cached_hash, true, function(err2) { + cache.update_timestamp(rid, userId, true, function(err2) { callback(err2 || store_err, -1, cache_details && cached_hash); }); } else { diff --git a/test/test.js b/test/test.js index 8bfd808..ad23e94 100644 --- a/test/test.js +++ b/test/test.js @@ -204,14 +204,6 @@ describe("Crafatar", function() { }); }); }); - it("should ignore file updates on invalid files", function(done) { - assert.doesNotThrow(function() { - cache.update_timestamp(rid, "0123456789abcdef0123456789abcdef", "invalid-file.png", false, function(err) { - assert.ifError(err); - done(); - }); - }); - }); it("should not find the file", function(done) { skins.open_skin(rid, "non/existent/path", function(err, img) { assert(err); From 2e738f8b40eab6d1e462b1fcb30e766db4fcc8f7 Mon Sep 17 00:00:00 2001 From: jomo Date: Sun, 30 Aug 2015 05:42:28 +0200 Subject: [PATCH 19/86] use crafatar/node-df until adriano-di-giovanni/node-df#3 is merged fixes #4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4d7d2fc..e725965 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "jade": "~1.11.0", "lwip": "~0.0.7", "mime": "~1.3.4", - "node-df": "~0.1.1", + "node-df": "crafatar/node-df", "redis": "~0.12.1", "request": "~2.60.0", "toobusy-js": "~0.4.2" From 52098ca2f8b9aae157a834afb7d3e689dd7aa3bd Mon Sep 17 00:00:00 2001 From: jomo Date: Sun, 30 Aug 2015 22:12:43 +0200 Subject: [PATCH 20/86] hotfix for #139 --- lib/cleaner.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/cleaner.js b/lib/cleaner.js index 67bc741..8c29a47 100644 --- a/lib/cleaner.js +++ b/lib/cleaner.js @@ -74,6 +74,12 @@ exp.run = function() { logging.error(err); } else if (clean) { logging.warn("DiskCleaner: Disk limit reached! Cleaning images now"); + + // hotfix for #139 | FIXME + logging.warn("DiskCleaner: Flushing Redis to prevent ENOENT"); + redis.flushall(); + // end hotfix + var skinsdir = config.directories.skins; var capesdir = config.directories.capes; var facesdir = config.directories.faces; From 5aaf075f949c6b22a3d1301ef01979ee4de6a69d Mon Sep 17 00:00:00 2001 From: jomo Date: Sun, 30 Aug 2015 22:21:01 +0200 Subject: [PATCH 21/86] travis :poop: --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 290c5e5..0562800 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,6 +11,8 @@ addons: - libgif-dev - build-essential - g++ +before_deploy: + - mkdir -pv /var/lib/crafatar/{faces,helms,skins,renders,capes} script: - npm run-script test-travis notifications: From 5f0e16897d61d141c065acad0e29a3f18ddc51cf Mon Sep 17 00:00:00 2001 From: jomo Date: Sun, 30 Aug 2015 22:24:35 +0200 Subject: [PATCH 22/86] =?UTF-8?q?travis=20:poop:=20N=C2=B02?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 0562800..7595c21 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,7 @@ addons: - libgif-dev - build-essential - g++ -before_deploy: +before_install: - mkdir -pv /var/lib/crafatar/{faces,helms,skins,renders,capes} script: - npm run-script test-travis From ccc7314ea0c609ec100f82e3fb986503334b6ad5 Mon Sep 17 00:00:00 2001 From: jomo Date: Sun, 30 Aug 2015 23:19:45 +0200 Subject: [PATCH 23/86] fix 'Permission denied' on travis --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 7595c21..77f0bf4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,7 +12,8 @@ addons: - build-essential - g++ before_install: - - mkdir -pv /var/lib/crafatar/{faces,helms,skins,renders,capes} + - mkdir -pv images/{faces,helms,skins,renders,capes} + - sed -i.bak 's/\/var\/lib\/crafatar\///g' config.example.js script: - npm run-script test-travis notifications: From 3a61e15abfc77db3061bd913135069b57015c087 Mon Sep 17 00:00:00 2001 From: jomo Date: Mon, 31 Aug 2015 00:10:35 +0200 Subject: [PATCH 24/86] various networking.js improvements - cleaned up some messy if/else code, replaced with nicely readable switch/case - catch JSON.parse errors --- lib/networking.js | 59 ++++++++++++++++++++++++++++++++--------------- test/test.js | 2 +- 2 files changed, 41 insertions(+), 20 deletions(-) diff --git a/lib/networking.js b/lib/networking.js index 8c3f662..0d728ae 100644 --- a/lib/networking.js +++ b/lib/networking.js @@ -69,7 +69,7 @@ exp.get_from_options = function(rid, url, options, callback) { }, timeout: config.server.http_timeout, followRedirect: false, - encoding: (options.encoding || null), + encoding: options.encoding || null, }, function(error, response, body) { // log url + code + description var code = response && response.statusCode; @@ -80,24 +80,34 @@ exp.get_from_options = function(rid, url, options, callback) { logfunc(rid, url, code, http_code[code]); } - // 200 or 301 depending on content type - if (!error && (code === 200 || code === 301)) { - // response received successfully - callback(body, response, null); - } else if (error) { - callback(body || null, response, error); - } else if (code === 404 || code === 204) { - // page does not exist - callback(null, response, null); - } else if (code === 429) { - // Too Many Requests exception - code 429 - // cause error so the image will not be cached - callback(body || null, response, (error || "TooManyRequests")); - } else { - // Probably 500 or the likes - logging.error(rid, "Unexpected response:", code, body); - callback(body || null, response, error); + + switch (code) { + case 200: + case 301: + case 302: // never seen, but mojang might use it in future + case 307: // never seen, but mojang might use it in future + case 308: // never seen, but mojang might use it in future + // these are okay + break; + case 404: + case 204: + // we don't want to cache this + body = null; + break; + case 429: + // this shouldn't usually happen, but occasionally does + // forcing error so it's not cached + error = error || "TooManyRequestsException"; + break; + default: + if (!error) { + // Probably 500 or the likes + logging.error(rid, "Unexpected response:", code, body); + } + break; } + + callback(body, response, error); }); }; @@ -144,7 +154,18 @@ exp.get_profile = function(rid, uuid, callback) { callback(null, null); } else { exp.get_from_options(rid, session_url + uuid, { encoding: "utf8" }, function(body, response, err) { - callback(err || null, (body !== null ? JSON.parse(body) : null)); + try { + body = body ? JSON.parse(body) : null; + callback(err || null, body); + } catch(e) { + if (e instanceof SyntaxError) { + logging.warn(rid, "Failed to parse JSON", e); + logging.debug(rid, body); + callback(err || null, null); + } else { + throw e; + } + } }); } }; diff --git a/test/test.js b/test/test.js index ad23e94..54b4160 100644 --- a/test/test.js +++ b/test/test.js @@ -1013,7 +1013,7 @@ describe("Crafatar", function() { it("uuid should be rate limited", function(done) { networking.get_profile(rid, id, function() { networking.get_profile(rid, id, function(err, profile) { - assert.strictEqual(err, "TooManyRequests"); + assert.strictEqual(err, "TooManyRequestsException"); assert.strictEqual(profile.error, "TooManyRequestsException"); done(); }); From 841eb39f05bc5b212e080599285a8efe58d2fe10 Mon Sep 17 00:00:00 2001 From: jomo Date: Mon, 31 Aug 2015 05:41:18 +0200 Subject: [PATCH 25/86] remove obsolete imports --- lib/cache.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/cache.js b/lib/cache.js index 0c04c67..61310c1 100644 --- a/lib/cache.js +++ b/lib/cache.js @@ -1,9 +1,7 @@ var logging = require("./logging"); var node_redis = require("redis"); var config = require("../config"); -var path = require("path"); var url = require("url"); -var fs = require("fs"); var redis = null; From b4ae89832a209db221437b825251ce576b01062c Mon Sep 17 00:00:00 2001 From: jomo Date: Wed, 2 Sep 2015 23:22:52 +0200 Subject: [PATCH 26/86] add missing 'cache' import in routes/skins --- lib/routes/skins.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/routes/skins.js b/lib/routes/skins.js index 3a245fa..b6173fb 100644 --- a/lib/routes/skins.js +++ b/lib/routes/skins.js @@ -1,6 +1,7 @@ var logging = require("../logging"); var helpers = require("../helpers"); var skins = require("../skins"); +var cache = require("../cache"); var path = require("path"); var lwip = require("lwip"); var url = require("url"); From 90022d9e6c212654213e9bda66cb8de8832f690d Mon Sep 17 00:00:00 2001 From: jomo Date: Wed, 2 Sep 2015 23:47:53 +0200 Subject: [PATCH 27/86] revert 4d949362beeaf432201f576d83518dd0bb0351a8 --- .travis.yml | 3 --- config.example.js | 10 +++++----- package.json | 2 +- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index 77f0bf4..290c5e5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,9 +11,6 @@ addons: - libgif-dev - build-essential - g++ -before_install: - - mkdir -pv images/{faces,helms,skins,renders,capes} - - sed -i.bak 's/\/var\/lib\/crafatar\///g' config.example.js script: - npm run-script test-travis notifications: diff --git a/config.example.js b/config.example.js index 770bdec..4e24d50 100644 --- a/config.example.js +++ b/config.example.js @@ -16,11 +16,11 @@ var config = { amount: 50000 // amount of skins for which all image types are deleted }, directories: { - faces: "/var/lib/crafatar/images/faces/", // directory where faces are kept. must have trailing "/" - helms: "/var/lib/crafatar/images/helms/", // directory where helms are kept. must have trailing "/" - skins: "/var/lib/crafatar/images/skins/", // directory where skins are kept. must have trailing "/" - renders: "/var/lib/crafatar/images/renders/", // directory where rendered skins are kept. must have trailing "/" - capes: "/var/lib/crafatar/images/capes/" // directory where capes are kept. must have trailing "/" + faces: "./images/faces/", // directory where faces are kept. must have trailing "/" + helms: "./images/helms/", // directory where helms are kept. must have trailing "/" + skins: "./images/skins/", // directory where skins are kept. must have trailing "/" + renders: "./images/renders/", // directory where rendered skins are kept. must have trailing "/" + capes: "./images/capes/" // directory where capes are kept. must have trailing "/" }, caching: { local: 1200, // seconds until we will check if user's skin changed. should be > 60 to comply with Mojang's rate limit diff --git a/package.json b/package.json index e725965..2d7001c 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "avatar" ], "scripts": { - "postinstall": "cp 'config.example.js' 'config.js'", + "postinstall": "mkdir -pv images/{faces,helms,skins,renders,capes} && cp 'config.example.js' 'config.js'", "start": "node www.js", "test": "mocha", "test-travis": "istanbul cover ./node_modules/mocha/bin/_mocha --report lcovonly -- -R spec && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage" From 47a978df6c00c29dccbf06e042b0dc9bd19427c9 Mon Sep 17 00:00:00 2001 From: jomo Date: Wed, 2 Sep 2015 23:57:08 +0200 Subject: [PATCH 28/86] add image dir to git --- .gitignore | 2 +- images/capes/.gitkeep | 0 images/faces/.gitkeep | 0 images/helms/.gitkeep | 0 images/renders/.gitkeep | 0 images/skins/.gitkeep | 0 package.json | 2 +- 7 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 images/capes/.gitkeep create mode 100644 images/faces/.gitkeep create mode 100644 images/helms/.gitkeep create mode 100644 images/renders/.gitkeep create mode 100644 images/skins/.gitkeep diff --git a/.gitignore b/.gitignore index 1c01a67..533683f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -images/ +images/*/*.png node_modules/ coverage/ .DS_Store diff --git a/images/capes/.gitkeep b/images/capes/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/images/faces/.gitkeep b/images/faces/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/images/helms/.gitkeep b/images/helms/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/images/renders/.gitkeep b/images/renders/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/images/skins/.gitkeep b/images/skins/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/package.json b/package.json index 2d7001c..e725965 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "avatar" ], "scripts": { - "postinstall": "mkdir -pv images/{faces,helms,skins,renders,capes} && cp 'config.example.js' 'config.js'", + "postinstall": "cp 'config.example.js' 'config.js'", "start": "node www.js", "test": "mocha", "test-travis": "istanbul cover ./node_modules/mocha/bin/_mocha --report lcovonly -- -R spec && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage" From fe12901f410596b459ce87ee969f3e24e47c390b Mon Sep 17 00:00:00 2001 From: jomo Date: Thu, 3 Sep 2015 00:26:25 +0200 Subject: [PATCH 29/86] update app.json Also fixed our description in package.json --- app.json | 19 +++++++++++++++---- package.json | 2 +- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/app.json b/app.json index f1cde27..939ca44 100644 --- a/app.json +++ b/app.json @@ -1,6 +1,6 @@ { "name": "Crafatar", - "description": "A Minecraft Avatar API written in NodeJS", + "description": "A blazing fast API for Minecraft faces!", "repository": "https://github.com/crafatar/crafatar", "keywords": [ "node", @@ -10,10 +10,21 @@ ], "website": "https://crafatar.com/", "env": { - "EPHEMERAL_STORAGE": "true", - "BUILDPACK_URL": "https://github.com/mojodna/heroku-buildpack-multi.git#build-env" + "EPHEMERAL_STORAGE": { + "description": "Set to true if your storage is gone after deploying", + "required": false, + "value": true + } }, "addons": [ "rediscloud" + ], + "buildpacks": [ + { + "url": "https://github.com/mojodna/heroku-buildpack-cairo.git" + }, + { + "url": "https://github.com/heroku/heroku-buildpack-nodejs.git" + } ] -} +} \ No newline at end of file diff --git a/package.json b/package.json index e725965..88d7bcf 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "private": true, "author": "Jake0oo0", - "description": "A Minecraft avatar service with support for avatars, 1.8 skins, and even 3D renders!", + "description": "A blazing fast API for Minecraft faces!", "contributors": [ { "name": "jomo" From 6d12ed685b4bc43b0dac42da6c2dcad1420b3064 Mon Sep 17 00:00:00 2001 From: jomo Date: Sun, 6 Sep 2015 00:22:57 +0200 Subject: [PATCH 30/86] enhance renders by using binary transparency this is a temporary fix for #32. it doesn't solve the problem, but it makes the renders much less worse. in combination with #134 this will hopefully lead to fixing the problem entirely --- lib/renders.js | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/lib/renders.js b/lib/renders.js index 470006d..2d0cc26 100644 --- a/lib/renders.js +++ b/lib/renders.js @@ -24,6 +24,18 @@ function scale_image(imageData, context, d_x, d_y, scale) { } } +// makes images less worse by using binary transparency +function enhance(context) { + var imagedata = context.getImageData(0, 0, context.canvas.width, context.canvas.height); + var data = imagedata.data; + // data is [r,g,b,a, r,g,b,a, *] + for (var i = 3; i < data.length; i += 4) { + // round to 0 or 255 + data[i] = Math.round(data[i] / 255) * 255; + } + context.putImageData(imagedata, 0, 0); +} + // draws the helmet on to the +skin_canvas+ // using the skin from the +model_ctx+ at the +scale+ exp.draw_helmet = function(skin_canvas, model_ctx, scale) { @@ -155,7 +167,7 @@ exp.draw_model = function(rid, 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); @@ -173,7 +185,10 @@ exp.draw_model = function(rid, img, scale, helm, body, callback) { exp.draw_helmet(skin_canvas, model_ctx, scale); } - model_canvas.toBuffer(function(err, buf){ + // FIXME: This is a temporary fix for #32 + enhance(model_ctx); + + model_canvas.toBuffer(function(err, buf) { if (err) { logging.error(rid, "error creating buffer:", err); } @@ -187,7 +202,7 @@ exp.draw_model = function(rid, img, scale, helm, body, callback) { // helper method to open a render from +renderpath+ // callback: error, image buffer exp.open_render = function(rid, renderpath, callback) { - fs.readFile(renderpath, function (err, buf) { + fs.readFile(renderpath, function(err, buf) { if (err) { logging.error(rid, "error while opening skin file:", err); } From 6a630f23b9a68667a27d75f906aea072bfdafdf1 Mon Sep 17 00:00:00 2001 From: jomo Date: Sun, 6 Sep 2015 00:47:17 +0200 Subject: [PATCH 31/86] add new test CRCs for @6d12ed6 --- test/test.js | 48 ++++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/test/test.js b/test/test.js index 54b4160..2f56c29 100644 --- a/test/test.js +++ b/test/test.js @@ -540,17 +540,17 @@ describe("Crafatar", function() { "head render with existing username": { url: "http://localhost:3000/renders/head/jeb_?scale=2", etag: '"a846b82963"', - crc32: [353633671, 370672768] + crc32: [1743362302, 208074514] }, "head render with non-existent username": { url: "http://localhost:3000/renders/head/0?scale=2", etag: '"steve"', - crc32: [883439147, 433083528] + crc32: [897270661, 1026982335] }, "head render with non-existent username defaulting to alex": { url: "http://localhost:3000/renders/head/0?scale=2&default=alex", etag: '"alex"', - crc32: [1240086237, 1108800327] + crc32: [2357619670, 3172866498] }, "head render with non-existent username defaulting to username": { url: "http://localhost:3000/avatars/0?scale=2&default=jeb_", @@ -570,17 +570,17 @@ describe("Crafatar", function() { "helm head render with existing username": { url: "http://localhost:3000/renders/head/jeb_?scale=2&helm", etag: '"a846b82963"', - crc32: [3456497067, 3490318764] + crc32: [4178514320, 2340078566] }, "helm head render with non-existent username": { url: "http://localhost:3000/renders/head/0?scale=2&helm", etag: '"steve"', - crc32: [1858563554, 2647471936] + crc32: [507497693, 3868868707] }, "helm head render with non-existent username defaulting to alex": { url: "http://localhost:3000/renders/head/0?scale=2&helm&default=alex", etag: '"alex"', - crc32: [2963161105, 1769904825] + crc32: [891113664, 1785326216] }, "helm head render with non-existent username defaulting to username": { url: "http://localhost:3000/renders/head/0?scale=2&helm&default=jeb_", @@ -600,17 +600,17 @@ describe("Crafatar", function() { "head render with existing uuid": { url: "http://localhost:3000/renders/head/853c80ef3c3749fdaa49938b674adae6?scale=2", etag: '"a846b82963"', - crc32: [353633671, 370672768] + crc32: [1743362302, 208074514] }, "head render with non-existent uuid": { url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2", etag: '"steve"', - crc32: [883439147, 433083528] + crc32: [897270661, 1026982335] }, "head render with non-existent uuid defaulting to alex": { url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&default=alex", etag: '"alex"', - crc32: [1240086237, 1108800327] + crc32: [2357619670, 3172866498] }, "head render with non-existent uuid defaulting to username": { url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&default=jeb_", @@ -630,17 +630,17 @@ describe("Crafatar", function() { "helm head render with existing uuid": { url: "http://localhost:3000/renders/head/853c80ef3c3749fdaa49938b674adae6?scale=2&helm", etag: '"a846b82963"', - crc32: [3456497067, 3490318764] + crc32: [4178514320, 2340078566] }, "helm head render with non-existent uuid": { url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&helm", etag: '"steve"', - crc32: [1858563554, 2647471936] + crc32: [507497693, 3868868707] }, "helm head render with non-existent uuid defaulting to alex": { url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&helm&default=alex", etag: '"alex"', - crc32: [2963161105, 1769904825] + crc32: [891113664, 1785326216] }, "helm head with non-existent uuid defaulting to username": { url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&helm&default=jeb_", @@ -660,17 +660,17 @@ describe("Crafatar", function() { "body render with existing username": { url: "http://localhost:3000/renders/body/jeb_?scale=2", etag: '"a846b82963"', - crc32: [1291941229, 2628108474] + crc32: [1023392610, 4127764743] }, "body render with non-existent username": { url: "http://localhost:3000/renders/body/0?scale=2", etag: '"steve"', - crc32: [2652947188, 2115706574] + crc32: [3559591930, 3663447404] }, "body render with non-existent username defaulting to alex": { url: "http://localhost:3000/renders/body/0?scale=2&default=alex", etag: '"alex"', - crc32: [407932087, 2516216042] + crc32: [470529151, 1823026927] }, "body render with non-existent username defaulting to username": { url: "http://localhost:3000/renders/body/0?scale=2&default=jeb_", @@ -690,17 +690,17 @@ describe("Crafatar", function() { "helm body render with existing username": { url: "http://localhost:3000/renders/body/jeb_?scale=2&helm", etag: '"a846b82963"', - crc32: [3556188297, 4269754007] + crc32: [3476579592, 97705180] }, "helm body render with non-existent username": { url: "http://localhost:3000/renders/body/0?scale=2&helm", etag: '"steve"', - crc32: [272191039, 542896675] + crc32: [3992841063, 1025743887] }, "helm body render with non-existent username defaulting to alex": { url: "http://localhost:3000/renders/body/0?scale=2&helm&default=alex", etag: '"alex"', - crc32: [737759773, 66512449] + crc32: [3317518715, 3621585514] }, "helm body render with non-existent username defaulting to username": { url: "http://localhost:3000/renders/body/0?scale=2&helm&default=jeb_", @@ -720,17 +720,17 @@ describe("Crafatar", function() { "body render with existing uuid": { url: "http://localhost:3000/renders/body/853c80ef3c3749fdaa49938b674adae6?scale=2", etag: '"a846b82963"', - crc32: [1291941229, 2628108474] + crc32: [1023392610, 4127764743] }, "body render with non-existent uuid": { url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2", etag: '"steve"', - crc32: [2652947188, 2115706574] + crc32: [3559591930, 3663447404] }, "body render with non-existent uuid defaulting to alex": { url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2&default=alex", etag: '"alex"', - crc32: [407932087, 2516216042] + crc32: [470529151, 1823026927] }, "body render with non-existent uuid defaulting to username": { url: "http://localhost:3000/renders/body/0?scale=2&default=jeb_", @@ -750,17 +750,17 @@ describe("Crafatar", function() { "helm body render with existing uuid": { url: "http://localhost:3000/renders/body/853c80ef3c3749fdaa49938b674adae6?scale=2&helm", etag: '"a846b82963"', - crc32: [3556188297, 4269754007] + crc32: [3476579592, 97705180] }, "helm body render with non-existent uuid": { url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2&helm", etag: '"steve"', - crc32: [272191039, 542896675] + crc32: [3992841063, 1025743887] }, "helm body render with non-existent uuid defaulting to alex": { url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2&helm&default=alex", etag: '"alex"', - crc32: [737759773, 66512449] + crc32: [3317518715, 3621585514] }, "helm body render with non-existent uuid defaulting to url": { url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2&helm&default=http%3A%2F%2Fexample.com", From 8ddc300a1189b9d27b18b8841235db008eb1dea3 Mon Sep 17 00:00:00 2001 From: jomo Date: Sun, 6 Sep 2015 03:39:48 +0200 Subject: [PATCH 32/86] #138 bump http_timeout to 2 seconds we run into the timeout quite frequently, even on a fast network. 2s should be long enough for mojang to reply --- config.example.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.example.js b/config.example.js index 4e24d50..df972f5 100644 --- a/config.example.js +++ b/config.example.js @@ -27,7 +27,7 @@ var config = { browser: 3600 // seconds until browser will request image again }, server: { - http_timeout: 1000, // ms until connection to Mojang is dropped + http_timeout: 2000, // ms until connection to Mojang is dropped debug_enabled: false, // enables logging.debug log_time: true // set to false if you use an external logger that provides timestamps } From 7e77142b29a82ff3edb294c81732319d2072f5c3 Mon Sep 17 00:00:00 2001 From: jomo Date: Tue, 15 Sep 2015 04:38:24 +0200 Subject: [PATCH 33/86] add link to crafatar/setup --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 69ef0c1..a4b6821 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,10 @@ Please [visit the website](https://crafatar.com) for details. * Open an [issue](https://github.com/crafatar/crafatar/issues/) on GitHub * You can [join IRC](https://webchat.esper.net/?channels=crafatar) in #crafatar on irc.esper.net. +# Installation + +Have a look at [crafatar/setup](https://github.com/crafatar/setup) to see how we set things up at Crafatar. + ## Installation on Heroku [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy) From 750d741308d49907c46d3cafcb46934f4d37dd1e Mon Sep 17 00:00:00 2001 From: jomo Date: Thu, 17 Sep 2015 01:51:11 +0200 Subject: [PATCH 34/86] log when cleaner has nothing to do --- lib/cleaner.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/cleaner.js b/lib/cleaner.js index 8c29a47..21c032d 100644 --- a/lib/cleaner.js +++ b/lib/cleaner.js @@ -64,7 +64,7 @@ exp.run = function() { logging.warn("RedisCleaner: Redis limit reached! flushing now"); redis.flushall(); } else { - logging.debug("RedisCleaner: Nothing to clean"); + logging.log("RedisCleaner: Nothing to clean"); } }); @@ -118,7 +118,7 @@ exp.run = function() { } }); } else { - logging.debug("DiskCleaner: Nothing to clean"); + logging.log("DiskCleaner: Nothing to clean"); } }); }; From b97087c099ea608e22962ac8a26366c59f642934 Mon Sep 17 00:00:00 2001 From: jomo Date: Sun, 20 Sep 2015 19:12:31 +0200 Subject: [PATCH 35/86] catch HTTP 500/503 and empty response, fixes #141 --- lib/networking.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/networking.js b/lib/networking.js index 0d728ae..0217d12 100644 --- a/lib/networking.js +++ b/lib/networking.js @@ -91,6 +91,8 @@ exp.get_from_options = function(rid, url, options, callback) { break; case 404: case 204: + case 500: + case 503: // we don't want to cache this body = null; break; @@ -107,6 +109,11 @@ exp.get_from_options = function(rid, url, options, callback) { break; } + if (body && !body.length) { + // empty response + body = null; + } + callback(body, response, error); }); }; From 26f6b089ef516ca54c6346a549c8ef0b09b476c3 Mon Sep 17 00:00:00 2001 From: jomo Date: Sun, 20 Sep 2015 20:49:37 +0200 Subject: [PATCH 36/86] log errors only once, fixes #140 also made sure some (network) errors have level 'WARN' these are printed without stacktrace --- lib/helpers.js | 8 +------- lib/networking.js | 10 +++------- lib/renders.js | 7 ------- lib/response.js | 16 ++++++++++++---- lib/routes/avatars.js | 3 --- lib/routes/capes.js | 1 - lib/routes/renders.js | 2 -- lib/routes/skins.js | 2 -- lib/skins.js | 1 - 9 files changed, 16 insertions(+), 34 deletions(-) diff --git a/lib/helpers.js b/lib/helpers.js index 04f9d07..23d5d48 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -44,12 +44,10 @@ function store_skin(rid, userId, profile, cache_details, callback) { } else { skins.save_image(img, skinpath, function(skin_err) { if (skin_err) { - logging.error(rid, skin_err); callback(skin_err, null); } else { skins.extract_face(img, facepath, function(err2) { if (err2) { - logging.error(rid, err2.stack); callback(err2, null); } else { logging.debug(rid, "face extracted"); @@ -95,7 +93,6 @@ function store_cape(rid, userId, profile, cache_details, callback) { } else { networking.get_from(rid, url, function(img, response, net_err) { if (net_err || !img) { - logging.error(rid, net_err.stack); callback(net_err, null); } else { skins.save_image(img, capepath, function(skin_err) { @@ -342,10 +339,7 @@ exp.get_render = function(rid, userId, scale, helm, body, callback) { callback(null, 0, skin_hash, null); } else { fs.writeFile(renderpath, drawn_img, "binary", function(fs_err) { - if (fs_err) { - logging.error(rid, fs_err.stack); - } - callback(null, 2, skin_hash, drawn_img); + callback(fs_err, 2, skin_hash, drawn_img); }); } }); diff --git a/lib/networking.js b/lib/networking.js index 0217d12..3c427c7 100644 --- a/lib/networking.js +++ b/lib/networking.js @@ -73,12 +73,9 @@ exp.get_from_options = function(rid, url, options, callback) { }, function(error, response, body) { // log url + code + description var code = response && response.statusCode; - if (error) { - logging.error(rid, url, error); - } else { - var logfunc = code && code < 405 ? logging.debug : logging.warn; - logfunc(rid, url, code, http_code[code]); - } + + var logfunc = code && code < 405 ? logging.debug : logging.warn; + logfunc(rid, url, code, http_code[code]); switch (code) { @@ -201,7 +198,6 @@ exp.save_texture = function(rid, tex_hash, outpath, callback) { var textureurl = textures_url + tex_hash; exp.get_from(rid, textureurl, function(img, response, err) { if (err) { - logging.error(rid, "error while downloading texture"); callback(err, response, null); } else { skins.save_image(img, outpath, function(img_err) { diff --git a/lib/renders.js b/lib/renders.js index 2d0cc26..af7940c 100644 --- a/lib/renders.js +++ b/lib/renders.js @@ -161,7 +161,6 @@ exp.draw_model = function(rid, img, scale, helm, body, callback) { var image = new Image(); image.onerror = function(err) { - logging.error(rid, "render error:", err.stack); callback(err, null); }; @@ -189,9 +188,6 @@ exp.draw_model = function(rid, img, scale, helm, body, callback) { enhance(model_ctx); model_canvas.toBuffer(function(err, buf) { - if (err) { - logging.error(rid, "error creating buffer:", err); - } callback(err, buf); }); }; @@ -203,9 +199,6 @@ exp.draw_model = function(rid, img, scale, helm, body, callback) { // callback: error, image buffer exp.open_render = function(rid, renderpath, callback) { fs.readFile(renderpath, function(err, buf) { - if (err) { - logging.error(rid, "error while opening skin file:", err); - } callback(err, buf); }); }; diff --git a/lib/response.js b/lib/response.js index 09a703a..8403ded 100644 --- a/lib/response.js +++ b/lib/response.js @@ -12,6 +12,9 @@ var human_status = { }; +// print these, but without stacktrace +var silent_errors = ["ETIMEDOUT", "ESOCKETTIMEDOUT", "ECONNRESET", "EHOSTUNREACH", "ECONNREFUSED"]; + // handles HTTP responses // +request+ a http.IncomingMessage // +response+ a http.ServerResponse @@ -23,7 +26,6 @@ var human_status = { // * hash: image hash, required when body is an image // * err: a possible Error module.exports = function(request, response, result) { - response.on("close", function() { logging.warn(request.id, "Connection closed"); }); @@ -38,7 +40,7 @@ module.exports = function(request, response, result) { // These headers are the same for every response var headers = { - "Content-Type": (result.body && result.type) || "text/plain", + "Content-Type": result.body && result.type || "text/plain", "Cache-Control": "max-age=" + config.caching.browser + ", public", "Response-Time": Date.now() - request.start, "X-Request-ID": request.id, @@ -46,8 +48,14 @@ module.exports = function(request, response, result) { }; if (result.err) { - logging.error(request.id, result.err); - logging.error(request.id, result.err.stack); + var silent = silent_errors.indexOf(result.err.code) !== -1; + if (result.err.stack && !silent) { + logging.error(request.id, result.err.stack); + } else if (silent) { + logging.warn(request.id, result.err); + } else { + logging.error(request.id, result.err); + } result.status = -1; } diff --git a/lib/routes/avatars.js b/lib/routes/avatars.js index 0d03a22..f8e9076 100644 --- a/lib/routes/avatars.js +++ b/lib/routes/avatars.js @@ -1,4 +1,3 @@ -var logging = require("../logging"); var helpers = require("../helpers"); var config = require("../../config"); var skins = require("../skins"); @@ -82,7 +81,6 @@ module.exports = function(req, callback) { try { helpers.get_avatar(req.id, userId, helm, size, function(err, status, image, hash) { if (err) { - logging.error(req.id, err); if (err.code === "ENOENT") { // no such file cache.remove_hash(req.id, userId); @@ -101,7 +99,6 @@ module.exports = function(req, callback) { } }); } catch (e) { - logging.error(req.id, "error:", e.stack); handle_default(-1, userId, size, def, req, e, callback); } }; \ No newline at end of file diff --git a/lib/routes/capes.js b/lib/routes/capes.js index c3f303d..8cb302b 100644 --- a/lib/routes/capes.js +++ b/lib/routes/capes.js @@ -32,7 +32,6 @@ module.exports = function(req, callback) { try { helpers.get_cape(rid, userId, function(err, hash, status, image) { if (err) { - logging.error(rid, err); if (err.code === "ENOENT") { // no such file cache.remove_hash(rid, userId); diff --git a/lib/routes/renders.js b/lib/routes/renders.js index aa3288d..edef93b 100644 --- a/lib/routes/renders.js +++ b/lib/routes/renders.js @@ -98,7 +98,6 @@ module.exports = function(req, callback) { try { helpers.get_render(rid, userId, scale, helm, body, function(err, status, hash, image) { if (err) { - logging.error(rid, err); if (err.code === "ENOENT") { // no such file cache.remove_hash(rid, userId); @@ -118,7 +117,6 @@ module.exports = function(req, callback) { } }); } catch(e) { - logging.error(rid, "error:", e.stack); handle_default(rid, scale, helm, body, -1, userId, scale, def, req, e, callback); } }; \ No newline at end of file diff --git a/lib/routes/skins.js b/lib/routes/skins.js index b6173fb..8b926ac 100644 --- a/lib/routes/skins.js +++ b/lib/routes/skins.js @@ -81,7 +81,6 @@ module.exports = function(req, callback) { try { helpers.get_skin(rid, userId, function(err, hash, status, image) { if (err) { - logging.error(req.id, err); if (err.code === "ENOENT") { // no such file cache.remove_hash(req.id, userId); @@ -100,7 +99,6 @@ module.exports = function(req, callback) { } }); } catch(e) { - logging.error(rid, "error:", e.stack); handle_default(-1, userId, def, req, e, callback); } }; \ No newline at end of file diff --git a/lib/skins.js b/lib/skins.js index a302443..4599d80 100644 --- a/lib/skins.js +++ b/lib/skins.js @@ -126,7 +126,6 @@ exp.default_skin = function(uuid) { exp.open_skin = function(rid, skinpath, callback) { fs.readFile(skinpath, function(err, buf) { if (err) { - logging.error(rid, "error while opening skin file:", err); callback(err, null); } else { callback(null, buf); From 9cdca6acdaad3af7068d542c983350f37a6572d4 Mon Sep 17 00:00:00 2001 From: jomo Date: Sun, 20 Sep 2015 21:28:43 +0200 Subject: [PATCH 37/86] don't throw strings --- lib/networking.js | 2 +- test/test.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/networking.js b/lib/networking.js index 3c427c7..80f43da 100644 --- a/lib/networking.js +++ b/lib/networking.js @@ -96,7 +96,7 @@ exp.get_from_options = function(rid, url, options, callback) { case 429: // this shouldn't usually happen, but occasionally does // forcing error so it's not cached - error = error || "TooManyRequestsException"; + error = error || new Error("TooManyRequestsException"); break; default: if (!error) { diff --git a/test/test.js b/test/test.js index 2f56c29..a77879f 100644 --- a/test/test.js +++ b/test/test.js @@ -1013,7 +1013,7 @@ describe("Crafatar", function() { it("uuid should be rate limited", function(done) { networking.get_profile(rid, id, function() { networking.get_profile(rid, id, function(err, profile) { - assert.strictEqual(err, "TooManyRequestsException"); + assert.strictEqual(err.message, "TooManyRequestsException"); assert.strictEqual(profile.error, "TooManyRequestsException"); done(); }); From 06895cdd81e1d139b474110a98844c0a4588f643 Mon Sep 17 00:00:00 2001 From: jomo Date: Sun, 20 Sep 2015 21:35:46 +0200 Subject: [PATCH 38/86] add TooManyRequestsException to silent_errors --- lib/response.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/response.js b/lib/response.js index 8403ded..143967c 100644 --- a/lib/response.js +++ b/lib/response.js @@ -13,7 +13,7 @@ var human_status = { // print these, but without stacktrace -var silent_errors = ["ETIMEDOUT", "ESOCKETTIMEDOUT", "ECONNRESET", "EHOSTUNREACH", "ECONNREFUSED"]; +var silent_errors = ["ETIMEDOUT", "ESOCKETTIMEDOUT", "ECONNRESET", "EHOSTUNREACH", "ECONNREFUSED", "TooManyRequestsException"]; // handles HTTP responses // +request+ a http.IncomingMessage From a15cb201443116e4a88661b6350af74830b72751 Mon Sep 17 00:00:00 2001 From: jomo Date: Sun, 20 Sep 2015 21:43:12 +0200 Subject: [PATCH 39/86] TooManyRequestsException shouldn't actually throw an error all other errors thrown here are network issues, this is not. --- lib/networking.js | 6 +----- lib/response.js | 2 +- test/test.js | 4 ++-- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/lib/networking.js b/lib/networking.js index 80f43da..c2f9253 100644 --- a/lib/networking.js +++ b/lib/networking.js @@ -88,16 +88,12 @@ exp.get_from_options = function(rid, url, options, callback) { break; case 404: case 204: + case 429: // this shouldn't usually happen, but occasionally does case 500: case 503: // we don't want to cache this body = null; break; - case 429: - // this shouldn't usually happen, but occasionally does - // forcing error so it's not cached - error = error || new Error("TooManyRequestsException"); - break; default: if (!error) { // Probably 500 or the likes diff --git a/lib/response.js b/lib/response.js index 143967c..8403ded 100644 --- a/lib/response.js +++ b/lib/response.js @@ -13,7 +13,7 @@ var human_status = { // print these, but without stacktrace -var silent_errors = ["ETIMEDOUT", "ESOCKETTIMEDOUT", "ECONNRESET", "EHOSTUNREACH", "ECONNREFUSED", "TooManyRequestsException"]; +var silent_errors = ["ETIMEDOUT", "ESOCKETTIMEDOUT", "ECONNRESET", "EHOSTUNREACH", "ECONNREFUSED"]; // handles HTTP responses // +request+ a http.IncomingMessage diff --git a/test/test.js b/test/test.js index a77879f..73c3bda 100644 --- a/test/test.js +++ b/test/test.js @@ -1013,8 +1013,8 @@ describe("Crafatar", function() { it("uuid should be rate limited", function(done) { networking.get_profile(rid, id, function() { networking.get_profile(rid, id, function(err, profile) { - assert.strictEqual(err.message, "TooManyRequestsException"); - assert.strictEqual(profile.error, "TooManyRequestsException"); + assert.ifError(err); + assert.strictEqual(profile, null); done(); }); }); From d49f7279b328573d098018282310e5e8caa496e6 Mon Sep 17 00:00:00 2001 From: jomo Date: Sun, 20 Sep 2015 21:45:37 +0200 Subject: [PATCH 40/86] log response ID first for access log also made sure 'headers' is defined before it's used --- lib/response.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/response.js b/lib/response.js index 8403ded..e78947b 100644 --- a/lib/response.js +++ b/lib/response.js @@ -26,18 +26,6 @@ var silent_errors = ["ETIMEDOUT", "ESOCKETTIMEDOUT", "ECONNRESET", "EHOSTUNREACH // * hash: image hash, required when body is an image // * err: a possible Error module.exports = function(request, response, result) { - response.on("close", function() { - logging.warn(request.id, "Connection closed"); - }); - - response.on("finish", function() { - logging.log(request.method, request.url.href, request.id, response.statusCode, headers["Response-Time"] + "ms", "(" + (human_status[result.status] || "-") + ")"); - }); - - response.on("error", function(err) { - logging.error(request.id, err); - }); - // These headers are the same for every response var headers = { "Content-Type": result.body && result.type || "text/plain", @@ -47,6 +35,18 @@ module.exports = function(request, response, result) { "Access-Control-Allow-Origin": "*" }; + response.on("close", function() { + logging.warn(request.id, "Connection closed"); + }); + + response.on("finish", function() { + logging.log(request.id, request.method, request.url.href, response.statusCode, headers["Response-Time"] + "ms", "(" + (human_status[result.status] || "-") + ")"); + }); + + response.on("error", function(err) { + logging.error(request.id, err); + }); + if (result.err) { var silent = silent_errors.indexOf(result.err.code) !== -1; if (result.err.stack && !silent) { From cc2840ae4b72290006858a0930a40f2760e48a92 Mon Sep 17 00:00:00 2001 From: jomo Date: Sun, 20 Sep 2015 22:02:37 +0200 Subject: [PATCH 41/86] log error if http code not available --- lib/networking.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/networking.js b/lib/networking.js index c2f9253..e34c8ae 100644 --- a/lib/networking.js +++ b/lib/networking.js @@ -75,8 +75,7 @@ exp.get_from_options = function(rid, url, options, callback) { var code = response && response.statusCode; var logfunc = code && code < 405 ? logging.debug : logging.warn; - logfunc(rid, url, code, http_code[code]); - + logfunc(rid, url, code || error && error.code, http_code[code]); switch (code) { case 200: From 1ecf3c01224ab5b93087bd86152d8eb16dbc581d Mon Sep 17 00:00:00 2001 From: jomo Date: Mon, 21 Sep 2015 22:21:14 +0200 Subject: [PATCH 42/86] add 504 to expected return codes, don't cache unexpected responses --- lib/networking.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/networking.js b/lib/networking.js index e34c8ae..ce7effc 100644 --- a/lib/networking.js +++ b/lib/networking.js @@ -90,6 +90,7 @@ exp.get_from_options = function(rid, url, options, callback) { case 429: // this shouldn't usually happen, but occasionally does case 500: case 503: + case 504: // we don't want to cache this body = null; break; @@ -98,6 +99,7 @@ exp.get_from_options = function(rid, url, options, callback) { // Probably 500 or the likes logging.error(rid, "Unexpected response:", code, body); } + body = null; break; } From 5e7d116364b8e8700a43d940e91f8af225e525ff Mon Sep 17 00:00:00 2001 From: jomo Date: Tue, 22 Sep 2015 21:37:55 +0200 Subject: [PATCH 43/86] readme IRC badge improvement the server is now in the badge label and the channel is shown on hover --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a4b6821..95388d8 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Crafatar [![travis](https://img.shields.io/travis/crafatar/crafatar/master.svg?style=flat-square)](https://travis-ci.org/crafatar/crafatar/) [![Coverage Status](https://img.shields.io/coveralls/crafatar/crafatar.svg?style=flat-square)](https://coveralls.io/r/crafatar/crafatar) [![Code Climate](https://img.shields.io/codeclimate/github/crafatar/crafatar.svg?style=flat-square)](https://codeclimate.com/github/crafatar/crafatar) -[![IRC: #crafatar](https://img.shields.io/badge/IRC-%23crafatar-blue.svg?style=flat-square)](https://webchat.esper.net/?channels=crafatar) [![dependency status](https://img.shields.io/david/crafatar/crafatar.svg?style=flat-square)](https://david-dm.org/crafatar/crafatar) [![devDependency status](https://img.shields.io/david/dev/crafatar/crafatar.svg?style=flat-square)](https://david-dm.org/crafatar/crafatar#info=devDependencies) [![docs status](https://inch-ci.org/github/crafatar/crafatar.svg?branch=master&style=flat-square)](https://inch-ci.org/github/crafatar/crafatar) +[![IRC: esper.net](https://img.shields.io/badge/IRC-esper.net-blue.svg?style=flat-square)](https://webchat.esper.net/?channels=crafatar "#crafatar") [![dependency status](https://img.shields.io/david/crafatar/crafatar.svg?style=flat-square)](https://david-dm.org/crafatar/crafatar) [![devDependency status](https://img.shields.io/david/dev/crafatar/crafatar.svg?style=flat-square)](https://david-dm.org/crafatar/crafatar#info=devDependencies) [![docs status](https://inch-ci.org/github/crafatar/crafatar.svg?branch=master&style=flat-square)](https://inch-ci.org/github/crafatar/crafatar) logo From 5e1e7f37011221a2912d1cf6d0334202a1f4394f Mon Sep 17 00:00:00 2001 From: jomo Date: Wed, 23 Sep 2015 00:29:50 +0200 Subject: [PATCH 44/86] update dependencies --- package.json | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 88d7bcf..5e3299e 100644 --- a/package.json +++ b/package.json @@ -2,12 +2,10 @@ "name": "crafatar", "version": "1.0.0", "private": true, - "author": "Jake0oo0", "description": "A blazing fast API for Minecraft faces!", "contributors": [ - { - "name": "jomo" - } + { "name": "jomo", "url": "https://github.com/jomo"}, + { "name": "Jake", "url": "https://github.com/Jake0oo0"} ], "repository": { "type": "git", @@ -30,20 +28,20 @@ "iojs": "2.0.x" }, "dependencies": { - "canvas": "^1.2.7", + "canvas": "^1.2.9", "crc": "~3.3.0", "jade": "~1.11.0", "lwip": "~0.0.7", "mime": "~1.3.4", "node-df": "crafatar/node-df", - "redis": "~0.12.1", - "request": "~2.60.0", + "redis": "~2.0.0", + "request": "~2.63.0", "toobusy-js": "~0.4.2" }, "devDependencies": { - "coveralls": "~2.11.4", - "istanbul": "~0.3.17", - "mocha": "~2.2.5", - "mocha-lcov-reporter": "~0.0.2" + "coveralls": "~2.11.2", + "istanbul": "~0.3.20", + "mocha": "~2.3.3", + "mocha-lcov-reporter": "~1.0.0" } } From c8d74d47be813dd069aff506664505ed67e7c710 Mon Sep 17 00:00:00 2001 From: jomo Date: Fri, 25 Sep 2015 19:14:53 +0200 Subject: [PATCH 45/86] avoid reserved property names (+ test), fixes #145 --- lib/helpers.js | 15 ++++++++------ test/test.js | 53 ++++++++++++++++++++++++++++---------------------- 2 files changed, 39 insertions(+), 29 deletions(-) diff --git a/lib/helpers.js b/lib/helpers.js index 23d5d48..b400aa2 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -119,15 +119,18 @@ var requests = { }; function push_request(userId, type, fun) { - if (!requests[type][userId]) { - requests[type][userId] = []; + // avoid special properties (e.g. 'constructor') + var userId_safe = "!" + userId; + if (!requests[type][userId_safe]) { + requests[type][userId_safe] = []; } - requests[type][userId].push(fun); + requests[type][userId_safe].push(fun); } // calls back all queued requests that match userId and type function resume(userId, type, err, hash) { - var callbacks = requests[type][userId]; + var userId_safe = "!" + userId; + var callbacks = requests[type][userId_safe]; if (callbacks) { if (callbacks.length > 1) { logging.debug(callbacks.length, "simultaneous requests for", userId); @@ -142,7 +145,7 @@ function resume(userId, type, err, hash) { } // it's still an empty array - delete requests[type][userId]; + delete requests[type][userId_safe]; } } @@ -152,7 +155,7 @@ function resume(userId, type, err, hash) { // callback: error, image hash function store_images(rid, userId, cache_details, type, callback) { var is_uuid = userId.length > 16; - if (requests[type][userId]) { + if (requests[type]["!" + userId]) { logging.debug(rid, "adding to request queue"); push_request(userId, type, callback); } else { diff --git a/test/test.js b/test/test.js index 73c3bda..ee5ff7b 100644 --- a/test/test.js +++ b/test/test.js @@ -301,30 +301,37 @@ describe("Crafatar", function() { }); it("should not fail on simultaneous requests", function(done) { - var url = "http://localhost:3000/avatars/696a82ce41f44b51aa31b8709b8686f0"; - // 10 requests at once - var requests = 10; - var finished = 0; - function partDone() { - finished++; - if (requests === finished) { - done(); + // do not change "constructor" ! + // it's a reserved property name, we're testing for that + var sids = ["696a82ce41f44b51aa31b8709b8686f0", "constructor"]; + + for (var j in sids) { + var id = sids[j]; + var url = "http://localhost:3000/avatars/" + id; + // 10 requests at once + var requests = 10; + var finished = 0; + function partDone() { + finished++; + if (requests === finished) { + done(); + } + } + function req() { + request.get(url, function(error, res, body) { + assert.ifError(error); + assert.strictEqual(res.statusCode, 200); + assert_headers(res); + assert(res.headers.etag); + assert.strictEqual(res.headers["content-type"], "image/png"); + assert(body); + partDone(); + }); + } + // make simultanous requests + for (var k = 0; k < requests; k++) { + req(k); } - } - function req() { - request.get(url, function(error, res, body) { - assert.ifError(error); - assert.strictEqual(res.statusCode, 200); - assert_headers(res); - assert(res.headers.etag); - assert.strictEqual(res.headers["content-type"], "image/png"); - assert(body); - partDone(); - }); - } - // make simultanous requests - for (var j = 0; j < requests; j++) { - req(j); } }); From 6fbfd6c355e1bd4e0e7e86b84030b5f4eaf67c7c Mon Sep 17 00:00:00 2001 From: jomo Date: Sat, 26 Sep 2015 16:22:32 +0200 Subject: [PATCH 46/86] update dependencies --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5e3299e..b1a35ca 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "mime": "~1.3.4", "node-df": "crafatar/node-df", "redis": "~2.0.0", - "request": "~2.63.0", + "request": "~2.64.0", "toobusy-js": "~0.4.2" }, "devDependencies": { From fd4fb0764c5be427836457e814e91c9a26e6774e Mon Sep 17 00:00:00 2001 From: jomo Date: Tue, 29 Sep 2015 23:32:16 +0200 Subject: [PATCH 47/86] return & use lwip-stripped image in skins.save_image no need to pass along (possibly) bulky or broken images! see #147 --- lib/helpers.js | 4 ++-- lib/networking.js | 4 ++-- lib/skins.js | 12 ++++++------ 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/helpers.js b/lib/helpers.js index b400aa2..14ea921 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -42,7 +42,7 @@ function store_skin(rid, userId, profile, cache_details, callback) { if (err1 || !img) { callback(err1, null); } else { - skins.save_image(img, skinpath, function(skin_err) { + skins.save_image(img, skinpath, function(skin_err, skin_img) { if (skin_err) { callback(skin_err, null); } else { @@ -95,7 +95,7 @@ function store_cape(rid, userId, profile, cache_details, callback) { if (net_err || !img) { callback(net_err, null); } else { - skins.save_image(img, capepath, function(skin_err) { + skins.save_image(img, capepath, function(skin_err, skin_img) { logging.debug(rid, "cape saved"); callback(skin_err, cape_hash); }); diff --git a/lib/networking.js b/lib/networking.js index ce7effc..0cf7c60 100644 --- a/lib/networking.js +++ b/lib/networking.js @@ -197,8 +197,8 @@ exp.save_texture = function(rid, tex_hash, outpath, callback) { if (err) { callback(err, response, null); } else { - skins.save_image(img, outpath, function(img_err) { - callback(img_err, response, img); + skins.save_image(img, outpath, function(img_err, saved_img) { + callback(img_err, response, saved_img); }); } }); diff --git a/lib/skins.js b/lib/skins.js index 4599d80..5df64ee 100644 --- a/lib/skins.js +++ b/lib/skins.js @@ -134,18 +134,18 @@ exp.open_skin = function(rid, skinpath, callback) { }; // write the image +buffer+ to the +outpath+ file -// callback: error +// the image is stripped down by lwip. +// callback: error, image exp.save_image = function(buffer, outpath, callback) { lwip.open(buffer, "png", function(err, image) { if (err) { - callback(err); + callback(err, image); } else { - image.batch() - .writeFile(outpath, function(write_err) { + image.writeFile(outpath, function(write_err) { if (write_err) { - callback(write_err); + callback(write_err, image); } else { - callback(null); + callback(null, image); } }); } From ecfec6a4074a8dbcd9e54429d52c8c1f4441a7f5 Mon Sep 17 00:00:00 2001 From: jomo Date: Wed, 30 Sep 2015 00:38:32 +0200 Subject: [PATCH 48/86] use MHF_Steve and MHF_Alex instead of steve and alex in default parameter See #142 (not fixed by this commit!) Basically, this just adds mhf_steve and mhf_alex as special cases for the default parameter only --- lib/public/images/{alex.png => mhf_alex.png} | Bin .../{alex_skin.png => mhf_alex_skin.png} | Bin .../images/{steve.png => mhf_steve.png} | Bin .../{steve_skin.png => mhf_steve_skin.png} | Bin lib/public/stylesheets/style.css | 4 +- lib/routes/avatars.js | 5 +- lib/routes/renders.js | 5 +- lib/routes/skins.js | 5 +- lib/skins.js | 8 +- lib/views/index.jade | 36 +++---- test/test.js | 96 +++++++++--------- 11 files changed, 84 insertions(+), 75 deletions(-) rename lib/public/images/{alex.png => mhf_alex.png} (100%) rename lib/public/images/{alex_skin.png => mhf_alex_skin.png} (100%) rename lib/public/images/{steve.png => mhf_steve.png} (100%) rename lib/public/images/{steve_skin.png => mhf_steve_skin.png} (100%) diff --git a/lib/public/images/alex.png b/lib/public/images/mhf_alex.png similarity index 100% rename from lib/public/images/alex.png rename to lib/public/images/mhf_alex.png diff --git a/lib/public/images/alex_skin.png b/lib/public/images/mhf_alex_skin.png similarity index 100% rename from lib/public/images/alex_skin.png rename to lib/public/images/mhf_alex_skin.png diff --git a/lib/public/images/steve.png b/lib/public/images/mhf_steve.png similarity index 100% rename from lib/public/images/steve.png rename to lib/public/images/mhf_steve.png diff --git a/lib/public/images/steve_skin.png b/lib/public/images/mhf_steve_skin.png similarity index 100% rename from lib/public/images/steve_skin.png rename to lib/public/images/mhf_steve_skin.png diff --git a/lib/public/stylesheets/style.css b/lib/public/stylesheets/style.css index 7d4de14..b6d9ebe 100644 --- a/lib/public/stylesheets/style.css +++ b/lib/public/stylesheets/style.css @@ -176,7 +176,7 @@ h4 { background-image: url("/avatars/853c80ef3c3749fdaa49938b674adae6"); } #avatar-example-5:hover .preview { - background-image: url("/avatars/0?default=alex"); + background-image: url("/avatars/0?default=mhf_alex"); } #avatar-example-6:hover .preview { background-image: url("/avatars/0?default=https%3A%2F%2Fi.imgur.com%2FocJVWAc.png"); @@ -193,7 +193,7 @@ h4 { background-image: url("/skins/jeb_"); } #skin-example-2:hover .preview { - background-image: url("/skins/0?default=alex"); + background-image: url("/skins/0?default=mhf_alex"); } #cape-example-1:hover .preview { diff --git a/lib/routes/avatars.js b/lib/routes/avatars.js index f8e9076..e5a6cf6 100644 --- a/lib/routes/avatars.js +++ b/lib/routes/avatars.js @@ -7,7 +7,7 @@ var url = require("url"); function handle_default(img_status, userId, size, def, req, err, callback) { def = def || skins.default_skin(userId); - if (def !== "steve" && def !== "alex") { + if (def !== "steve" && def !== "mhf_steve" && def !== "alex" && def !== "mhf_alex") { if (helpers.id_valid(def)) { // clean up the old URL to match new image var parsed = req.url; @@ -29,6 +29,9 @@ function handle_default(img_status, userId, size, def, req, err, callback) { } } else { // handle steve and alex + if (def.substr(0, 4) !== "mhf_") { + def = "mhf_" + def; + } skins.resize_img(path.join(__dirname, "..", "public", "images", def + ".png"), size, function(resize_err, image) { callback({ status: img_status, diff --git a/lib/routes/renders.js b/lib/routes/renders.js index edef93b..9f26d6e 100644 --- a/lib/routes/renders.js +++ b/lib/routes/renders.js @@ -12,7 +12,7 @@ var fs = require("fs"); // helmet is query param function handle_default(rid, scale, helm, body, img_status, userId, size, def, req, err, callback) { def = def || skins.default_skin(userId); - if (def !== "steve" && def !== "alex") { + if (def !== "steve" && def !== "mhf_steve" && def !== "alex" && def !== "mhf_alex") { if (helpers.id_valid(def)) { // clean up the old URL to match new image var parsed = req.url; @@ -34,6 +34,9 @@ function handle_default(rid, scale, helm, body, img_status, userId, size, def, r } } else { // handle steve and alex + if (def.substr(0, 4) !== "mhf_") { + def = "mhf_" + def; + } fs.readFile(path.join(__dirname, "..", "public", "images", def + "_skin.png"), function (fs_err, buf) { // we render the default skins, but not custom images renders.draw_model(rid, buf, scale, helm, body, function(render_err, def_img) { diff --git a/lib/routes/skins.js b/lib/routes/skins.js index 8b926ac..8cb1a77 100644 --- a/lib/routes/skins.js +++ b/lib/routes/skins.js @@ -8,7 +8,7 @@ var url = require("url"); function handle_default(img_status, userId, def, req, err, callback) { def = def || skins.default_skin(userId); - if (def !== "steve" && def !== "alex") { + if (def !== "steve" && def !== "mhf_steve" && def !== "alex" && def !== "mhf_alex") { if (helpers.id_valid(def)) { // clean up the old URL to match new image var parsed = req.url; @@ -30,6 +30,9 @@ function handle_default(img_status, userId, def, req, err, callback) { } } else { // handle steve and alex + if (def.substr(0, 4) !== "mhf_") { + def = "mhf_" + def; + } lwip.open(path.join(__dirname, "..", "public", "images", def + "_skin.png"), function(lwip_err, image) { if (image) { image.toBuffer("png", function(buf_err, buffer) { diff --git a/lib/skins.js b/lib/skins.js index 5df64ee..a6e1637 100644 --- a/lib/skins.js +++ b/lib/skins.js @@ -56,7 +56,7 @@ exp.extract_helm = function(rid, facefile, buffer, outname, callback) { } else { face_helm_img.toBuffer("png", {compression: "none"}, function(buf_err2, face_helm_buffer) { if (buf_err2) { - callback(buf_err2) + callback(buf_err2); } else { if (face_helm_buffer.toString() !== face_buffer.toString()) { face_helm_img.writeFile(outname, function(write_err) { @@ -101,11 +101,11 @@ exp.resize_img = function(inname, size, callback) { }); }; -// returns "alex" or "steve" calculated by the +uuid+ +// returns "mhf_alex" or "mhf_steve" calculated by the +uuid+ exp.default_skin = function(uuid) { if (uuid.length <= 16) { // we can't get the skin type by username - return "steve"; + return "mhf_steve"; } else { // great thanks to Minecrell for research into Minecraft and Java's UUID hashing! // https://git.io/xJpV @@ -117,7 +117,7 @@ exp.default_skin = function(uuid) { parseInt(uuid[15], 16) ^ parseInt(uuid[23], 16) ^ parseInt(uuid[31], 16); - return lsbs_even ? "alex" : "steve"; + return lsbs_even ? "mhf_alex" : "mhf_steve"; } }; diff --git a/lib/views/index.jade b/lib/views/index.jade index 251a5c0..49cadd3 100644 --- a/lib/views/index.jade +++ b/lib/views/index.jade @@ -62,14 +62,14 @@ block content td default td string td - | The standard value is calculated based on the UUID (even = alex, odd = steve).
- | Usernames always default to steve. + | The standard value is calculated based on the UUID (even = MHF_Alex, odd = MHF_Steve).
+ | Usernames always default to MHF_Steve. td | The image to be served when the userid has no skin.
- | Valid options are - a(href="/avatars/0?default=steve") steve - | , - a(href="/avatars/0?default=alex") alex + | Valid options are any userid, including + a(href="/avatars/0?default=MHF_Steve") MHF_Steve + | and + a(href="/avatars/0?default=MHF_Alex") MHF_Alex | , or a custom URL. tr td helm @@ -95,8 +95,8 @@ block content .example #{domain}/avatars/853c80ef3c3749fdaa49938b674adae6 p.preview Jeb's avatar by UUID #avatar-example-5.example-wrapper - .example #{domain}/avatars/jeb_?default=alex - p.preview Jeb's avatar, or fall back to alex (this example assumes jeb_ does not exist) + .example #{domain}/avatars/jeb_?default=MHF_Alex + p.preview Jeb's avatar, or fall back to MHF_Alex (this example assumes jeb_ does not exist) #avatar-example-6.example-wrapper .example #{domain}/avatars/jeb_?default=https%3A%2F%2Fi.imgur.com%2FocJVWAc.png p.preview @@ -195,14 +195,14 @@ block content td default td string td - | The standard value is calculated based on the UUID (even = alex, odd = steve).
- | Usernames always default to steve. + | The standard value is calculated based on the UUID (even = MHF_Alex, odd = MHF_Steve).
+ | Usernames always default to MHF_Steve. td | The image to be served when the userid has no skin.
- | Valid options are - a(href="/skins/0?default=steve") steve - | , - a(href="/skins/0?default=alex") alex + | Valid options are any userid, including + a(href="/skins/0?default=MHF_Steve") MHF_Steve + | and + a(href="/skins/0?default=MHF_Alex") MHF_Alex | , or a custom URL. section @@ -214,8 +214,8 @@ block content .example #{domain}/skins/jeb_ p.preview Jeb's skin #skin-example-2.example-wrapper - .example #{domain}/skins/jeb_?default=alex - p.preview Jeb's skin, or fall back to alex (this example assumes jeb_ does not exist) + .example #{domain}/skins/jeb_?default=MHF_Alex + p.preview Jeb's skin, or fall back to MHF_Alex (this example assumes jeb_ does not exist) p.preview-placeholder | Hover over the example URLs above for a preview! .preview-background @@ -347,7 +347,7 @@ block content img.preload(src="/avatars/020242a17b9441799eff511eea1221da?size=64", alt="preloaded image") img.preload(src="/avatars/020242a17b9441799eff511eea1221da?size=64&helm", alt="preloaded image") img.preload(src="/avatars/069a79f444e94726a5befca90e38aaf5?size=64", alt="preloaded image") - img.preload(src="/avatars/0?default=alex", alt="preloaded image") + img.preload(src="/avatars/0?default=MHF_Alex", alt="preloaded image") img.preload(src="/avatars/0?default=https%3A%2F%2Fi.imgur.com%2FocJVWAc.png", alt="preloaded image") img.preload(src="/avatars/0ea8eca3dbf647cc9d1ac64551ca975c?size=64", alt="preloaded image") img.preload(src="/avatars/0ea8eca3dbf647cc9d1ac64551ca975c?size=64&helm", alt="preloaded image") @@ -393,5 +393,5 @@ block content img.preload(src="/capes/md_5", alt="preloaded image") img.preload(src="/renders/body/jeb_?helm&scale=4", alt="preloaded image") img.preload(src="/renders/head/853c80ef3c3749fdaa49938b674adae6?scale=8", alt="preloaded image") - img.preload(src="/skins/0?default=alex", alt="preloaded image") + img.preload(src="/skins/0?default=MHF_Alex", alt="preloaded image") img.preload(src="/skins/jeb_", alt="preloaded image") \ No newline at end of file diff --git a/test/test.js b/test/test.js index ee5ff7b..312ae14 100644 --- a/test/test.js +++ b/test/test.js @@ -145,15 +145,15 @@ describe("Crafatar", function() { }); }); }); - it("Username should default to Steve", function(done) { - assert.strictEqual(skins.default_skin("TestUser"), "steve"); + it("Username should default to MHF_Steve", function(done) { + assert.strictEqual(skins.default_skin("TestUser"), "mhf_steve"); done(); }); for (var a in alex_ids) { var alexid = alex_ids[a]; (function(alex_id) { - it("UUID " + alex_id + " should default to Alex", function(done) { - assert.strictEqual(skins.default_skin(alex_id), "alex"); + it("UUID " + alex_id + " should default to MHF_Alex", function(done) { + assert.strictEqual(skins.default_skin(alex_id), "mhf_alex"); done(); }); }(alexid)); @@ -161,8 +161,8 @@ describe("Crafatar", function() { for (var s in steve_ids) { var steveid = steve_ids[s]; (function(steve_id) { - it("UUID " + steve_id + " should default to Steve", function(done) { - assert.strictEqual(skins.default_skin(steve_id), "steve"); + it("UUID " + steve_id + " should default to MHF_Steve", function(done) { + assert.strictEqual(skins.default_skin(steve_id), "mhf_steve"); done(); }); }(steveid)); @@ -343,12 +343,12 @@ describe("Crafatar", function() { }, "avatar with non-existent username": { url: "http://localhost:3000/avatars/0?size=16", - etag: '"steve"', + etag: '"mhf_steve"', crc32: [2416827277, 1243826040] }, "avatar with non-existent username defaulting to alex": { - url: "http://localhost:3000/avatars/0?size=16&default=alex", - etag: '"alex"', + url: "http://localhost:3000/avatars/0?size=16&default=mhf_alex", + etag: '"mhf_alex"', crc32: [862751081, 809395677] }, "avatar with non-existent username defaulting to username": { @@ -373,12 +373,12 @@ describe("Crafatar", function() { }, "helm avatar with non-existent username": { url: "http://localhost:3000/avatars/0?size=16&helm", - etag: '"steve"', + etag: '"mhf_steve"', crc32: [2416827277, 1243826040] }, "helm avatar with non-existent username defaulting to alex": { - url: "http://localhost:3000/avatars/0?size=16&helm&default=alex", - etag: '"alex"', + url: "http://localhost:3000/avatars/0?size=16&helm&default=mhf_alex", + etag: '"mhf_alex"', crc32: [862751081, 809395677] }, "helm avatar with non-existent username defaulting to username": { @@ -403,12 +403,12 @@ describe("Crafatar", function() { }, "avatar with non-existent uuid": { url: "http://localhost:3000/avatars/00000000000000000000000000000000?size=16", - etag: '"steve"', + etag: '"mhf_steve"', crc32: [2416827277, 1243826040] }, "avatar with non-existent uuid defaulting to alex": { - url: "http://localhost:3000/avatars/00000000000000000000000000000000?size=16&default=alex", - etag: '"alex"', + url: "http://localhost:3000/avatars/00000000000000000000000000000000?size=16&default=mhf_alex", + etag: '"mhf_alex"', crc32: [862751081, 809395677] }, "avatar with non-existent uuid defaulting to username": { @@ -433,12 +433,12 @@ describe("Crafatar", function() { }, "helm avatar with non-existent uuid": { url: "http://localhost:3000/avatars/00000000000000000000000000000000?size=16&helm", - etag: '"steve"', + etag: '"mhf_steve"', crc32: [2416827277, 1243826040] }, "helm avatar with non-existent uuid defaulting to alex": { - url: "http://localhost:3000/avatars/00000000000000000000000000000000?size=16&helm&default=alex", - etag: '"alex"', + url: "http://localhost:3000/avatars/00000000000000000000000000000000?size=16&helm&default=mhf_alex", + etag: '"mhf_alex"', crc32: [862751081, 809395677] }, "helm avatar with non-existent uuid defaulting to username": { @@ -491,12 +491,12 @@ describe("Crafatar", function() { }, "skin with non-existent username": { url: "http://localhost:3000/skins/0", - etag: '"steve"', + etag: '"mhf_steve"', crc32: 981937087 }, "skin with non-existent username defaulting to alex": { - url: "http://localhost:3000/skins/0?default=alex", - etag: '"alex"', + url: "http://localhost:3000/skins/0?default=mhf_alex", + etag: '"mhf_alex"', crc32: 2298915739 }, "skin with non-existent username defaulting to username": { @@ -521,12 +521,12 @@ describe("Crafatar", function() { }, "skin with non-existent uuid": { url: "http://localhost:3000/skins/00000000000000000000000000000000", - etag: '"steve"', + etag: '"mhf_steve"', crc32: 981937087 }, "skin with non-existent uuid defaulting to alex": { - url: "http://localhost:3000/skins/00000000000000000000000000000000?default=alex", - etag: '"alex"', + url: "http://localhost:3000/skins/00000000000000000000000000000000?default=mhf_alex", + etag: '"mhf_alex"', crc32: 2298915739 }, "skin with non-existent uuid defaulting to username": { @@ -551,12 +551,12 @@ describe("Crafatar", function() { }, "head render with non-existent username": { url: "http://localhost:3000/renders/head/0?scale=2", - etag: '"steve"', + etag: '"mhf_steve"', crc32: [897270661, 1026982335] }, "head render with non-existent username defaulting to alex": { - url: "http://localhost:3000/renders/head/0?scale=2&default=alex", - etag: '"alex"', + url: "http://localhost:3000/renders/head/0?scale=2&default=mhf_alex", + etag: '"mhf_alex"', crc32: [2357619670, 3172866498] }, "head render with non-existent username defaulting to username": { @@ -581,12 +581,12 @@ describe("Crafatar", function() { }, "helm head render with non-existent username": { url: "http://localhost:3000/renders/head/0?scale=2&helm", - etag: '"steve"', + etag: '"mhf_steve"', crc32: [507497693, 3868868707] }, "helm head render with non-existent username defaulting to alex": { - url: "http://localhost:3000/renders/head/0?scale=2&helm&default=alex", - etag: '"alex"', + url: "http://localhost:3000/renders/head/0?scale=2&helm&default=mhf_alex", + etag: '"mhf_alex"', crc32: [891113664, 1785326216] }, "helm head render with non-existent username defaulting to username": { @@ -611,12 +611,12 @@ describe("Crafatar", function() { }, "head render with non-existent uuid": { url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2", - etag: '"steve"', + etag: '"mhf_steve"', crc32: [897270661, 1026982335] }, "head render with non-existent uuid defaulting to alex": { - url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&default=alex", - etag: '"alex"', + url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&default=mhf_alex", + etag: '"mhf_alex"', crc32: [2357619670, 3172866498] }, "head render with non-existent uuid defaulting to username": { @@ -641,12 +641,12 @@ describe("Crafatar", function() { }, "helm head render with non-existent uuid": { url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&helm", - etag: '"steve"', + etag: '"mhf_steve"', crc32: [507497693, 3868868707] }, "helm head render with non-existent uuid defaulting to alex": { - url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&helm&default=alex", - etag: '"alex"', + url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&helm&default=mhf_alex", + etag: '"mhf_alex"', crc32: [891113664, 1785326216] }, "helm head with non-existent uuid defaulting to username": { @@ -671,12 +671,12 @@ describe("Crafatar", function() { }, "body render with non-existent username": { url: "http://localhost:3000/renders/body/0?scale=2", - etag: '"steve"', + etag: '"mhf_steve"', crc32: [3559591930, 3663447404] }, "body render with non-existent username defaulting to alex": { - url: "http://localhost:3000/renders/body/0?scale=2&default=alex", - etag: '"alex"', + url: "http://localhost:3000/renders/body/0?scale=2&default=mhf_alex", + etag: '"mhf_alex"', crc32: [470529151, 1823026927] }, "body render with non-existent username defaulting to username": { @@ -701,12 +701,12 @@ describe("Crafatar", function() { }, "helm body render with non-existent username": { url: "http://localhost:3000/renders/body/0?scale=2&helm", - etag: '"steve"', + etag: '"mhf_steve"', crc32: [3992841063, 1025743887] }, "helm body render with non-existent username defaulting to alex": { - url: "http://localhost:3000/renders/body/0?scale=2&helm&default=alex", - etag: '"alex"', + url: "http://localhost:3000/renders/body/0?scale=2&helm&default=mhf_alex", + etag: '"mhf_alex"', crc32: [3317518715, 3621585514] }, "helm body render with non-existent username defaulting to username": { @@ -731,12 +731,12 @@ describe("Crafatar", function() { }, "body render with non-existent uuid": { url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2", - etag: '"steve"', + etag: '"mhf_steve"', crc32: [3559591930, 3663447404] }, "body render with non-existent uuid defaulting to alex": { - url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2&default=alex", - etag: '"alex"', + url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2&default=mhf_alex", + etag: '"mhf_alex"', crc32: [470529151, 1823026927] }, "body render with non-existent uuid defaulting to username": { @@ -761,12 +761,12 @@ describe("Crafatar", function() { }, "helm body render with non-existent uuid": { url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2&helm", - etag: '"steve"', + etag: '"mhf_steve"', crc32: [3992841063, 1025743887] }, "helm body render with non-existent uuid defaulting to alex": { - url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2&helm&default=alex", - etag: '"alex"', + url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2&helm&default=mhf_alex", + etag: '"mhf_alex"', crc32: [3317518715, 3621585514] }, "helm body render with non-existent uuid defaulting to url": { From 83defa68856e05ce810dd67b87c0805f4eec27a4 Mon Sep 17 00:00:00 2001 From: jomo Date: Wed, 30 Sep 2015 00:52:39 +0200 Subject: [PATCH 49/86] make default parameter case insensitive, add missing docs to renders See #142 --- lib/routes/avatars.js | 2 +- lib/routes/capes.js | 3 +-- lib/routes/renders.js | 2 +- lib/routes/skins.js | 2 +- lib/views/index.jade | 13 +++++++++++++ 5 files changed, 17 insertions(+), 5 deletions(-) diff --git a/lib/routes/avatars.js b/lib/routes/avatars.js index e5a6cf6..f9f683d 100644 --- a/lib/routes/avatars.js +++ b/lib/routes/avatars.js @@ -48,7 +48,7 @@ function handle_default(img_status, userId, size, def, req, err, callback) { module.exports = function(req, callback) { var userId = (req.url.path_list[1] || "").split(".")[0]; var size = parseInt(req.url.query.size) || config.avatars.default_size; - var def = req.url.query.default; + var def = req.url.query.default && req.url.query.default.toLowerCase(); var helm = req.url.query.hasOwnProperty("helm"); // check for extra paths diff --git a/lib/routes/capes.js b/lib/routes/capes.js index 8cb302b..0f8ad90 100644 --- a/lib/routes/capes.js +++ b/lib/routes/capes.js @@ -1,11 +1,10 @@ -var logging = require("../logging"); var helpers = require("../helpers"); var cache = require("../cache"); // GET cape request module.exports = function(req, callback) { var userId = (req.url.path_list[1] || "").split(".")[0]; - var def = req.url.query.default; + var def = req.url.query.default && req.url.query.default.toLowerCase(); var rid = req.id; // check for extra paths diff --git a/lib/routes/renders.js b/lib/routes/renders.js index 9f26d6e..f8d6732 100644 --- a/lib/routes/renders.js +++ b/lib/routes/renders.js @@ -58,7 +58,7 @@ module.exports = function(req, callback) { var rid = req.id; var body = raw_type === "body"; var userId = (req.url.path_list[2] || "").split(".")[0]; - var def = req.url.query.default; + var def = req.url.query.default && req.url.query.default.toLowerCase(); var scale = parseInt(req.url.query.scale) || config.renders.default_scale; var helm = req.url.query.hasOwnProperty("helm"); diff --git a/lib/routes/skins.js b/lib/routes/skins.js index 8cb1a77..bde53d1 100644 --- a/lib/routes/skins.js +++ b/lib/routes/skins.js @@ -57,7 +57,7 @@ function handle_default(img_status, userId, def, req, err, callback) { // GET skin request module.exports = function(req, callback) { var userId = (req.url.path_list[1] || "").split(".")[0]; - var def = req.url.query.default; + var def = req.url.query.default && req.url.query.default.toLowerCase(); var rid = req.id; // check for extra paths diff --git a/lib/views/index.jade b/lib/views/index.jade index 49cadd3..487feb6 100644 --- a/lib/views/index.jade +++ b/lib/views/index.jade @@ -147,6 +147,19 @@ block content td null td td Apply the "second" layer (hat) to the avatar. + tr + td default + td string + td + | The standard value is calculated based on the UUID (even = MHF_Alex, odd = MHF_Steve).
+ | Usernames always default to MHF_Steve. + td + | The image to be served when the userid has no skin.
+ | Valid options are any userid, including + a(href="/renders/body/0?default=MHF_Steve") MHF_Steve + | and + a(href="/renders/body/0?default=MHF_Alex") MHF_Alex + | , or a custom URL. section a(id="render-examples", class="anchor") From 06caf589abfc4f7a552714558ac5f78abddeeabb Mon Sep 17 00:00:00 2001 From: jomo Date: Wed, 30 Sep 2015 17:00:52 +0200 Subject: [PATCH 50/86] don't lowercase default URLs --- lib/routes/avatars.js | 3 ++- lib/routes/capes.js | 2 +- lib/routes/renders.js | 3 ++- lib/routes/skins.js | 3 ++- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/routes/avatars.js b/lib/routes/avatars.js index f9f683d..e018465 100644 --- a/lib/routes/avatars.js +++ b/lib/routes/avatars.js @@ -29,6 +29,7 @@ function handle_default(img_status, userId, size, def, req, err, callback) { } } else { // handle steve and alex + def = def.toLowerCase(); if (def.substr(0, 4) !== "mhf_") { def = "mhf_" + def; } @@ -48,7 +49,7 @@ function handle_default(img_status, userId, size, def, req, err, callback) { module.exports = function(req, callback) { var userId = (req.url.path_list[1] || "").split(".")[0]; var size = parseInt(req.url.query.size) || config.avatars.default_size; - var def = req.url.query.default && req.url.query.default.toLowerCase(); + var def = req.url.query.default; var helm = req.url.query.hasOwnProperty("helm"); // check for extra paths diff --git a/lib/routes/capes.js b/lib/routes/capes.js index 0f8ad90..77f1953 100644 --- a/lib/routes/capes.js +++ b/lib/routes/capes.js @@ -4,7 +4,7 @@ var cache = require("../cache"); // GET cape request module.exports = function(req, callback) { var userId = (req.url.path_list[1] || "").split(".")[0]; - var def = req.url.query.default && req.url.query.default.toLowerCase(); + var def = req.url.query.default; var rid = req.id; // check for extra paths diff --git a/lib/routes/renders.js b/lib/routes/renders.js index f8d6732..561788a 100644 --- a/lib/routes/renders.js +++ b/lib/routes/renders.js @@ -34,6 +34,7 @@ function handle_default(rid, scale, helm, body, img_status, userId, size, def, r } } else { // handle steve and alex + def = def.toLowerCase(); if (def.substr(0, 4) !== "mhf_") { def = "mhf_" + def; } @@ -58,7 +59,7 @@ module.exports = function(req, callback) { var rid = req.id; var body = raw_type === "body"; var userId = (req.url.path_list[2] || "").split(".")[0]; - var def = req.url.query.default && req.url.query.default.toLowerCase(); + var def = req.url.query.default; var scale = parseInt(req.url.query.scale) || config.renders.default_scale; var helm = req.url.query.hasOwnProperty("helm"); diff --git a/lib/routes/skins.js b/lib/routes/skins.js index bde53d1..310b4bd 100644 --- a/lib/routes/skins.js +++ b/lib/routes/skins.js @@ -30,6 +30,7 @@ function handle_default(img_status, userId, def, req, err, callback) { } } else { // handle steve and alex + def = def.toLowerCase(); if (def.substr(0, 4) !== "mhf_") { def = "mhf_" + def; } @@ -57,7 +58,7 @@ function handle_default(img_status, userId, def, req, err, callback) { // GET skin request module.exports = function(req, callback) { var userId = (req.url.path_list[1] || "").split(".")[0]; - var def = req.url.query.default && req.url.query.default.toLowerCase(); + var def = req.url.query.default; var rid = req.id; // check for extra paths From 7714e0e0ef3a6554d8e8d3b1302b6497d872c4c4 Mon Sep 17 00:00:00 2001 From: jomo Date: Wed, 30 Sep 2015 21:06:16 +0200 Subject: [PATCH 51/86] add case sensitive default URL tests, so 06caf589abfc4f7a552714558ac5f78abddeeabb won't happen again --- test/test.js | 64 ++++++++++++++++++++++++++-------------------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/test/test.js b/test/test.js index 312ae14..ab6e4c9 100644 --- a/test/test.js +++ b/test/test.js @@ -362,9 +362,9 @@ describe("Crafatar", function() { redirect: "/avatars/853c80ef3c3749fdaa49938b674adae6?size=16" }, "avatar with non-existent username defaulting to url": { - url: "http://localhost:3000/avatars/0?size=16&default=http%3A%2F%2Fexample.com", + url: "http://localhost:3000/avatars/0?size=16&default=http%3A%2F%2Fexample.com%2FCaseSensitive", crc32: 0, - redirect: "http://example.com" + redirect: "http://example.com/CaseSensitive" }, "helm avatar with existing username": { url: "http://localhost:3000/avatars/jeb_?size=16&helm", @@ -392,9 +392,9 @@ describe("Crafatar", function() { redirect: "/avatars/853c80ef3c3749fdaa49938b674adae6?size=16&helm=" }, "helm avatar with non-existent username defaulting to url": { - url: "http://localhost:3000/avatars/0?size=16&helm&default=http%3A%2F%2Fexample.com", + url: "http://localhost:3000/avatars/0?size=16&helm&default=http%3A%2F%2Fexample.com%2FCaseSensitive", crc32: 0, - redirect: "http://example.com" + redirect: "http://example.com/CaseSensitive" }, "avatar with existing uuid": { url: "http://localhost:3000/avatars/853c80ef3c3749fdaa49938b674adae6?size=16", @@ -422,9 +422,9 @@ describe("Crafatar", function() { redirect: "/avatars/853c80ef3c3749fdaa49938b674adae6?size=16" }, "avatar with non-existent uuid defaulting to url": { - url: "http://localhost:3000/avatars/00000000000000000000000000000000?size=16&default=http%3A%2F%2Fexample.com", + url: "http://localhost:3000/avatars/00000000000000000000000000000000?size=16&default=http%3A%2F%2Fexample.com%2FCaseSensitive", crc32: 0, - redirect: "http://example.com" + redirect: "http://example.com/CaseSensitive" }, "helm avatar with existing uuid": { url: "http://localhost:3000/avatars/853c80ef3c3749fdaa49938b674adae6?size=16&helm", @@ -452,9 +452,9 @@ describe("Crafatar", function() { redirect: "/avatars/853c80ef3c3749fdaa49938b674adae6?size=16" }, "helm avatar with non-existent uuid defaulting to url": { - url: "http://localhost:3000/avatars/00000000000000000000000000000000?size=16&helm&default=http%3A%2F%2Fexample.com", + url: "http://localhost:3000/avatars/00000000000000000000000000000000?size=16&helm&default=http%3A%2F%2Fexample.com%2FCaseSensitive", crc32: 0, - redirect: "http://example.com" + redirect: "http://example.com/CaseSensitive" }, "cape with existing username": { url: "http://localhost:3000/capes/jeb_", @@ -466,9 +466,9 @@ describe("Crafatar", function() { crc32: 0 }, "cape with non-existent username defaulting to url": { - url: "http://localhost:3000/capes/0?default=http%3A%2F%2Fexample.com", + url: "http://localhost:3000/capes/0?default=http%3A%2F%2Fexample.com%2FCaseSensitive", crc32: 0, - redirect: "http://example.com" + redirect: "http://example.com/CaseSensitive" }, "cape with existing uuid": { url: "http://localhost:3000/capes/853c80ef3c3749fdaa49938b674adae6", @@ -480,9 +480,9 @@ describe("Crafatar", function() { crc32: 0 }, "cape with non-existent uuid defaulting to url": { - url: "http://localhost:3000/capes/00000000000000000000000000000000?default=http%3A%2F%2Fexample.com", + url: "http://localhost:3000/capes/00000000000000000000000000000000?default=http%3A%2F%2Fexample.com%2FCaseSensitive", crc32: 0, - redirect: "http://example.com" + redirect: "http://example.com/CaseSensitive" }, "skin with existing username": { url: "http://localhost:3000/skins/jeb_", @@ -510,9 +510,9 @@ describe("Crafatar", function() { redirect: "/skins/853c80ef3c3749fdaa49938b674adae6?size=16" }, "skin with non-existent username defaulting to url": { - url: "http://localhost:3000/skins/0?default=http%3A%2F%2Fexample.com", + url: "http://localhost:3000/skins/0?default=http%3A%2F%2Fexample.com%2FCaseSensitive", crc32: 0, - redirect: "http://example.com" + redirect: "http://example.com/CaseSensitive" }, "skin with existing uuid": { url: "http://localhost:3000/skins/853c80ef3c3749fdaa49938b674adae6", @@ -540,9 +540,9 @@ describe("Crafatar", function() { redirect: "/skins/853c80ef3c3749fdaa49938b674adae6?size=16" }, "skin with non-existent uuid defaulting to url": { - url: "http://localhost:3000/skins/00000000000000000000000000000000?default=http%3A%2F%2Fexample.com", + url: "http://localhost:3000/skins/00000000000000000000000000000000?default=http%3A%2F%2Fexample.com%2FCaseSensitive", crc32: 0, - redirect: "http://example.com" + redirect: "http://example.com/CaseSensitive" }, "head render with existing username": { url: "http://localhost:3000/renders/head/jeb_?scale=2", @@ -570,9 +570,9 @@ describe("Crafatar", function() { redirect: "/avatars/853c80ef3c3749fdaa49938b674adae6?scale=2" }, "head render with non-existent username defaulting to url": { - url: "http://localhost:3000/renders/head/0?scale=2&default=http%3A%2F%2Fexample.com", + url: "http://localhost:3000/renders/head/0?scale=2&default=http%3A%2F%2Fexample.com%2FCaseSensitive", crc32: 0, - redirect: "http://example.com" + redirect: "http://example.com/CaseSensitive" }, "helm head render with existing username": { url: "http://localhost:3000/renders/head/jeb_?scale=2&helm", @@ -600,9 +600,9 @@ describe("Crafatar", function() { redirect: "/renders/head/853c80ef3c3749fdaa49938b674adae6?scale=2&helm=" }, "helm head render with non-existent username defaulting to url": { - url: "http://localhost:3000/renders/head/0?scale=2&helm&default=http%3A%2F%2Fexample.com", + url: "http://localhost:3000/renders/head/0?scale=2&helm&default=http%3A%2F%2Fexample.com%2FCaseSensitive", crc32: 0, - redirect: "http://example.com" + redirect: "http://example.com/CaseSensitive" }, "head render with existing uuid": { url: "http://localhost:3000/renders/head/853c80ef3c3749fdaa49938b674adae6?scale=2", @@ -630,9 +630,9 @@ describe("Crafatar", function() { redirect: "/renders/head/853c80ef3c3749fdaa49938b674adae6?scale=2" }, "head render with non-existent uuid defaulting to url": { - url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&default=http%3A%2F%2Fexample.com", + url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&default=http%3A%2F%2Fexample.com%2FCaseSensitive", crc32: 0, - redirect: "http://example.com" + redirect: "http://example.com/CaseSensitive" }, "helm head render with existing uuid": { url: "http://localhost:3000/renders/head/853c80ef3c3749fdaa49938b674adae6?scale=2&helm", @@ -660,9 +660,9 @@ describe("Crafatar", function() { redirect: "/renders/head/853c80ef3c3749fdaa49938b674adae6?scale=2&helm=" }, "helm head render with non-existent uuid defaulting to url": { - url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&helm&default=http%3A%2F%2Fexample.com", + url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&helm&default=http%3A%2F%2Fexample.com%2FCaseSensitive", crc32: 0, - redirect: "http://example.com" + redirect: "http://example.com/CaseSensitive" }, "body render with existing username": { url: "http://localhost:3000/renders/body/jeb_?scale=2", @@ -690,9 +690,9 @@ describe("Crafatar", function() { redirect: "/renders/body/853c80ef3c3749fdaa49938b674adae6?scale=2" }, "body render with non-existent username defaulting to url": { - url: "http://localhost:3000/renders/body/0?scale=2&default=http%3A%2F%2Fexample.com", + url: "http://localhost:3000/renders/body/0?scale=2&default=http%3A%2F%2Fexample.com%2FCaseSensitive", crc32: 0, - redirect: "http://example.com" + redirect: "http://example.com/CaseSensitive" }, "helm body render with existing username": { url: "http://localhost:3000/renders/body/jeb_?scale=2&helm", @@ -720,9 +720,9 @@ describe("Crafatar", function() { redirect: "/renders/body/853c80ef3c3749fdaa49938b674adae6?scale=2&helm=" }, "helm body render with non-existent username defaulting to url": { - url: "http://localhost:3000/renders/body/0?scale=2&helm&default=http%3A%2F%2Fexample.com", + url: "http://localhost:3000/renders/body/0?scale=2&helm&default=http%3A%2F%2Fexample.com%2FCaseSensitive", crc32: 0, - redirect: "http://example.com" + redirect: "http://example.com/CaseSensitive" }, "body render with existing uuid": { url: "http://localhost:3000/renders/body/853c80ef3c3749fdaa49938b674adae6?scale=2", @@ -750,9 +750,9 @@ describe("Crafatar", function() { redirect: "/renders/body/853c80ef3c3749fdaa49938b674adae6?scale=2" }, "body render with non-existent uuid defaulting to url": { - url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2&default=http%3A%2F%2Fexample.com", + url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2&default=http%3A%2F%2Fexample.com%2FCaseSensitive", crc32: 0, - redirect: "http://example.com" + redirect: "http://example.com/CaseSensitive" }, "helm body render with existing uuid": { url: "http://localhost:3000/renders/body/853c80ef3c3749fdaa49938b674adae6?scale=2&helm", @@ -770,9 +770,9 @@ describe("Crafatar", function() { crc32: [3317518715, 3621585514] }, "helm body render with non-existent uuid defaulting to url": { - url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2&helm&default=http%3A%2F%2Fexample.com", + url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2&helm&default=http%3A%2F%2Fexample.com%2FCaseSensitive", crc32: 0, - redirect: "http://example.com" + redirect: "http://example.com/CaseSensitive" }, }; From 01ce06cd22aa2f03e10ea9af7bf81f4730a53b53 Mon Sep 17 00:00:00 2001 From: jomo Date: Wed, 7 Oct 2015 01:10:41 +0200 Subject: [PATCH 52/86] Move Installation to wiki --- README.md | 38 ++------------------------------------ 1 file changed, 2 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 95388d8..63ff7e4 100644 --- a/README.md +++ b/README.md @@ -37,41 +37,7 @@ Please [visit the website](https://crafatar.com) for details. Have a look at [crafatar/setup](https://github.com/crafatar/setup) to see how we set things up at Crafatar. -## Installation on Heroku -[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy) - -## Installation on Dokku -##### [dokku server] -Install the [dokku-redis](https://github.com/ohardy/dokku-redis#redis-plugin-for-dokku) plugin. -```shell -dokku redis:start -dokku apps:create crafatar -dokku config:set crafatar BIND=0.0.0.0 PORT=5000 -``` -For persistent images and logs: -```shell -dokku docker-options:add crafatar deploy "-v /var/lib/crafatar/images:/app/images" -dokku docker-options:add crafatar deploy "-v /var/log/crafatar:/app/logs" -``` -If you want to listen on extra domains: -```shell -dokku domains crafatar:add example.com -``` -##### [your machine] -Add dokku remote and deploy! -```shell -git remote add dokku dokku@example.com:crafatar -git push dokku master -``` - -## Installation on your machine -* Use io.js -* [Install](https://github.com/Automattic/node-canvas/wiki) Cairo. -* `npm install` -* Start `redis-server` -* `npm start` -* Access [http://localhost:3000](http://localhost:3000) - +For more info about local setup, Heroku, or Dokku please see [Installation](https://github.com/crafatar/crafatar/wiki/Installation) on the wiki. ## Tests ```shell @@ -87,4 +53,4 @@ env VERBOSE_TEST=true npm test It can be helpful to monitor redis commands to debug caching errors: ```shell redis-cli monitor -``` +``` \ No newline at end of file From 8b2ccf3368ee6402220291c9063a86d6a5ae2992 Mon Sep 17 00:00:00 2001 From: jomo Date: Tue, 13 Oct 2015 00:50:25 +0200 Subject: [PATCH 53/86] add new CRC checksums updated OS X + cairo, so obviously the checksums change, right? right?? --- test/test.js | 50 +++++++++++++++++++++++++------------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/test/test.js b/test/test.js index ab6e4c9..0c6406d 100644 --- a/test/test.js +++ b/test/test.js @@ -547,17 +547,17 @@ describe("Crafatar", function() { "head render with existing username": { url: "http://localhost:3000/renders/head/jeb_?scale=2", etag: '"a846b82963"', - crc32: [1743362302, 208074514] + crc32: [1743362302, 208074514, 2506366593] }, "head render with non-existent username": { url: "http://localhost:3000/renders/head/0?scale=2", etag: '"mhf_steve"', - crc32: [897270661, 1026982335] + crc32: [897270661, 1026982335, 1726107733] }, "head render with non-existent username defaulting to alex": { url: "http://localhost:3000/renders/head/0?scale=2&default=mhf_alex", etag: '"mhf_alex"', - crc32: [2357619670, 3172866498] + crc32: [2357619670, 3172866498, 2214491831] }, "head render with non-existent username defaulting to username": { url: "http://localhost:3000/avatars/0?scale=2&default=jeb_", @@ -577,17 +577,17 @@ describe("Crafatar", function() { "helm head render with existing username": { url: "http://localhost:3000/renders/head/jeb_?scale=2&helm", etag: '"a846b82963"', - crc32: [4178514320, 2340078566] + crc32: [4178514320, 2340078566, 3980890516] }, "helm head render with non-existent username": { url: "http://localhost:3000/renders/head/0?scale=2&helm", etag: '"mhf_steve"', - crc32: [507497693, 3868868707] + crc32: [507497693, 3868868707, 7372195] }, "helm head render with non-existent username defaulting to alex": { url: "http://localhost:3000/renders/head/0?scale=2&helm&default=mhf_alex", etag: '"mhf_alex"', - crc32: [891113664, 1785326216] + crc32: [891113664, 1785326216, 622500655] }, "helm head render with non-existent username defaulting to username": { url: "http://localhost:3000/renders/head/0?scale=2&helm&default=jeb_", @@ -607,17 +607,17 @@ describe("Crafatar", function() { "head render with existing uuid": { url: "http://localhost:3000/renders/head/853c80ef3c3749fdaa49938b674adae6?scale=2", etag: '"a846b82963"', - crc32: [1743362302, 208074514] + crc32: [1743362302, 208074514, 2506366593] }, "head render with non-existent uuid": { url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2", etag: '"mhf_steve"', - crc32: [897270661, 1026982335] + crc32: [897270661, 1026982335, 1726107733] }, "head render with non-existent uuid defaulting to alex": { url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&default=mhf_alex", etag: '"mhf_alex"', - crc32: [2357619670, 3172866498] + crc32: [2357619670, 3172866498, 2214491831] }, "head render with non-existent uuid defaulting to username": { url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&default=jeb_", @@ -637,17 +637,17 @@ describe("Crafatar", function() { "helm head render with existing uuid": { url: "http://localhost:3000/renders/head/853c80ef3c3749fdaa49938b674adae6?scale=2&helm", etag: '"a846b82963"', - crc32: [4178514320, 2340078566] + crc32: [4178514320, 2340078566, 3980890516] }, "helm head render with non-existent uuid": { url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&helm", etag: '"mhf_steve"', - crc32: [507497693, 3868868707] + crc32: [507497693, 3868868707, 7372195] }, "helm head render with non-existent uuid defaulting to alex": { url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&helm&default=mhf_alex", etag: '"mhf_alex"', - crc32: [891113664, 1785326216] + crc32: [891113664, 1785326216, 622500655] }, "helm head with non-existent uuid defaulting to username": { url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&helm&default=jeb_", @@ -667,17 +667,17 @@ describe("Crafatar", function() { "body render with existing username": { url: "http://localhost:3000/renders/body/jeb_?scale=2", etag: '"a846b82963"', - crc32: [1023392610, 4127764743] + crc32: [1023392610, 4127764743, 3884408742] }, "body render with non-existent username": { url: "http://localhost:3000/renders/body/0?scale=2", etag: '"mhf_steve"', - crc32: [3559591930, 3663447404] + crc32: [3559591930, 3663447404, 1521463481] }, "body render with non-existent username defaulting to alex": { url: "http://localhost:3000/renders/body/0?scale=2&default=mhf_alex", etag: '"mhf_alex"', - crc32: [470529151, 1823026927] + crc32: [470529151, 1823026927, 2079926997] }, "body render with non-existent username defaulting to username": { url: "http://localhost:3000/renders/body/0?scale=2&default=jeb_", @@ -697,17 +697,17 @@ describe("Crafatar", function() { "helm body render with existing username": { url: "http://localhost:3000/renders/body/jeb_?scale=2&helm", etag: '"a846b82963"', - crc32: [3476579592, 97705180] + crc32: [3476579592, 97705180, 3086172613] }, "helm body render with non-existent username": { url: "http://localhost:3000/renders/body/0?scale=2&helm", etag: '"mhf_steve"', - crc32: [3992841063, 1025743887] + crc32: [3992841063, 1025743887, 1906839968] }, "helm body render with non-existent username defaulting to alex": { url: "http://localhost:3000/renders/body/0?scale=2&helm&default=mhf_alex", etag: '"mhf_alex"', - crc32: [3317518715, 3621585514] + crc32: [3317518715, 3621585514, 294661951] }, "helm body render with non-existent username defaulting to username": { url: "http://localhost:3000/renders/body/0?scale=2&helm&default=jeb_", @@ -727,17 +727,17 @@ describe("Crafatar", function() { "body render with existing uuid": { url: "http://localhost:3000/renders/body/853c80ef3c3749fdaa49938b674adae6?scale=2", etag: '"a846b82963"', - crc32: [1023392610, 4127764743] + crc32: [1023392610, 4127764743, 3884408742] }, "body render with non-existent uuid": { url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2", etag: '"mhf_steve"', - crc32: [3559591930, 3663447404] + crc32: [3559591930, 3663447404, 1521463481] }, "body render with non-existent uuid defaulting to alex": { url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2&default=mhf_alex", etag: '"mhf_alex"', - crc32: [470529151, 1823026927] + crc32: [470529151, 1823026927, 2079926997] }, "body render with non-existent uuid defaulting to username": { url: "http://localhost:3000/renders/body/0?scale=2&default=jeb_", @@ -757,17 +757,17 @@ describe("Crafatar", function() { "helm body render with existing uuid": { url: "http://localhost:3000/renders/body/853c80ef3c3749fdaa49938b674adae6?scale=2&helm", etag: '"a846b82963"', - crc32: [3476579592, 97705180] + crc32: [3476579592, 97705180, 3086172613] }, "helm body render with non-existent uuid": { url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2&helm", etag: '"mhf_steve"', - crc32: [3992841063, 1025743887] + crc32: [3992841063, 1025743887, 1906839968] }, "helm body render with non-existent uuid defaulting to alex": { url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2&helm&default=mhf_alex", etag: '"mhf_alex"', - crc32: [3317518715, 3621585514] + crc32: [3317518715, 3621585514, 294661951] }, "helm body render with non-existent uuid defaulting to url": { url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2&helm&default=http%3A%2F%2Fexample.com%2FCaseSensitive", @@ -794,7 +794,7 @@ describe("Crafatar", function() { } } } else { - matches = (location.crc32 === crc(body)); + matches = location.crc32 === crc(body); } try { assert.ok(matches); From b0f50cbed018ddb62f634d52a9e062662fda9b70 Mon Sep 17 00:00:00 2001 From: jomo Date: Tue, 13 Oct 2015 00:51:03 +0200 Subject: [PATCH 54/86] print base64 encoded body if CRC does not match --- test/test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test.js b/test/test.js index 0c6406d..f909cdc 100644 --- a/test/test.js +++ b/test/test.js @@ -799,7 +799,7 @@ describe("Crafatar", function() { try { assert.ok(matches); } catch(e) { - throw new Error(crc(body) + " != " + location.crc32); + throw new Error(crc(body) + " != " + location.crc32 + " | " + body.toString("base64")); } assert.strictEqual(res.headers.location, location.redirect); if (location.etag === undefined) { From bf1e26d2c5d4192c44f2ded76f24d792d746fb18 Mon Sep 17 00:00:00 2001 From: jomo Date: Wed, 14 Oct 2015 01:12:30 +0200 Subject: [PATCH 55/86] get rid of jade/haml, use ejs instead --- lib/routes/index.js | 7 +- lib/views/index.html.ejs | 384 ++++++++++++++++++++++++++++++++++++ lib/views/index.jade | 410 --------------------------------------- lib/views/layout.jade | 31 --- package.json | 12 +- 5 files changed, 397 insertions(+), 447 deletions(-) create mode 100644 lib/views/index.html.ejs delete mode 100644 lib/views/index.jade delete mode 100644 lib/views/layout.jade diff --git a/lib/routes/index.js b/lib/routes/index.js index c3d76cf..da20c4d 100644 --- a/lib/routes/index.js +++ b/lib/routes/index.js @@ -1,9 +1,10 @@ var config = require("../../config"); var path = require("path"); -var jade = require("jade"); +var read = require("fs").readFileSync; +var ejs = require("ejs"); -// compile jade -var index = jade.compileFile(path.join(__dirname, "..", "views", "index.jade")); +var str = read(path.join(__dirname, "..", "views", "index.html.ejs"), "utf-8"); +var index = ejs.compile(str); module.exports = function(req, callback) { var html = index({ diff --git a/lib/views/index.html.ejs b/lib/views/index.html.ejs new file mode 100644 index 0000000..a105664 --- /dev/null +++ b/lib/views/index.html.ejs @@ -0,0 +1,384 @@ + + + + + Crafatar + + + + + + + + + + + + + + + + +Fork me on GitHub + + +
+
+

Crafatar

+

A blazing fast API for Minecraft faces!

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

Documentation

+
+
+

Avatars

Replace + userid with a Mojang UUID or username to get the related head. All images are PNGs. +
<%= domain %>/avatars/ + userid +
+
+

Avatar Parameters

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
parametertypedefaultdescription
sizeinteger<%= config.avatars.default_size %>The size of the image in pixels, <%= config.avatars.min_size %> - <%= config.avatars.max_size%>.
defaultstringThe standard value is calculated based on the UUID (even = MHF_Alex, odd = MHF_Steve).
+ Usernames always default to MHF_Steve.
The image to be served when the userid has no skin.
+ Valid options are any userid, including MHF_Steve and MHF_Alex, or a custom URL.
helmnullApply the "second" layer (hat) to the avatar.
+
+
+

Avatar Examples

+
+
+
<%= domain %>/avatars/jeb_
+

Jeb's avatar

+
+
+
<%= domain %>/avatars/jeb_?helm
+

Jeb's avatar with helm

+
+
+
<%= domain %>/avatars/jeb_?size=128
+

Jeb's avatar, 128 × 128

+
+
+
<%= domain %>/avatars/853c80ef3c3749fdaa49938b674adae6
+

Jeb's avatar by UUID

+
+
+
<%= domain %>/avatars/jeb_?default=MHF_Alex
+

Jeb's avatar, or fall back to MHF_Alex (this example assumes jeb_ does not exist)

+
+
+
<%= domain %>/avatars/jeb_?default=https%3A%2F%2Fi.imgur.com%2FocJVWAc.png
+

Jeb's avatar, or fall back to a custom image (this example assumes jeb_ does not exist)

+
+

Hover over the example URLs above for a preview!

+
+
+
+
+
+

3D Renders

+

Crafatar also provides support for 3D renders of Minecraft skins.
+ Please note that this feature is currently beta!
+ Replace + userid with a Mojang UUID or username to get a render of the skin. The head render type returns a render of the skin's head.<%= domain %>/renders/head/useridThe body render returns a render of the entire skin.<%= domain %>/renders/body/userid

+
+

Render Parameters

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
parametertypedefaultdescription
scaleinteger<%= config.renders.default_scale %>. The actual size differs between the type of render.The scale factor of the image <%= config.renders.min_scale %> - <%= config.renders.max_scale %>.
helmnullApply the "second" layer (hat) to the avatar.
defaultstringThe standard value is calculated based on the UUID (even = MHF_Alex, odd = MHF_Steve).
+ Usernames always default to MHF_Steve.
The image to be served when the userid has no skin.
+ Valid options are any userid, including MHF_Steve and MHF_Alex, or a custom URL.
+
+
+

Render Examples

+
+
+
<%= domain %>/renders/body/jeb_?helm&scale=4
+

Jeb's body, with helmet, scale 4

+
+
+
<%= domain %>/renders/head/853c80ef3c3749fdaa49938b674adae6?scale=8
+

Jeb's head, by UUID, scale 8

+
+

Hover over the example URLs above for a preview!

+
+
+
+
+
+

Skins

+

You can also get the full skin file of a player.
+ Replace + userid with a Mojang UUID or username to get the related skin.
+ The user's skin is returned, or the default image is served.
+ You can use the default parameter here as well.<%= domain %>/skins/userid

+
+

Skin Parameters

+ + + + + + + + + + + + + + + + + +
parametertypedefaultdescription
defaultstringThe standard value is calculated based on the UUID (even = MHF_Alex, odd = MHF_Steve).
+ Usernames always default to MHF_Steve.
The image to be served when the userid has no skin.
+ Valid options are any userid, including MHF_Steve and MHF_Alex, or a custom URL.
+
+
+

Skin Examples

+
+
+
<%= domain %>/skins/jeb_
+

Jeb's skin

+
+
+
<%= domain %>/skins/jeb_?default=MHF_Alex
+

Jeb's skin, or fall back to MHF_Alex (this example assumes jeb_ does not exist)

+
+

Hover over the example URLs above for a preview!

+
+
+
+
+
+

Capes

+

A cape endpoint is also available to get the active cape of a user.
+ Replace + userid with a Mojang UUID or username to get the related cape.
+ The user's cape is returned, otherwise a 404 is returned.
+

<%= domain %>/capes/ + userid +
+

+
+

Cape Examples

+
+
+
<%= domain %>/capes/Dinnerbone
+

Dinnerbone's Cape Mojang capes are not transparent...

+
+
+
<%= domain %>/capes/md_5
+

md_5's Cape

+
+

Hover over the example URLs above for a preview!

+
+
+
+
+
+

Meta

+
+

CORS

+

Crafatar supports CORS so you can make AJAX request from within the browser!

+
+
+

HTTP Headers

+

Responses come with these HTTP headers, useful for debugging.
+ Please note that these headers are cached by CloudFlare (CF-Cache-Status: HIT).

+
+

Response-Time

+

The time, in milliseconds, it took Crafatar to process the request.

+
+
+

X-Storage-Type

+

Details about how the requested image was stored on the server

+
    +
  • none: No external requests. Cached: User has no skin.
  • +
  • cached: No external requests. Skin cached and stored locally.
  • +
  • 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.
  • +
  • downloaded: 2 external requests. First request or skin changed, skin downloaded.
  • +
  • server error: This can happen, for example, when Mojang's servers are down.
    + If possible, a cached image is served instead.
  • +
  • user error: You have done something wrong, such as requesting a malformed userid.
    + Check the response body for details.
  • +
+
+
+

X-Request-ID

+

The internal ID assigned to this request.
+ If you think something is wrong with your request, please contact us and provide this ID.

+
+
+
+

About Usernames

+

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.
+ You don't have to change anything when using UUIDs and someone changes their Username.
+ Malformed usernames are rejected.

+
+
+

About UUIDs

+

UUIDs may use the blank or dashed format.
+ Malformed UUIDs are rejected.

+
+
+

About Caching

+

Crafatar caches skins for <%= config.caching.local / 60 %> minutes before checking for skin changes.
+ Images are cached in your browser for <%= config.caching.browser / 60 %> minutes until a new request to Crafatar is made.
+ When you changed your skin you can try clearing your browser cache to see the change faster.

+
+
+
+

Contact

+ +
+
+
+
+
+

Copyright Crafatar <%= new Date().getFullYear() %>

+
+
+ preloaded image + preloaded image + preloaded image + preloaded image + preloaded image + preloaded image + preloaded image + preloaded image + preloaded image + preloaded image + preloaded image + preloaded image + preloaded image + preloaded image + preloaded image + preloaded image + preloaded image + preloaded image + preloaded image + preloaded image + preloaded image + preloaded image + preloaded image + preloaded image + preloaded image + preloaded image + preloaded image + preloaded image + preloaded image + preloaded image + preloaded image + preloaded image + preloaded image + preloaded image + preloaded image + preloaded image + preloaded image + preloaded image + preloaded image + preloaded image + preloaded image + preloaded image + preloaded image + preloaded image + preloaded image + preloaded image + preloaded image + preloaded image + preloaded image + preloaded image + preloaded image + + \ No newline at end of file diff --git a/lib/views/index.jade b/lib/views/index.jade deleted file mode 100644 index 487feb6..0000000 --- a/lib/views/index.jade +++ /dev/null @@ -1,410 +0,0 @@ -extends layout - -block content - .jumbotron - .container - h1 Crafatar - p A blazing fast API for Minecraft faces! - .avatar-wrapper - .avatar.jomo(title="jomo's avatar") - .avatar.jake_0(title="jake_0's avatar") - .avatar.sk89q(title="sk89q's avatar") - .avatar.md_5(title="md_5's avatar") - .avatar.notch(title="notch's avatar") - .avatar.jeb(title="jeb's avatar") - .avatar.dinnerbone.flipped(title="dinnerbone's avatar") - .avatar.ez(title="ez' avatar") - .avatar.grumm.flipped(title="grumm's avatar") - .avatar.themogmimer(title="themogmimer's avatar") - .avatar.searge(title="searge's avatar") - .avatar.xlson(title="xlson's avatar") - .avatar.krisjelbring(title="krisjelbring's avatar") - .avatar.minecraftchick(title="minecraftchick's avatar") - .avatar.kappe(title="kappe's avatar") - .avatar.marc(title="marc's avatar") - .avatar.mollstam(title="mollstam's avatar") - .avatar.evilseph(title="evilseph's avatar") - .avatar.thinkofdeath(title="thinkofdeath's avatar") - - .container - section(id="documentation") - h2 Documentation - .row - section - a(id="avatars", class="anchor") - a(href="#avatars") - h3 Avatars - | Replace - mark.green userid - | with a Mojang UUID or username to get the related head. All images are PNGs. - .code - | #{domain}/avatars/ - mark.green userid - - section - a(id="avatar-parameters" class="anchor") - a(href="#avatar-parameters") - h4 Avatar Parameters - table(class="table table-striped") - thead - tr - td parameter - td type - td default - td description - tbody - tr - td size - td integer - td #{config.avatars.default_size} - td The size of the image in pixels, #{config.avatars.min_size} - #{config.avatars.max_size}. - tr - td default - td string - td - | The standard value is calculated based on the UUID (even = MHF_Alex, odd = MHF_Steve).
- | Usernames always default to MHF_Steve. - td - | The image to be served when the userid has no skin.
- | Valid options are any userid, including - a(href="/avatars/0?default=MHF_Steve") MHF_Steve - | and - a(href="/avatars/0?default=MHF_Alex") MHF_Alex - | , or a custom URL. - tr - td helm - td null - td - td Apply the "second" layer (hat) to the avatar. - - section - a(id="avatar-examples", class="anchor") - a(href="#avatar-examples") - h4 Avatar Examples - .code - #avatar-example-1.example-wrapper - .example #{domain}/avatars/jeb_ - p.preview Jeb's avatar - #avatar-example-2.example-wrapper - .example #{domain}/avatars/jeb_?helm - p.preview Jeb's avatar with helm - #avatar-example-3.example-wrapper - .example #{domain}/avatars/jeb_?size=128 - p.preview Jeb's avatar, 128 × 128 - #avatar-example-4.example-wrapper - .example #{domain}/avatars/853c80ef3c3749fdaa49938b674adae6 - p.preview Jeb's avatar by UUID - #avatar-example-5.example-wrapper - .example #{domain}/avatars/jeb_?default=MHF_Alex - p.preview Jeb's avatar, or fall back to MHF_Alex (this example assumes jeb_ does not exist) - #avatar-example-6.example-wrapper - .example #{domain}/avatars/jeb_?default=https%3A%2F%2Fi.imgur.com%2FocJVWAc.png - p.preview - | Jeb's avatar, or fall back to a custom image (this example assumes jeb_ does not exist) - p.preview-placeholder - | Hover over the example URLs above for a preview! - .preview-background - - - section - a(id="renders" class="anchor") - a(href="#renders") - h3 3D Renders - p - | Crafatar also provides support for 3D renders of Minecraft skins.
- | Please note that this feature is currently beta!
- | Replace - mark.green userid - | with a Mojang UUID or username to get a render of the skin. - | The head render type returns a render of the skin's head. - span.code - | #{domain}/renders/head/ - mark.green userid - | The body render returns a render of the entire skin. - span.code - | #{domain}/renders/body/ - mark.green userid - - section - a(id="render-parameters" class="anchor") - a(href="#render-parameters") - h4 Render Parameters - table(class="table table-striped") - thead - tr - td parameter - td type - td default - td description - tbody - tr - td scale - td integer - td #{config.renders.default_scale}. The actual size differs between the type of render. - td The scale factor of the image #{config.renders.min_scale} - #{config.renders.max_scale}. - tr - td helm - td null - td - td Apply the "second" layer (hat) to the avatar. - tr - td default - td string - td - | The standard value is calculated based on the UUID (even = MHF_Alex, odd = MHF_Steve).
- | Usernames always default to MHF_Steve. - td - | The image to be served when the userid has no skin.
- | Valid options are any userid, including - a(href="/renders/body/0?default=MHF_Steve") MHF_Steve - | and - a(href="/renders/body/0?default=MHF_Alex") MHF_Alex - | , or a custom URL. - - section - a(id="render-examples", class="anchor") - a(href="#render-examples") - h4 Render Examples - .code - #render-example-1.example-wrapper - .example #{domain}/renders/body/jeb_?helm&scale=4 - p.preview Jeb's body, with helmet, scale 4 - #render-example-2.example-wrapper - .example #{domain}/renders/head/853c80ef3c3749fdaa49938b674adae6?scale=8 - p.preview Jeb's head, by UUID, scale 8 - p.preview-placeholder - | Hover over the example URLs above for a preview! - .preview-background - - - section - a(id="skins" class="anchor") - a(href="#skins") - h3 Skins - p - | You can also get the full skin file of a player.
- | Replace - mark.green userid - | with a Mojang UUID or username to get the related skin.
- | The user's skin is returned, or the default image is served.
- | You can use the default parameter here as well. - span.code - | #{domain}/skins/ - mark.green userid - - section - a(id="skin-parameters" class="anchor") - a(href="#skin-parameters") - h4 Skin Parameters - table(class="table table-striped") - thead - tr - td parameter - td type - td default - td description - tbody - tr - td default - td string - td - | The standard value is calculated based on the UUID (even = MHF_Alex, odd = MHF_Steve).
- | Usernames always default to MHF_Steve. - td - | The image to be served when the userid has no skin.
- | Valid options are any userid, including - a(href="/skins/0?default=MHF_Steve") MHF_Steve - | and - a(href="/skins/0?default=MHF_Alex") MHF_Alex - | , or a custom URL. - - section - a(id="skin-examples", class="anchor") - a(href="#skin-examples") - h4 Skin Examples - .code - #skin-example-1.example-wrapper - .example #{domain}/skins/jeb_ - p.preview Jeb's skin - #skin-example-2.example-wrapper - .example #{domain}/skins/jeb_?default=MHF_Alex - p.preview Jeb's skin, or fall back to MHF_Alex (this example assumes jeb_ does not exist) - p.preview-placeholder - | Hover over the example URLs above 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 userid - | with a Mojang UUID or username to get the related cape.
- | The user's cape is returned, otherwise a 404 is returned.
- .code - | #{domain}/capes/ - mark.green userid - - 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 example URLs above for a preview! - .preview-background - - section - a(id="meta" class="anchor") - a(href="#meta") - h2 Meta - - section - a(id="meta-cors" class="anchor") - a(href="#meta-cors") - h3 CORS - p - | Crafatar supports CORS so you can make AJAX request from within the browser! - - section - a(id="meta-http-headers" class="anchor") - a(href="#meta-http-headers") - h3 HTTP Headers - p - | Responses come with these HTTP headers, useful for debugging.
- | Please note that these headers are cached by CloudFlare (CF-Cache-Status: HIT). - - section - a(id="meta-response-time" class="anchor") - a(href="#meta-response-time") - h4 Response-Time - p The time, in milliseconds, it took Crafatar to process the request. - - section - a(id="meta-x-storage-type" class="anchor") - a(href="#meta-x-storage-type") - h4 X-Storage-Type - p Details about how the requested image was stored on the server - 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 downloaded: 2 external requests. First request or skin changed, skin downloaded. - li - | server error: This can happen, for example, when Mojang's servers are down.
- | If possible, a cached image is served instead. - li - | user error: You have done something wrong, such as requesting a malformed userid.
- | Check the response body for details. - section - a(id="meta-x-request-id" class="anchor") - a(href="#meta-x-request-id") - h4 X-Request-ID - p - | The internal ID assigned to this request.
- | If you think something is wrong with your request, please contact us and provide this ID. - - section - a(id="meta-about-usernames" class="anchor") - a(href="#meta-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.
- | You don't have to change anything when using UUIDs and someone changes their Username.
- | Malformed usernames are rejected. - - section - a(id="meta-about-uuids" class="anchor") - a(href="#meta-about-uuids") - h3 About UUIDs - p - | UUIDs may use the blank or dashed format.
- | Malformed UUIDs are rejected. - - section - a(id="meta-about-caching" class="anchor") - a(href="#meta-about-caching") - h3 About Caching - p - | Crafatar caches skins for #{config.caching.local/60} minutes before checking for skin changes.
- | Images are cached in your browser for #{config.caching.browser/60} minutes until a new request to Crafatar is made.
- | When you changed your skin you can try clearing your browser cache to see the change faster. - - - section - a(id="contact" class="anchor") - a(href="#contact") - h2 Contact - ul - li Follow us on twitter @crafatar - li Open an issue on GitHub - li Join us in #crafatar on irc.esper.net - - footer - hr - p(class="pull-right") Copyright Crafatar #{new Date().getFullYear()} - - - // preload hover images - img.preload(src="/avatars/020242a17b9441799eff511eea1221da?size=64", alt="preloaded image") - img.preload(src="/avatars/020242a17b9441799eff511eea1221da?size=64&helm", alt="preloaded image") - img.preload(src="/avatars/069a79f444e94726a5befca90e38aaf5?size=64", alt="preloaded image") - img.preload(src="/avatars/0?default=MHF_Alex", alt="preloaded image") - img.preload(src="/avatars/0?default=https%3A%2F%2Fi.imgur.com%2FocJVWAc.png", alt="preloaded image") - img.preload(src="/avatars/0ea8eca3dbf647cc9d1ac64551ca975c?size=64", alt="preloaded image") - img.preload(src="/avatars/0ea8eca3dbf647cc9d1ac64551ca975c?size=64&helm", alt="preloaded image") - img.preload(src="/avatars/1c1bd09a6a0f4928a7914102a35d2670?size=64", alt="preloaded image") - img.preload(src="/avatars/1c1bd09a6a0f4928a7914102a35d2670?size=64&helm", alt="preloaded image") - img.preload(src="/avatars/2d5aa9cdaeb049189930461fc9b91cc5?size=64", alt="preloaded image") - img.preload(src="/avatars/2d5aa9cdaeb049189930461fc9b91cc5?size=64&helm", alt="preloaded image") - img.preload(src="/avatars/4566e69fc90748ee8d71d7ba5aa00d20?size=64", alt="preloaded image") - img.preload(src="/avatars/4566e69fc90748ee8d71d7ba5aa00d20?size=64&helm", alt="preloaded image") - img.preload(src="/avatars/61699b2ed3274a019f1e0ea8c3f06bc6?size=64", alt="preloaded image") - img.preload(src="/avatars/61699b2ed3274a019f1e0ea8c3f06bc6?size=64&helm", alt="preloaded image") - img.preload(src="/avatars/696a82ce41f44b51aa31b8709b8686f0?size=64", alt="preloaded image") - img.preload(src="/avatars/696a82ce41f44b51aa31b8709b8686f0?size=64&helm", alt="preloaded image") - img.preload(src="/avatars/7125ba8b1c864508b92bb5c042ccfe2b?size=64", alt="preloaded image") - img.preload(src="/avatars/7125ba8b1c864508b92bb5c042ccfe2b?size=64&helm", alt="preloaded image") - img.preload(src="/avatars/7d043c7389524696bfba571c05b6aec0?size=64", alt="preloaded image") - img.preload(src="/avatars/7d043c7389524696bfba571c05b6aec0?size=64&helm", alt="preloaded image") - img.preload(src="/avatars/853c80ef3c3749fdaa49938b674adae6", alt="preloaded image") - img.preload(src="/avatars/853c80ef3c3749fdaa49938b674adae6?size=64", alt="preloaded image") - img.preload(src="/avatars/853c80ef3c3749fdaa49938b674adae6?size=64&helm", alt="preloaded image") - img.preload(src="/avatars/9769ecf6331448f3ace67ae06cec64a3?size=64", alt="preloaded image") - img.preload(src="/avatars/9769ecf6331448f3ace67ae06cec64a3?size=64&helm", alt="preloaded image") - img.preload(src="/avatars/ae795aa86327408e92ab25c8a59f3ba1?size=64", alt="preloaded image") - img.preload(src="/avatars/ae795aa86327408e92ab25c8a59f3ba1?size=64&helm", alt="preloaded image") - img.preload(src="/avatars/af74a02d19cb445bb07f6866a861f783?size=64", alt="preloaded image") - img.preload(src="/avatars/af74a02d19cb445bb07f6866a861f783?size=64&helm", alt="preloaded image") - img.preload(src="/avatars/b05881186e75410db2db4d3066b223f7?size=64", alt="preloaded image") - img.preload(src="/avatars/b05881186e75410db2db4d3066b223f7?size=64&helm", alt="preloaded image") - img.preload(src="/avatars/b9583ca43e64488a9c8c4ab27e482255?size=64", alt="preloaded image") - img.preload(src="/avatars/b9583ca43e64488a9c8c4ab27e482255?size=64&helm", alt="preloaded image") - img.preload(src="/avatars/c9b54008fd8047428b238787b5f2401c?size=64", alt="preloaded image") - img.preload(src="/avatars/c9b54008fd8047428b238787b5f2401c?size=64&helm", alt="preloaded image") - img.preload(src="/avatars/d8f9a4340f2d415f9acfcd70341c75ec?size=64", alt="preloaded image") - img.preload(src="/avatars/d8f9a4340f2d415f9acfcd70341c75ec?size=64&helm", alt="preloaded image") - img.preload(src="/avatars/e6b5c088068044df9e1b9bf11792291b?size=64", alt="preloaded image") - img.preload(src="/avatars/e6b5c088068044df9e1b9bf11792291b?size=64&helm", alt="preloaded image") - img.preload(src="/avatars/f8cdb6839e9043eea81939f85d9c5d69?size=64", alt="preloaded image") - img.preload(src="/avatars/f8cdb6839e9043eea81939f85d9c5d69?size=64&helm", alt="preloaded image") - img.preload(src="/avatars/jeb_", alt="preloaded image") - img.preload(src="/avatars/jeb_?helm", alt="preloaded image") - img.preload(src="/avatars/jeb_?size=128", alt="preloaded image") - img.preload(src="/capes/Dinnerbone", alt="preloaded image") - img.preload(src="/capes/md_5", alt="preloaded image") - img.preload(src="/renders/body/jeb_?helm&scale=4", alt="preloaded image") - img.preload(src="/renders/head/853c80ef3c3749fdaa49938b674adae6?scale=8", alt="preloaded image") - img.preload(src="/skins/0?default=MHF_Alex", alt="preloaded image") - img.preload(src="/skins/jeb_", alt="preloaded image") \ No newline at end of file diff --git a/lib/views/layout.jade b/lib/views/layout.jade deleted file mode 100644 index 0eef6f8..0000000 --- a/lib/views/layout.jade +++ /dev/null @@ -1,31 +0,0 @@ -doctype html -html(lang="en") - head - title= title - link(rel="icon", sizes="16x16", type="image/png", href="/favicon.png") - link(href="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.1/css/bootstrap.min.css", rel="stylesheet") - link(rel="stylesheet", href="/stylesheets/style.css") - - meta(name="description", content="Crafatar is a blazing fast Minecraft avatar API with support for avatars, skins, and even 3D renders!") - meta(name="keywords", content="minecraft, avatar, renders, skins, uuid, username") - meta(name="viewport", content="initial-scale=1,maximum-scale=1") - - meta(charset='utf-8') - meta(property='og:title', content='Crafatar') - meta(property='og:type', content='website') - meta(property='og:url', content='https://crafatar.com') - meta(property='og:image', content='https://crafatar.com/logo.png') - meta(property='og:description', content='A blazing fast Minecraft avatar API with support for avatars, skins, and 3D renders.') - - meta(name='twitter:card', content='summary') - meta(name='twitter:creator', content='@Crafatar') - body - a.forkme(href="https://github.com/crafatar/crafatar", target="_blank") Fork me on GitHub - a.sponsor(href="https://akliz.net/crafatar", target="_blank", title="Crafatar is sponsored by Akliz") - img(src="/images/akliz.png", alt="Akliz") - .navbar.navbar-default.navbar-fixed-top - .container - .navbar-header - a.navbar-brand(href="/") Crafatar - a.navbar-brand.twitter(href="https://twitter.com/Crafatar", target="_blank") crafatar - block content \ No newline at end of file diff --git a/package.json b/package.json index b1a35ca..8bb2adb 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,14 @@ "private": true, "description": "A blazing fast API for Minecraft faces!", "contributors": [ - { "name": "jomo", "url": "https://github.com/jomo"}, - { "name": "Jake", "url": "https://github.com/Jake0oo0"} + { + "name": "jomo", + "url": "https://github.com/jomo" + }, + { + "name": "Jake", + "url": "https://github.com/Jake0oo0" + } ], "repository": { "type": "git", @@ -30,7 +36,7 @@ "dependencies": { "canvas": "^1.2.9", "crc": "~3.3.0", - "jade": "~1.11.0", + "ejs": "^2.3.4", "lwip": "~0.0.7", "mime": "~1.3.4", "node-df": "crafatar/node-df", From a3cbedb8592331c14ed0e6c691cb0c8383c1263f Mon Sep 17 00:00:00 2001 From: jomo Date: Wed, 14 Oct 2015 23:41:51 +0200 Subject: [PATCH 56/86] update to bootstrap 4 alpha - there's no official CDN for bootstrap 4 yet - fixed breaking change in bootstrap's navbar --- lib/public/images/akliz.png | Bin 830 -> 1701 bytes lib/public/stylesheets/bootstrap.min.css | 5 +++++ lib/public/stylesheets/style.css | 1 - lib/views/index.html.ejs | 7 ++++--- 4 files changed, 9 insertions(+), 4 deletions(-) create mode 100644 lib/public/stylesheets/bootstrap.min.css diff --git a/lib/public/images/akliz.png b/lib/public/images/akliz.png index 3783439ab3026611f81dd21330157051e0669dab..ba4d2a68f3c4acd023e679cd16d88283afd08947 100644 GIT binary patch literal 1701 zcmV;W23q-vP)004R> z004l5008;`004mK004C`008P>0026e000+ooVrmw00006VoOIv0RI600RN!9r;`8x z010qNS#tmY3labT3lag+-G2N400tRJL_t(&-qo60Op|9A$N&9GE%ZQ(V9*#54YYtn z4HPrMEuJn!BHrkY#!Q)sTZ{`jB~h4RL^N=LXbcz3cs0#-;kLxYc)0Nx4G}d)4YUs9 zoCyghZEJzAhyO0#aVTwRffl+q`OGKbP(l_26d{r>J2w7dXYN^0R?c)Pi5Um@FtD4TjmnDXQ3V#cPM#E}pb2p4 zQlO_tgS&TY(bF^Y;HNW2lCWAu5+RgG2ngQm1tdw0OP8wg;6WvZhgDE0#tvv}18lYl zS4jXiZUi(M;MOg`=zV}A3u`6{YoY%r|nLo3b42s zC@j=Mt38fYtBydeP8_{NQWEg(0bof6uw@I7lLNeZ<)6Jm0Y9abN5|J9o%ar^J3y5&7^T`SK^U&XMc47(JuuB;uuK+ii^d$y}hD` z^74SpOyKryU~tgCC&s%I%gc4658%Q0%grS}{6ju{DynIfT;@vE?%!dVk$$D#69x!fy0M|#fBTSSadWP8YoGD2<_V^q?|ZLEVZSDoIjt; z$RHm-Cf~gySFR+_pBLvHIuxkD)~%rxC@CR(d&wI&NTV^Ftv1kN(Ivoccfw@)30A8D z4o4+?KGDQ_y=Yz%sILb)I)pX)d_sj2CxEgt;Otr8{d>Ue7T@*s08LH6h7CYf74Ya0 z@ZoAi48H;M=g{-Vv==IHzBzMFP;&ckm!#R(`AtolDPkhR9IA?MrIk_46 z`L#%2=u$Ww5jd0C3|cIaDGY2!40 zi778{Zi4qyVk$Vr(;}Z=5|Q8(57>lEA{-)YQQ3MnM7Iy@S(< z-d+?QMoSC&`@v$Ns|z_f`1cPg74F@G`zZth3=d;^8jBaBvI5!JXl{mHkBJEk4fzTD z`i0K>`11z?1K{(pXc4}DhslJ-@e)!HX$BuL!lQ6W^Q#G0Fu_bxBO#7qHg;>cZ2xz^Ro6QTOXJ{PYFuRvHYB)oROsHZwaDOPxR?_CTHZ(`Y+ve%X<#L lxJb>43oe#3XTvO){?gmxV_JK3%ar6_2&q^pY8CD&{SV0JtX=>B diff --git a/lib/public/stylesheets/bootstrap.min.css b/lib/public/stylesheets/bootstrap.min.css new file mode 100644 index 0000000..e22f474 --- /dev/null +++ b/lib/public/stylesheets/bootstrap.min.css @@ -0,0 +1,5 @@ +@charset "UTF-8";/*! + * Bootstrap v4.0.0-alpha (http://getbootstrap.com) + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + *//*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */img,legend{border:0}dl,ol,p,pre,ul{margin-top:0}address,dl,ol,p,ul{margin-bottom:1rem}b,dt,optgroup,strong{font-weight:700}caption,th{text-align:left}fieldset,legend,td,th{padding:0}pre,textarea{overflow:auto}.btn-group>.btn-group,.btn-toolbar .btn-group,.btn-toolbar .input-group,.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9,.dropdown-menu,.table-reflow thead,.table-reflow tr{float:left}.btn,.c-indicator{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none}.btn-toolbar:after,.clearfix:after,.container-fluid:after,.container:after,.dl-horizontal:after,.dropdown-item,.modal-footer:after,.modal-header:after,.nav-tabs:after,.navbar:after,.pager:after,.row:after{clear:both}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}dfn{font-style:italic}h1{margin:.67em 0;font-size:2em}mark{color:#000;background:#ff0}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{vertical-align:middle}svg:not(:root){overflow:hidden}hr{height:0;-webkit-box-sizing:content-box;box-sizing:content-box}code,kbd,pre,samp{font-size:1em}button,input,optgroup,select,textarea{margin:0;font:inherit;color:inherit}address,legend{line-height:inherit}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}input{line-height:normal}input[type=checkbox],input[type=radio]{-webkit-box-sizing:border-box;box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:none}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}textarea{resize:vertical}table{border-spacing:0;border-collapse:collapse}@media print{blockquote,img,pre,tr{page-break-inside:avoid}*,:after,:before{text-shadow:none!important;-webkit-box-shadow:none!important;box-shadow:none!important}a,a:visited{text-decoration:underline}abbr[title]:after{content:" (" attr(title) ")"}blockquote,pre{border:1px solid #999}thead{display:table-header-group}img{max-width:100%!important}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #ddd!important}}html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-webkit-box-sizing:border-box;box-sizing:border-box;font-size:16px;-webkit-tap-highlight-color:transparent}*,:after,:before{-webkit-box-sizing:inherit;box-sizing:inherit}@-ms-viewport{width:device-width}@viewport{width:device-width}body{margin:0;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:1rem;line-height:1.5;color:#373a3c;background-color:#fff}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}abbr[data-original-title],abbr[title]{cursor:help;border-bottom:1px dotted #818a91}address{font-style:normal}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dd,label{margin-bottom:.5rem}dd{margin-left:0}blockquote,figure{margin:0 0 1rem}a{color:#0275d8;text-decoration:none}a:focus,a:hover{color:#014c8c;text-decoration:underline}a:focus{outline:dotted thin;outline:-webkit-focus-ring-color auto 5px;outline-offset:-2px}[role=button]{cursor:pointer}table{background-color:transparent}caption{padding-top:.75rem;padding-bottom:.75rem;color:#818a91;caption-side:bottom}label{display:inline-block}button,input,select,textarea{margin:0;line-height:inherit}fieldset{min-width:0;margin:0;border:0}legend{display:block;width:100%;margin-bottom:.5rem;font-size:1.5rem}.list-inline>li,output{display:inline-block}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit;margin-bottom:.5rem}.display-1,.display-2,.display-3,.display-4,.lead{font-weight:300}.blockquote,hr{margin-bottom:1rem}.h1,h1{font-size:2.5rem}.h2,h2{font-size:2rem}.h3,h3{font-size:1.75rem}.h4,h4{font-size:1.5rem}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}.lead{font-size:1.25rem}.display-1{font-size:3.5rem}.display-2{font-size:4.5rem}.display-3{font-size:5.5rem}.display-4{font-size:6rem}hr{margin-top:1rem;border:0;border-top:.0625rem solid rgba(0,0,0,.1)}.small,small{font-size:80%;font-weight:400}.mark,mark{padding:.2em;background-color:#fcf8e3}.list-inline,.list-unstyled{padding-left:0;list-style:none}.list-inline{margin-left:-5px}.list-inline>li{padding-right:5px;padding-left:5px}.dl-horizontal{margin-right:-1.875rem;margin-left:-1.875rem}.container,.container-fluid{margin-right:auto;margin-left:auto}.dl-horizontal:after,.dl-horizontal:before{display:table;content:" "}.initialism{font-size:90%;text-transform:uppercase}.blockquote{padding:.5rem 1rem;font-size:1.25rem;border-left:.25rem solid #eceeef}.blockquote ol:last-child,.blockquote p:last-child,.blockquote ul:last-child{margin-bottom:0}.blockquote footer{display:block;font-size:80%;line-height:1.5;color:#818a91}.blockquote footer:before{content:"\2014 \00A0"}.blockquote-reverse{padding-right:1rem;padding-left:0;text-align:right;border-right:.25rem solid #eceeef;border-left:0}.blockquote-reverse footer:before{content:""}.blockquote-reverse footer:after{content:"\00A0 \2014"}.figure{display:inline-block}.figure>img{margin-bottom:.5rem;line-height:1}.table,pre{margin-bottom:1rem}.figure-caption{font-size:90%;color:#818a91}.carousel-inner>.carousel-item>a>img,.carousel-inner>.carousel-item>img,.figure>img,.img-responsive{display:block;max-width:100%;height:auto}.img-rounded{border-radius:.3rem}.img-thumbnail{display:inline-block;max-width:100%;height:auto;padding:.25rem;line-height:1.5;background-color:#fff;border:1px solid #ddd;border-radius:.25rem;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out}code,kbd{padding:.2rem .4rem;font-size:90%}.img-circle{border-radius:50%}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{color:#bd4147;background-color:#f7f7f9;border-radius:.25rem}kbd{color:#fff;background-color:#333;border-radius:.2rem}kbd kbd{padding:0;font-size:100%;font-weight:700}pre{display:block;font-size:90%;line-height:1.5;color:#373a3c}.container-fluid:after,.container-fluid:before,.container:after,.container:before,.row:after,.row:before{display:table;content:" "}pre code{padding:0;font-size:inherit;color:inherit;background-color:transparent;border-radius:0}.container,.container-fluid{padding-right:.9375rem;padding-left:.9375rem}.pre-scrollable{max-height:340px;overflow-y:scroll}.row{margin-right:-.9375rem;margin-left:-.9375rem}.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{position:relative;min-height:1px;padding-right:.9375rem;padding-left:.9375rem}.col-xs-1{width:8.333333%}.col-xs-2{width:16.666667%}.col-xs-3{width:25%}.col-xs-4{width:33.333333%}.col-xs-5{width:41.666667%}.col-xs-6{width:50%}.col-xs-7{width:58.333333%}.col-xs-8{width:66.666667%}.col-xs-9{width:75%}.col-xs-10{width:83.333333%}.col-xs-11{width:91.666667%}.col-xs-12{width:100%}.col-xs-pull-0{right:auto}.col-xs-pull-1{right:8.333333%}.col-xs-pull-2{right:16.666667%}.col-xs-pull-3{right:25%}.col-xs-pull-4{right:33.333333%}.col-xs-pull-5{right:41.666667%}.col-xs-pull-6{right:50%}.col-xs-pull-7{right:58.333333%}.col-xs-pull-8{right:66.666667%}.col-xs-pull-9{right:75%}.col-xs-pull-10{right:83.333333%}.col-xs-pull-11{right:91.666667%}.col-xs-pull-12{right:100%}.col-xs-push-0{left:auto}.col-xs-push-1{left:8.333333%}.col-xs-push-2{left:16.666667%}.col-xs-push-3{left:25%}.col-xs-push-4{left:33.333333%}.col-xs-push-5{left:41.666667%}.col-xs-push-6{left:50%}.col-xs-push-7{left:58.333333%}.col-xs-push-8{left:66.666667%}.col-xs-push-9{left:75%}.col-xs-push-10{left:83.333333%}.col-xs-push-11{left:91.666667%}.col-xs-push-12{left:100%}.col-xs-offset-0{margin-left:0}.col-xs-offset-1{margin-left:8.333333%}.col-xs-offset-2{margin-left:16.666667%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-4{margin-left:33.333333%}.col-xs-offset-5{margin-left:41.666667%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-7{margin-left:58.333333%}.col-xs-offset-8{margin-left:66.666667%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-10{margin-left:83.333333%}.col-xs-offset-11{margin-left:91.666667%}.col-xs-offset-12{margin-left:100%}@media (min-width:34em){.container{max-width:34rem}.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9{float:left}.col-sm-1{width:8.333333%}.col-sm-2{width:16.666667%}.col-sm-3{width:25%}.col-sm-4{width:33.333333%}.col-sm-5{width:41.666667%}.col-sm-6{width:50%}.col-sm-7{width:58.333333%}.col-sm-8{width:66.666667%}.col-sm-9{width:75%}.col-sm-10{width:83.333333%}.col-sm-11{width:91.666667%}.col-sm-12{width:100%}.col-sm-pull-0{right:auto}.col-sm-pull-1{right:8.333333%}.col-sm-pull-2{right:16.666667%}.col-sm-pull-3{right:25%}.col-sm-pull-4{right:33.333333%}.col-sm-pull-5{right:41.666667%}.col-sm-pull-6{right:50%}.col-sm-pull-7{right:58.333333%}.col-sm-pull-8{right:66.666667%}.col-sm-pull-9{right:75%}.col-sm-pull-10{right:83.333333%}.col-sm-pull-11{right:91.666667%}.col-sm-pull-12{right:100%}.col-sm-push-0{left:auto}.col-sm-push-1{left:8.333333%}.col-sm-push-2{left:16.666667%}.col-sm-push-3{left:25%}.col-sm-push-4{left:33.333333%}.col-sm-push-5{left:41.666667%}.col-sm-push-6{left:50%}.col-sm-push-7{left:58.333333%}.col-sm-push-8{left:66.666667%}.col-sm-push-9{left:75%}.col-sm-push-10{left:83.333333%}.col-sm-push-11{left:91.666667%}.col-sm-push-12{left:100%}.col-sm-offset-0{margin-left:0}.col-sm-offset-1{margin-left:8.333333%}.col-sm-offset-2{margin-left:16.666667%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-4{margin-left:33.333333%}.col-sm-offset-5{margin-left:41.666667%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-7{margin-left:58.333333%}.col-sm-offset-8{margin-left:66.666667%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-10{margin-left:83.333333%}.col-sm-offset-11{margin-left:91.666667%}.col-sm-offset-12{margin-left:100%}}@media (min-width:48em){.container{max-width:45rem}.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9{float:left}.col-md-1{width:8.333333%}.col-md-2{width:16.666667%}.col-md-3{width:25%}.col-md-4{width:33.333333%}.col-md-5{width:41.666667%}.col-md-6{width:50%}.col-md-7{width:58.333333%}.col-md-8{width:66.666667%}.col-md-9{width:75%}.col-md-10{width:83.333333%}.col-md-11{width:91.666667%}.col-md-12{width:100%}.col-md-pull-0{right:auto}.col-md-pull-1{right:8.333333%}.col-md-pull-2{right:16.666667%}.col-md-pull-3{right:25%}.col-md-pull-4{right:33.333333%}.col-md-pull-5{right:41.666667%}.col-md-pull-6{right:50%}.col-md-pull-7{right:58.333333%}.col-md-pull-8{right:66.666667%}.col-md-pull-9{right:75%}.col-md-pull-10{right:83.333333%}.col-md-pull-11{right:91.666667%}.col-md-pull-12{right:100%}.col-md-push-0{left:auto}.col-md-push-1{left:8.333333%}.col-md-push-2{left:16.666667%}.col-md-push-3{left:25%}.col-md-push-4{left:33.333333%}.col-md-push-5{left:41.666667%}.col-md-push-6{left:50%}.col-md-push-7{left:58.333333%}.col-md-push-8{left:66.666667%}.col-md-push-9{left:75%}.col-md-push-10{left:83.333333%}.col-md-push-11{left:91.666667%}.col-md-push-12{left:100%}.col-md-offset-0{margin-left:0}.col-md-offset-1{margin-left:8.333333%}.col-md-offset-2{margin-left:16.666667%}.col-md-offset-3{margin-left:25%}.col-md-offset-4{margin-left:33.333333%}.col-md-offset-5{margin-left:41.666667%}.col-md-offset-6{margin-left:50%}.col-md-offset-7{margin-left:58.333333%}.col-md-offset-8{margin-left:66.666667%}.col-md-offset-9{margin-left:75%}.col-md-offset-10{margin-left:83.333333%}.col-md-offset-11{margin-left:91.666667%}.col-md-offset-12{margin-left:100%}}@media (min-width:62em){.container{max-width:60rem}.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9{float:left}.col-lg-1{width:8.333333%}.col-lg-2{width:16.666667%}.col-lg-3{width:25%}.col-lg-4{width:33.333333%}.col-lg-5{width:41.666667%}.col-lg-6{width:50%}.col-lg-7{width:58.333333%}.col-lg-8{width:66.666667%}.col-lg-9{width:75%}.col-lg-10{width:83.333333%}.col-lg-11{width:91.666667%}.col-lg-12{width:100%}.col-lg-pull-0{right:auto}.col-lg-pull-1{right:8.333333%}.col-lg-pull-2{right:16.666667%}.col-lg-pull-3{right:25%}.col-lg-pull-4{right:33.333333%}.col-lg-pull-5{right:41.666667%}.col-lg-pull-6{right:50%}.col-lg-pull-7{right:58.333333%}.col-lg-pull-8{right:66.666667%}.col-lg-pull-9{right:75%}.col-lg-pull-10{right:83.333333%}.col-lg-pull-11{right:91.666667%}.col-lg-pull-12{right:100%}.col-lg-push-0{left:auto}.col-lg-push-1{left:8.333333%}.col-lg-push-2{left:16.666667%}.col-lg-push-3{left:25%}.col-lg-push-4{left:33.333333%}.col-lg-push-5{left:41.666667%}.col-lg-push-6{left:50%}.col-lg-push-7{left:58.333333%}.col-lg-push-8{left:66.666667%}.col-lg-push-9{left:75%}.col-lg-push-10{left:83.333333%}.col-lg-push-11{left:91.666667%}.col-lg-push-12{left:100%}.col-lg-offset-0{margin-left:0}.col-lg-offset-1{margin-left:8.333333%}.col-lg-offset-2{margin-left:16.666667%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-4{margin-left:33.333333%}.col-lg-offset-5{margin-left:41.666667%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-7{margin-left:58.333333%}.col-lg-offset-8{margin-left:66.666667%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-10{margin-left:83.333333%}.col-lg-offset-11{margin-left:91.666667%}.col-lg-offset-12{margin-left:100%}}@media (min-width:75em){.container{max-width:72.25rem}.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9{float:left}.col-xl-1{width:8.333333%}.col-xl-2{width:16.666667%}.col-xl-3{width:25%}.col-xl-4{width:33.333333%}.col-xl-5{width:41.666667%}.col-xl-6{width:50%}.col-xl-7{width:58.333333%}.col-xl-8{width:66.666667%}.col-xl-9{width:75%}.col-xl-10{width:83.333333%}.col-xl-11{width:91.666667%}.col-xl-12{width:100%}.col-xl-pull-0{right:auto}.col-xl-pull-1{right:8.333333%}.col-xl-pull-2{right:16.666667%}.col-xl-pull-3{right:25%}.col-xl-pull-4{right:33.333333%}.col-xl-pull-5{right:41.666667%}.col-xl-pull-6{right:50%}.col-xl-pull-7{right:58.333333%}.col-xl-pull-8{right:66.666667%}.col-xl-pull-9{right:75%}.col-xl-pull-10{right:83.333333%}.col-xl-pull-11{right:91.666667%}.col-xl-pull-12{right:100%}.col-xl-push-0{left:auto}.col-xl-push-1{left:8.333333%}.col-xl-push-2{left:16.666667%}.col-xl-push-3{left:25%}.col-xl-push-4{left:33.333333%}.col-xl-push-5{left:41.666667%}.col-xl-push-6{left:50%}.col-xl-push-7{left:58.333333%}.col-xl-push-8{left:66.666667%}.col-xl-push-9{left:75%}.col-xl-push-10{left:83.333333%}.col-xl-push-11{left:91.666667%}.col-xl-push-12{left:100%}.col-xl-offset-0{margin-left:0}.col-xl-offset-1{margin-left:8.333333%}.col-xl-offset-2{margin-left:16.666667%}.col-xl-offset-3{margin-left:25%}.col-xl-offset-4{margin-left:33.333333%}.col-xl-offset-5{margin-left:41.666667%}.col-xl-offset-6{margin-left:50%}.col-xl-offset-7{margin-left:58.333333%}.col-xl-offset-8{margin-left:66.666667%}.col-xl-offset-9{margin-left:75%}.col-xl-offset-10{margin-left:83.333333%}.col-xl-offset-11{margin-left:91.666667%}.col-xl-offset-12{margin-left:100%}}.table{width:100%;max-width:100%}.table td,.table th{padding:.75rem;line-height:1.5;vertical-align:top;border-top:1px solid #eceeef}.table thead th{vertical-align:bottom;border-bottom:2px solid #eceeef}.table tbody+tbody{border-top:2px solid #eceeef}.table .table{background-color:#fff}.table-sm td,.table-sm th{padding:.3rem}.table-bordered,.table-bordered td,.table-bordered th{border:1px solid #eceeef}.table-bordered thead td,.table-bordered thead th{border-bottom-width:2px}.table-striped tbody tr:nth-of-type(odd){background-color:#f9f9f9}.table-active,.table-active>td,.table-active>th,.table-hover tbody tr:hover{background-color:#f5f5f5}.table-hover .table-active:hover,.table-hover .table-active:hover>td,.table-hover .table-active:hover>th{background-color:#e8e8e8}.table-success,.table-success>td,.table-success>th{background-color:#dff0d8}.table-hover .table-success:hover,.table-hover .table-success:hover>td,.table-hover .table-success:hover>th{background-color:#d0e9c6}.table-info,.table-info>td,.table-info>th{background-color:#d9edf7}.table-hover .table-info:hover,.table-hover .table-info:hover>td,.table-hover .table-info:hover>th{background-color:#c4e3f3}.table-warning,.table-warning>td,.table-warning>th{background-color:#fcf8e3}.table-hover .table-warning:hover,.table-hover .table-warning:hover>td,.table-hover .table-warning:hover>th{background-color:#faf2cc}.table-danger,.table-danger>td,.table-danger>th{background-color:#f2dede}.table-hover .table-danger:hover,.table-hover .table-danger:hover>td,.table-hover .table-danger:hover>th{background-color:#ebcccc}.table-responsive{display:block;width:100%;overflow-x:auto}.collapsing,.dropdown-divider,.embed-responsive,.modal,.modal-open,.navbar-divider{overflow:hidden}.thead-inverse th{color:#fff;background-color:#373a3c}.thead-default th{color:#55595c;background-color:#eceeef}.table-inverse{color:#eceeef;background-color:#373a3c}.table-inverse.table-bordered{border:0}.table-inverse td,.table-inverse th,.table-inverse thead th{border-color:#55595c}.table-reflow tbody{display:block;white-space:nowrap}.table-reflow td,.table-reflow th{border-top:1px solid #eceeef;border-left:1px solid #eceeef}.table-reflow td:last-child,.table-reflow th:last-child{border-right:1px solid #eceeef}.table-reflow tbody:last-child tr:last-child td,.table-reflow tbody:last-child tr:last-child th,.table-reflow tfoot:last-child tr:last-child td,.table-reflow tfoot:last-child tr:last-child th,.table-reflow thead:last-child tr:last-child td,.table-reflow thead:last-child tr:last-child th{border-bottom:1px solid #eceeef}.table-reflow tr td,.table-reflow tr th{display:block!important;border:1px solid #eceeef}.form-control,.form-control-file,.form-control-range{display:block}.form-control{width:100%;padding:.375rem .75rem;font-size:1rem;line-height:1.5;color:#55595c;background-color:#fff;background-image:none;border:.0625rem solid #ccc;border-radius:.25rem}.form-control::-ms-expand{background-color:transparent;border:0}.form-control:focus{border-color:#66afe9;outline:0}.form-control::-webkit-input-placeholder{color:#999;opacity:1}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999;opacity:1}.form-control::placeholder{color:#999;opacity:1}.has-success .checkbox,.has-success .checkbox-inline,.has-success .control-label,.has-success .form-control-feedback,.has-success .help-block,.has-success .radio,.has-success .radio-inline,.has-success.checkbox label,.has-success.checkbox-inline label,.has-success.radio label,.has-success.radio-inline label{color:#5cb85c}.form-control:disabled,.form-control[readonly],fieldset[disabled] .form-control{background-color:#eceeef;opacity:1}.form-control[disabled],fieldset[disabled] .form-control{cursor:not-allowed}.form-control-label{padding:.4375rem .75rem;margin-bottom:0}@media screen and (-webkit-min-device-pixel-ratio:0){input[type=date].form-control,input[type=time].form-control,input[type=datetime-local].form-control,input[type=month].form-control{line-height:2.375rem}.input-group-sm input[type=date].form-control,.input-group-sm input[type=time].form-control,.input-group-sm input[type=datetime-local].form-control,.input-group-sm input[type=month].form-control,input[type=date].input-sm,input[type=time].input-sm,input[type=datetime-local].input-sm,input[type=month].input-sm{line-height:1.95rem}.input-group-lg input[type=date].form-control,.input-group-lg input[type=time].form-control,.input-group-lg input[type=datetime-local].form-control,.input-group-lg input[type=month].form-control,input[type=date].input-lg,input[type=time].input-lg,input[type=datetime-local].input-lg,input[type=month].input-lg{line-height:3.291667rem}}.form-control-static{min-height:2.375rem;padding-top:.4375rem;padding-bottom:.4375rem;margin-bottom:0}.form-control-static.form-control-lg,.form-control-static.form-control-sm,.input-group-lg>.form-control-static.form-control,.input-group-lg>.form-control-static.input-group-addon,.input-group-lg>.input-group-btn>.form-control-static.btn,.input-group-sm>.form-control-static.form-control,.input-group-sm>.form-control-static.input-group-addon,.input-group-sm>.input-group-btn>.form-control-static.btn{padding-right:0;padding-left:0}.form-control-sm,.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{padding:.275rem .75rem;font-size:.85rem;line-height:1.5;border-radius:.2rem}.form-control-lg,.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{padding:.75rem 1.25rem;font-size:1.25rem;line-height:1.333333;border-radius:.3rem}.form-group{margin-bottom:15px}.checkbox,.radio{position:relative;display:block;margin-bottom:.75rem}.checkbox label,.checkbox-inline,.radio label,.radio-inline{padding-left:1.25rem;margin-bottom:0;cursor:pointer;font-weight:400}.checkbox label input:only-child,.radio label input:only-child{position:static}.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox],.radio input[type=radio],.radio-inline input[type=radio]{position:absolute;margin-top:.25rem;margin-left:-1.25rem}.collapsing,.dropdown,.dropup{position:relative}.checkbox+.checkbox,.radio+.radio{margin-top:-.25rem}.checkbox-inline,.radio-inline{position:relative;display:inline-block;vertical-align:middle}.checkbox-inline+.checkbox-inline,.radio-inline+.radio-inline{margin-top:0;margin-left:.75rem}fieldset[disabled] input[type=checkbox],fieldset[disabled] input[type=radio],input[type=checkbox].disabled,input[type=checkbox]:disabled,input[type=radio].disabled,input[type=radio]:disabled{cursor:not-allowed}.checkbox-inline.disabled,.checkbox.disabled label,.radio-inline.disabled,.radio.disabled label,fieldset[disabled] .checkbox label,fieldset[disabled] .checkbox-inline,fieldset[disabled] .radio label,fieldset[disabled] .radio-inline{cursor:not-allowed}.form-control-error,.form-control-success,.form-control-warning{padding-right:2.25rem;background-repeat:no-repeat;background-position:center right .59375rem;-webkit-background-size:1.54375rem 1.54375rem;background-size:1.54375rem 1.54375rem}.has-success .form-control{border-color:#5cb85c}.has-success .input-group-addon{color:#5cb85c;background-color:#eaf6ea;border-color:#5cb85c}.has-warning .checkbox,.has-warning .checkbox-inline,.has-warning .control-label,.has-warning .form-control-feedback,.has-warning .help-block,.has-warning .radio,.has-warning .radio-inline,.has-warning.checkbox label,.has-warning.checkbox-inline label,.has-warning.radio label,.has-warning.radio-inline label{color:#f0ad4e}.has-success .form-control-success{background-image:url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkNoZWNrIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiIHk9IjBweCIgdmlld0JveD0iMCAwIDYxMiA3OTIiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDYxMiA3OTIiIHhtbDpzcGFjZT0icHJlc2VydmUiPjxwYXRoIGZpbGw9IiM1Q0I4NUMiIGQ9Ik0yMzMuOCw2MTAuMWMtMTMuMywwLTI1LjktNi4yLTM0LTE2LjlMOTAuNSw0NDguOEM3Ni4zLDQzMCw4MCw0MDMuMyw5OC44LDM4OS4xYzE4LjgtMTQuMyw0NS41LTEwLjUsNTkuOCw4LjNsNzEuOSw5NWwyMjAuOS0yNTAuNWMxMi41LTIwLDM4LjgtMjYuMSw1OC44LTEzLjZjMjAsMTIuNCwyNi4xLDM4LjcsMTMuNiw1OC44TDI3MCw1OTBjLTcuNCwxMi0yMC4yLDE5LjQtMzQuMywyMC4xQzIzNS4xLDYxMC4xLDIzNC41LDYxMC4xLDIzMy44LDYxMC4xeiIvPjwvc3ZnPg==)}.has-warning .form-control{border-color:#f0ad4e}.has-warning .input-group-addon{color:#f0ad4e;background-color:#fff;border-color:#f0ad4e}.has-error .checkbox,.has-error .checkbox-inline,.has-error .control-label,.has-error .form-control-feedback,.has-error .help-block,.has-error .radio,.has-error .radio-inline,.has-error.checkbox label,.has-error.checkbox-inline label,.has-error.radio label,.has-error.radio-inline label{color:#d9534f}.has-warning .form-control-warning{background-image:url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48c3ZnIHZlcnNpb249IjEuMSIgaWQ9Ildhcm5pbmciIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgNjEyIDc5MiIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgNjEyIDc5MiIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHBhdGggZmlsbD0iI0YwQUQ0RSIgZD0iTTYwMyw2NDAuMmwtMjc4LjUtNTA5Yy0zLjgtNi42LTEwLjgtMTAuNi0xOC41LTEwLjZzLTE0LjcsNC4xLTE4LjUsMTAuNkw5LDY0MC4yYy0zLjcsNi41LTMuNiwxNC40LDAuMiwyMC44YzMuOCw2LjUsMTAuOCwxMC40LDE4LjMsMTAuNGg1NTcuMWM3LjUsMCwxNC41LTMuOSwxOC4zLTEwLjRDNjA2LjYsNjU0LjYsNjA2LjcsNjQ2LjYsNjAzLDY0MC4yeiBNMzM2LjYsNjEwLjJoLTYxLjJWNTQ5aDYxLjJWNjEwLjJ6IE0zMzYuNiw1MDMuMWgtNjEuMlYzMDQuMmg2MS4yVjUwMy4xeiIvPjwvc3ZnPg==)}.has-error .form-control{border-color:#d9534f}.has-error .input-group-addon{color:#d9534f;background-color:#fdf7f7;border-color:#d9534f}.has-error .form-control-error{background-image:url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkNyb3NzIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiIHk9IjBweCIgdmlld0JveD0iMCAwIDYxMiA3OTIiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDYxMiA3OTIiIHhtbDpzcGFjZT0icHJlc2VydmUiPjxwYXRoIGZpbGw9IiNEOTUzNEYiIGQ9Ik00NDcsNTQ0LjRjLTE0LjQsMTQuNC0zNy42LDE0LjQtNTEuOSwwTDMwNiw0NTEuN2wtODkuMSw5Mi43Yy0xNC40LDE0LjQtMzcuNiwxNC40LTUxLjksMGMtMTQuNC0xNC40LTE0LjQtMzcuNiwwLTUxLjlsOTIuNC05Ni40TDE2NSwyOTkuNmMtMTQuNC0xNC40LTE0LjQtMzcuNiwwLTUxLjlzMzcuNi0xNC40LDUxLjksMGw4OS4yLDkyLjdsODkuMS05Mi43YzE0LjQtMTQuNCwzNy42LTE0LjQsNTEuOSwwYzE0LjQsMTQuNCwxNC40LDM3LjYsMCw1MS45TDM1NC43LDM5Nmw5Mi40LDk2LjRDNDYxLjQsNTA2LjgsNDYxLjQsNTMwLDQ0Nyw1NDQuNHoiLz48L3N2Zz4=)}.btn-danger-outline,.btn-info-outline,.btn-info.active,.btn-info:active,.btn-primary-outline,.btn-primary.active,.btn-primary:active,.btn-secondary-outline,.btn-secondary.active,.btn-secondary:active,.btn-success-outline,.btn-success.active,.btn-success:active,.btn-warning-outline,.btn-warning.active,.btn-warning:active,.btn.active,.btn:active,.open>.btn-info.dropdown-toggle,.open>.btn-primary.dropdown-toggle,.open>.btn-secondary.dropdown-toggle,.open>.btn-success.dropdown-toggle,.open>.btn-warning.dropdown-toggle{background-image:none}@media (min-width:34em){.form-inline .form-control-static,.form-inline .form-group{display:inline-block}.form-inline .control-label,.form-inline .form-group{margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .form-control,.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .checkbox,.form-inline .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .checkbox label,.form-inline .radio label{padding-left:0}.form-inline .checkbox input[type=checkbox],.form-inline .radio input[type=radio]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.btn-block,input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.btn{display:inline-block;padding:.375rem 1rem;font-size:1rem;font-weight:400;line-height:1.5;text-align:center;white-space:nowrap;vertical-align:middle;-ms-touch-action:manipulation;touch-action:manipulation;cursor:pointer;user-select:none;border:.0625rem solid transparent;border-radius:.25rem}.btn.active.focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn:active:focus,.btn:focus{outline:dotted thin;outline:-webkit-focus-ring-color auto 5px;outline-offset:-2px}.btn.focus,.btn:focus,.btn:hover{text-decoration:none}.btn.active,.btn:active{outline:0}.btn.disabled,.btn:disabled,fieldset[disabled] .btn{cursor:not-allowed;opacity:.65}a.btn.disaabled,fieldset[disabled] a.btn{pointer-events:none}.btn-primary{color:#fff;background-color:#0275d8;border-color:#0275d8}.btn-primary.active,.btn-primary.focus,.btn-primary:active,.btn-primary:focus,.btn-primary:hover,.open>.btn-primary.dropdown-toggle{color:#fff;background-color:#025aa5;border-color:#01549b}.btn-primary.disabled.focus,.btn-primary.disabled:focus,.btn-primary:disabled.focus,.btn-primary:disabled:focus,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:focus{background-color:#0275d8;border-color:#0275d8}.btn-primary.disabled:hover,.btn-primary:disabled:hover,fieldset[disabled] .btn-primary:hover{background-color:#0275d8;border-color:#0275d8}.btn-secondary{color:#373a3c;background-color:#fff;border-color:#ccc}.btn-secondary.active,.btn-secondary.focus,.btn-secondary:active,.btn-secondary:focus,.btn-secondary:hover,.open>.btn-secondary.dropdown-toggle{color:#373a3c;background-color:#e6e6e6;border-color:#adadad}.btn-secondary.disabled.focus,.btn-secondary.disabled:focus,.btn-secondary:disabled.focus,.btn-secondary:disabled:focus,fieldset[disabled] .btn-secondary.focus,fieldset[disabled] .btn-secondary:focus{background-color:#fff;border-color:#ccc}.btn-secondary.disabled:hover,.btn-secondary:disabled:hover,fieldset[disabled] .btn-secondary:hover{background-color:#fff;border-color:#ccc}.btn-info{color:#fff;background-color:#5bc0de;border-color:#5bc0de}.btn-info.active,.btn-info.focus,.btn-info:active,.btn-info:focus,.btn-info:hover,.open>.btn-info.dropdown-toggle{color:#fff;background-color:#31b0d5;border-color:#2aabd2}.btn-info.disabled.focus,.btn-info.disabled:focus,.btn-info:disabled.focus,.btn-info:disabled:focus,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:focus{background-color:#5bc0de;border-color:#5bc0de}.btn-info.disabled:hover,.btn-info:disabled:hover,fieldset[disabled] .btn-info:hover{background-color:#5bc0de;border-color:#5bc0de}.btn-success{color:#fff;background-color:#5cb85c;border-color:#5cb85c}.btn-success.active,.btn-success.focus,.btn-success:active,.btn-success:focus,.btn-success:hover,.open>.btn-success.dropdown-toggle{color:#fff;background-color:#449d44;border-color:#419641}.btn-success.disabled.focus,.btn-success.disabled:focus,.btn-success:disabled.focus,.btn-success:disabled:focus,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:focus{background-color:#5cb85c;border-color:#5cb85c}.btn-success.disabled:hover,.btn-success:disabled:hover,fieldset[disabled] .btn-success:hover{background-color:#5cb85c;border-color:#5cb85c}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#f0ad4e}.btn-warning.active,.btn-warning.focus,.btn-warning:active,.btn-warning:focus,.btn-warning:hover,.open>.btn-warning.dropdown-toggle{color:#fff;background-color:#ec971f;border-color:#eb9316}.btn-warning.disabled.focus,.btn-warning.disabled:focus,.btn-warning:disabled.focus,.btn-warning:disabled:focus,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:focus{background-color:#f0ad4e;border-color:#f0ad4e}.btn-warning.disabled:hover,.btn-warning:disabled:hover,fieldset[disabled] .btn-warning:hover{background-color:#f0ad4e;border-color:#f0ad4e}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d9534f}.btn-danger.active,.btn-danger.focus,.btn-danger:active,.btn-danger:focus,.btn-danger:hover,.open>.btn-danger.dropdown-toggle{color:#fff;background-color:#c9302c;border-color:#c12e2a}.btn-danger.active,.btn-danger:active,.open>.btn-danger.dropdown-toggle{background-image:none}.btn-danger.disabled.focus,.btn-danger.disabled:focus,.btn-danger:disabled.focus,.btn-danger:disabled:focus,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:focus{background-color:#d9534f;border-color:#d9534f}.btn-danger.disabled:hover,.btn-danger:disabled:hover,fieldset[disabled] .btn-danger:hover{background-color:#d9534f;border-color:#d9534f}.btn-primary-outline{color:#0275d8;background-color:transparent;border-color:#0275d8}.btn-primary-outline.active,.btn-primary-outline.focus,.btn-primary-outline:active,.btn-primary-outline:focus,.btn-primary-outline:hover,.open>.btn-primary-outline.dropdown-toggle{color:#fff;background-color:#0275d8;border-color:#0275d8}.btn-primary-outline.disabled.focus,.btn-primary-outline.disabled:focus,.btn-primary-outline:disabled.focus,.btn-primary-outline:disabled:focus,fieldset[disabled] .btn-primary-outline.focus,fieldset[disabled] .btn-primary-outline:focus{border-color:#43a7fd}.btn-primary-outline.disabled:hover,.btn-primary-outline:disabled:hover,fieldset[disabled] .btn-primary-outline:hover{border-color:#43a7fd}.btn-secondary-outline{color:#ccc;background-color:transparent;border-color:#ccc}.btn-secondary-outline.active,.btn-secondary-outline.focus,.btn-secondary-outline:active,.btn-secondary-outline:focus,.btn-secondary-outline:hover,.open>.btn-secondary-outline.dropdown-toggle{color:#fff;background-color:#ccc;border-color:#ccc}.btn-secondary-outline.disabled.focus,.btn-secondary-outline.disabled:focus,.btn-secondary-outline:disabled.focus,.btn-secondary-outline:disabled:focus,fieldset[disabled] .btn-secondary-outline.focus,fieldset[disabled] .btn-secondary-outline:focus{border-color:#fff}.btn-secondary-outline.disabled:hover,.btn-secondary-outline:disabled:hover,fieldset[disabled] .btn-secondary-outline:hover{border-color:#fff}.btn-info-outline{color:#5bc0de;background-color:transparent;border-color:#5bc0de}.btn-info-outline.active,.btn-info-outline.focus,.btn-info-outline:active,.btn-info-outline:focus,.btn-info-outline:hover,.open>.btn-info-outline.dropdown-toggle{color:#fff;background-color:#5bc0de;border-color:#5bc0de}.btn-info-outline.disabled.focus,.btn-info-outline.disabled:focus,.btn-info-outline:disabled.focus,.btn-info-outline:disabled:focus,fieldset[disabled] .btn-info-outline.focus,fieldset[disabled] .btn-info-outline:focus{border-color:#b0e1ef}.btn-info-outline.disabled:hover,.btn-info-outline:disabled:hover,fieldset[disabled] .btn-info-outline:hover{border-color:#b0e1ef}.btn-success-outline{color:#5cb85c;background-color:transparent;border-color:#5cb85c}.btn-success-outline.active,.btn-success-outline.focus,.btn-success-outline:active,.btn-success-outline:focus,.btn-success-outline:hover,.open>.btn-success-outline.dropdown-toggle{color:#fff;background-color:#5cb85c;border-color:#5cb85c}.btn-success-outline.disabled.focus,.btn-success-outline.disabled:focus,.btn-success-outline:disabled.focus,.btn-success-outline:disabled:focus,fieldset[disabled] .btn-success-outline.focus,fieldset[disabled] .btn-success-outline:focus{border-color:#a3d7a3}.btn-success-outline.disabled:hover,.btn-success-outline:disabled:hover,fieldset[disabled] .btn-success-outline:hover{border-color:#a3d7a3}.btn-warning-outline{color:#f0ad4e;background-color:transparent;border-color:#f0ad4e}.btn-warning-outline.active,.btn-warning-outline.focus,.btn-warning-outline:active,.btn-warning-outline:focus,.btn-warning-outline:hover,.open>.btn-warning-outline.dropdown-toggle{color:#fff;background-color:#f0ad4e;border-color:#f0ad4e}.btn-warning-outline.disabled.focus,.btn-warning-outline.disabled:focus,.btn-warning-outline:disabled.focus,.btn-warning-outline:disabled:focus,fieldset[disabled] .btn-warning-outline.focus,fieldset[disabled] .btn-warning-outline:focus{border-color:#f8d9ac}.btn-warning-outline.disabled:hover,.btn-warning-outline:disabled:hover,fieldset[disabled] .btn-warning-outline:hover{border-color:#f8d9ac}.btn-danger-outline{color:#d9534f;background-color:transparent;border-color:#d9534f}.btn-danger-outline.active,.btn-danger-outline.focus,.btn-danger-outline:active,.btn-danger-outline:focus,.btn-danger-outline:hover,.open>.btn-danger-outline.dropdown-toggle{color:#fff;background-color:#d9534f;border-color:#d9534f}.btn-danger-outline.disabled.focus,.btn-danger-outline.disabled:focus,.btn-danger-outline:disabled.focus,.btn-danger-outline:disabled:focus,fieldset[disabled] .btn-danger-outline.focus,fieldset[disabled] .btn-danger-outline:focus{border-color:#eba5a3}.btn-danger-outline.disabled:hover,.btn-danger-outline:disabled:hover,fieldset[disabled] .btn-danger-outline:hover{border-color:#eba5a3}.btn-link{font-weight:400;color:#0275d8;border-radius:0}.btn-link,.btn-link.active,.btn-link:active,.btn-link:disabled,fieldset[disabled] .btn-link{background-color:transparent}.btn-link,.btn-link:active,.btn-link:focus,.btn-link:hover{border-color:transparent}.btn-link:focus,.btn-link:hover{color:#014c8c;text-decoration:underline;background-color:transparent}.btn-link:disabled:focus,.btn-link:disabled:hover,fieldset[disabled] .btn-link:focus,fieldset[disabled] .btn-link:hover{color:#818a91;text-decoration:none}.btn-group-lg>.btn,.btn-lg{padding:.75rem 1.25rem;font-size:1.25rem;line-height:1.333333;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .75rem;font-size:.85rem;line-height:1.5;border-radius:.2rem}.btn-block{display:block}.btn-block+.btn-block{margin-top:5px}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}.collapsing{height:0;-webkit-transition-timing-function:ease;-o-transition-timing-function:ease;transition-timing-function:ease;-webkit-transition-duration:.35s;-o-transition-duration:.35s;transition-duration:.35s;-webkit-transition-property:height;-o-transition-property:height;transition-property:height}.dropdown-toggle:after{display:inline-block;width:0;height:0;margin-left:.25rem;vertical-align:middle;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-left:.3em solid transparent}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;min-width:160px;padding:5px 0;margin:2px 0 0;font-size:1rem;text-align:left;list-style:none;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropdown-header,.dropdown-item{display:block;padding:3px 20px;line-height:1.5;white-space:nowrap}.dropdown-divider{height:1px;margin:.5rem 0;background-color:#e5e5e5}.dropdown-item{width:100%;font-weight:400;color:#373a3c;text-align:inherit;background:0 0;border:0}.c-indicator,.label,.pager{text-align:center}.dropdown-item:focus,.dropdown-item:hover{color:#2b2d2f;text-decoration:none;background-color:#f5f5f5}.dropdown-item.active,.dropdown-item.active:focus,.dropdown-item.active:hover{color:#fff;text-decoration:none;background-color:#0275d8;outline:0}.dropdown-item.disabled,.dropdown-item.disabled:focus,.dropdown-item.disabled:hover{color:#818a91}.dropdown-item.disabled:focus,.dropdown-item.disabled:hover{text-decoration:none;cursor:not-allowed;background-color:transparent;background-image:none;filter:"progid:DXImageTransform.Microsoft.gradient(enabled = false)"}.c-input,.file{cursor:pointer}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{right:0;left:auto}.dropdown-menu-left{right:auto;left:0}.dropdown-header{font-size:.85rem;color:#818a91}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover,.input-group-btn>.btn:active,.input-group-btn>.btn:focus,.input-group-btn>.btn:hover{z-index:2}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{content:"";border-top:0;border-bottom:.3em solid}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:2px}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;float:left}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar:after,.btn-toolbar:before{display:table;content:" "}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn .caret,.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-bottom-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-right:8px;padding-left:8px}.btn-group-lg.btn-group>.btn+.dropdown-toggle,.btn-group>.btn-lg+.dropdown-toggle{padding-right:12px;padding-left:12px}.btn-group-lg>.btn .caret,.btn-lg .caret{border-width:.3em .3em 0}.dropup .btn-group-lg>.btn .caret,.dropup .btn-lg .caret{border-width:0 .3em .3em}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group:after,.btn-group-vertical>.btn-group:before{display:table;content:" "}.btn-group-vertical>.btn-group:after{clear:both}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-right-radius:.25rem;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-left-radius:0;border-top-right-radius:0;border-bottom-left-radius:.25rem}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-top-right-radius:0}[data-toggle=buttons]>.btn input[type=checkbox],[data-toggle=buttons]>.btn input[type=radio],[data-toggle=buttons]>.btn-group>.btn input[type=checkbox],[data-toggle=buttons]>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.c-input,.input-group,.input-group-btn,.input-group-btn>.btn{position:relative}.input-group{display:table;border-collapse:separate}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group .form-control,.input-group-addon,.input-group-btn{display:table-cell}.input-group .form-control:not(:first-child):not(:last-child),.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1;color:#55595c;text-align:center;background-color:#eceeef;border:1px solid #ccc;border-radius:.25rem}.alert-link,.close,.label{font-weight:700}.input-group-addon.form-control-sm,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.input-group-addon.btn{padding:.275rem .75rem;font-size:.85rem;border-radius:.2rem}.input-group-addon.form-control-lg,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.input-group-addon.btn{padding:1.25rem;font-size:1.25rem;border-radius:.3rem}.input-group-addon input[type=checkbox],.input-group-addon input[type=radio]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn-group:not(:last-child)>.btn,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:first-child>.btn-group:not(:first-child)>.btn,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{font-size:0;white-space:nowrap}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{z-index:2;margin-left:-1px}.c-input{display:inline;padding-left:1.5rem;color:#555}.c-input>input{position:absolute;z-index:-1;opacity:0}.c-input>input:checked~.c-indicator{color:#fff;background-color:#0074d9}.c-input>input:active~.c-indicator{color:#fff;background-color:#84c6ff}.c-input+.c-input{margin-left:1rem}.c-indicator{position:absolute;top:0;left:0;display:block;width:1rem;height:1rem;font-size:65%;line-height:1rem;color:#eee;user-select:none;background-color:#eee;background-repeat:no-repeat;background-position:center center;-webkit-background-size:50% 50%;background-size:50% 50%}.c-checkbox .c-indicator{border-radius:.25rem}.c-checkbox input:checked~.c-indicator{background-image:url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAxNy4xLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB2aWV3Qm94PSIwIDAgOCA4IiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDAgMCA4IDgiIHhtbDpzcGFjZT0icHJlc2VydmUiPg0KPHBhdGggZmlsbD0iI0ZGRkZGRiIgZD0iTTYuNCwxTDUuNywxLjdMMi45LDQuNUwyLjEsMy43TDEuNCwzTDAsNC40bDAuNywwLjdsMS41LDEuNWwwLjcsMC43bDAuNy0wLjdsMy41LTMuNWwwLjctMC43TDYuNCwxTDYuNCwxeiINCgkvPg0KPC9zdmc+DQo=)}.c-checkbox input:indeterminate~.c-indicator{background-color:#0074d9;background-image:url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAxNy4xLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB3aWR0aD0iOHB4IiBoZWlnaHQ9IjhweCIgdmlld0JveD0iMCAwIDggOCIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgOCA4IiB4bWw6c3BhY2U9InByZXNlcnZlIj4NCjxwYXRoIGZpbGw9IiNGRkZGRkYiIGQ9Ik0wLDN2Mmg4VjNIMHoiLz4NCjwvc3ZnPg0K)}.c-radio .c-indicator{border-radius:50%}.c-radio input:checked~.c-indicator{background-image:url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAxNy4xLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB2aWV3Qm94PSIwIDAgOCA4IiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDAgMCA4IDgiIHhtbDpzcGFjZT0icHJlc2VydmUiPg0KPHBhdGggZmlsbD0iI0ZGRkZGRiIgZD0iTTQsMUMyLjMsMSwxLDIuMywxLDRzMS4zLDMsMywzczMtMS4zLDMtM1M1LjcsMSw0LDF6Ii8+DQo8L3N2Zz4NCg==)}.c-inputs-stacked .c-input{display:inline}.c-inputs-stacked .c-input:after{display:block;margin-bottom:.25rem;content:""}.c-select,.file{display:inline-block}.c-inputs-stacked .c-input+.c-input{margin-left:0}.c-select{max-width:100%;-webkit-appearance:none;padding:.375rem 1.75rem .375rem .75rem;padding-right:.75rem\9;vertical-align:middle;background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAUCAMAAACzvE1FAAAADFBMVEUzMzMzMzMzMzMzMzMKAG/3AAAAA3RSTlMAf4C/aSLHAAAAPElEQVR42q3NMQ4AIAgEQTn//2cLdRKppSGzBYwzVXvznNWs8C58CiussPJj8h6NwgorrKRdTvuV9v16Afn0AYFOB7aYAAAAAElFTkSuQmCC) right .75rem center no-repeat #fff;background-image:none\9;-webkit-background-size:8px 10px;background-size:8px 10px;border:1px solid #ccc;-moz-appearance:none;appearance:none}.c-select:focus{border-color:#51a7e8;outline:0;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.075),0 0 5px rgba(81,167,232,.5);box-shadow:inset 0 1px 2px rgba(0,0,0,.075),0 0 5px rgba(81,167,232,.5)}.c-select::-ms-expand{opacity:0}.c-select-sm{padding-top:3px;padding-bottom:3px;font-size:12px}.c-select-sm:not([multiple]){height:26px;min-height:26px}.file{position:relative;height:2.5rem}.file-custom,.file-custom:before{position:absolute;height:2.5rem;padding:.5rem 1rem;line-height:1.5;color:#555}.file input{min-width:14rem;margin:0;filter:alpha(opacity=0);opacity:0}.file-custom{top:0;right:0;left:0;z-index:5;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:#fff;border:.075rem solid #ddd;border-radius:.25rem;-webkit-box-shadow:inset 0 .2rem .4rem rgba(0,0,0,.05);box-shadow:inset 0 .2rem .4rem rgba(0,0,0,.05)}.file-custom:after{content:"Choose file..."}.file-custom:before{top:-.075rem;right:-.075rem;bottom:-.075rem;z-index:6;display:block;content:"Browse";background-color:#eee;border:.075rem solid #ddd;border-radius:0 .25rem .25rem 0}.file input:focus~.file-custom{-webkit-box-shadow:0 0 0 .075rem #fff,0 0 0 .2rem #0074d9;box-shadow:0 0 0 .075rem #fff,0 0 0 .2rem #0074d9}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:inline-block}.nav-link:focus,.nav-link:hover{text-decoration:none}.nav-link.disabled{color:#818a91}.nav-link.disabled,.nav-link.disabled:focus,.nav-link.disabled:hover{color:#818a91;cursor:not-allowed;background-color:transparent}.nav-inline .nav-link+.nav-link{margin-left:1rem}.nav-pills .nav-item+.nav-item,.nav-tabs .nav-item+.nav-item{margin-left:.2rem}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs:after,.nav-tabs:before{display:table;content:" "}.nav-tabs .nav-item{float:left;margin-bottom:-1px}.nav-tabs .nav-link{display:block;padding:.5em 1em;border:1px solid transparent;border-radius:.25rem .25rem 0 0}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#eceeef #eceeef #ddd}.nav-tabs .nav-link.disabled,.nav-tabs .nav-link.disabled:focus,.nav-tabs .nav-link.disabled:hover{color:#818a91;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.open .nav-link,.nav-tabs .nav-item.open .nav-link:focus,.nav-tabs .nav-item.open .nav-link:hover,.nav-tabs .nav-link.active,.nav-tabs .nav-link.active:focus,.nav-tabs .nav-link.active:hover{color:#55595c;background-color:#fff;border-color:#ddd #ddd transparent}.nav-pills .nav-item{float:left}.nav-pills .nav-link{display:block;padding:.5em 1em;border-radius:.25rem}.nav-pills .nav-item.open .nav-link,.nav-pills .nav-item.open .nav-link:focus,.nav-pills .nav-item.open .nav-link:hover,.nav-pills .nav-link.active,.nav-pills .nav-link.active:focus,.nav-pills .nav-link.active:hover{color:#fff;cursor:default;background-color:#0275d8}.nav-stacked .nav-item{display:block;float:none}.nav-stacked .nav-item+.nav-item{margin-top:.2rem;margin-left:0}.navbar-divider,.navbar-nav .nav-item+.nav-item,.navbar-nav .nav-link+.nav-link{margin-left:1rem}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.navbar{position:relative;padding:.5rem 1rem}.navbar:after,.navbar:before{display:table;content:" "}.navbar-static-top{z-index:1000}.navbar-fixed-bottom,.navbar-fixed-top{position:fixed;right:0;left:0;z-index:1030;margin-bottom:0}.card,.card-title{margin-bottom:.75rem}.navbar-fixed-top{top:0}.navbar-fixed-bottom{bottom:0}.navbar-sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1030;width:100%}@media (min-width:34em){.navbar{border-radius:.25rem}.navbar-fixed-bottom,.navbar-fixed-top,.navbar-static-top,.navbar-sticky-top{border-radius:0}}.navbar-brand{float:left;padding-top:.25rem;padding-bottom:.25rem;margin-right:1rem;font-size:1.25rem}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-brand>img{display:block}.navbar-divider{float:left;width:1px;padding-top:.425rem;padding-bottom:.425rem;margin-right:1rem}.navbar-divider:before{content:'\00a0'}.navbar-toggler{padding:.5rem .75rem;font-size:1.25rem;line-height:1;background:0 0;border:.0625rem solid transparent;border-radius:.25rem}.navbar-toggler:focus,.navbar-toggler:hover{text-decoration:none}@media (min-width:34em){.navbar-toggleable-xs{display:block!important}}@media (min-width:48em){.navbar-toggleable-sm{display:block!important}}.navbar-nav .nav-item{float:left}.navbar-nav .nav-link{display:block;padding-top:.425rem;padding-bottom:.425rem}.navbar-light .navbar-brand,.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.8)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.6)}.navbar-light .navbar-nav .active>.nav-link,.navbar-light .navbar-nav .active>.nav-link:focus,.navbar-light .navbar-nav .active>.nav-link:hover,.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .nav-link.active:focus,.navbar-light .navbar-nav .nav-link.active:hover,.navbar-light .navbar-nav .nav-link.open,.navbar-light .navbar-nav .nav-link.open:focus,.navbar-light .navbar-nav .nav-link.open:hover,.navbar-light .navbar-nav .open>.nav-link,.navbar-light .navbar-nav .open>.nav-link:focus,.navbar-light .navbar-nav .open>.nav-link:hover{color:rgba(0,0,0,.8)}.navbar-light .navbar-divider{background-color:rgba(0,0,0,.075)}.navbar-dark .navbar-brand,.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,.5)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.card-inverse .card-blockquote,.card-inverse .card-footer,.card-inverse .card-header,.card-inverse .card-title,.navbar-dark .navbar-nav .active>.nav-link,.navbar-dark .navbar-nav .active>.nav-link:focus,.navbar-dark .navbar-nav .active>.nav-link:hover,.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .nav-link.active:focus,.navbar-dark .navbar-nav .nav-link.active:hover,.navbar-dark .navbar-nav .nav-link.open,.navbar-dark .navbar-nav .nav-link.open:focus,.navbar-dark .navbar-nav .nav-link.open:hover,.navbar-dark .navbar-nav .open>.nav-link,.navbar-dark .navbar-nav .open>.nav-link:focus,.navbar-dark .navbar-nav .open>.nav-link:hover{color:#fff}.navbar-dark .navbar-divider{background-color:rgba(255,255,255,.075)}.card{position:relative;border:.0625rem solid #e5e5e5;border-radius:.25rem}.card-block{padding:1.25rem}.card-footer,.card-header{padding:.75rem 1.25rem;background-color:#f5f5f5}.card-title{margin-top:0}.card-subtitle{margin-top:-.375rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1.25rem}.card>.list-group:first-child .list-group-item:first-child{border-radius:.25rem .25rem 0 0}.card>.list-group:last-child .list-group-item:last-child{border-radius:0 0 .25rem .25rem}.card-header{border-bottom:.0625rem solid #e5e5e5}.card-header:first-child{border-radius:.1875rem .1875rem 0 0}.card-footer{border-top:.0625rem solid #e5e5e5}.card-footer:last-child{border-radius:0 0 .1875rem .1875rem}.card-primary{background-color:#0275d8;border-color:#0275d8}.card-success{background-color:#5cb85c;border-color:#5cb85c}.card-info{background-color:#5bc0de;border-color:#5bc0de}.card-warning{background-color:#f0ad4e;border-color:#f0ad4e}.card-danger{background-color:#d9534f;border-color:#d9534f}.card-inverse .card-footer,.card-inverse .card-header{border-bottom:.075rem solid rgba(255,255,255,.2)}.card-inverse .card-blockquote>footer,.card-inverse .card-link,.card-inverse .card-text{color:rgba(255,255,255,.65)}.card-inverse .card-link:focus,.card-inverse .card-link:hover{color:#fff}.card-blockquote{padding:0;margin-bottom:0;border-left:0}.card-img{border-radius:.25rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1.25rem}.card-img-top{border-radius:.25rem .25rem 0 0}.card-img-bottom{border-radius:0 0 .25rem .25rem}.card-deck{display:table;table-layout:fixed;border-spacing:1.25rem 0}.card-deck .card{display:table-cell;width:1%;vertical-align:top}.card-columns .card,.progress{width:100%}.card-deck-wrapper{margin-right:-1.25rem;margin-left:-1.25rem}.card-group{display:table;width:100%;table-layout:fixed}.card-group .card{display:table-cell;vertical-align:top}.breadcrumb>li,.card-columns .card,.pagination{display:inline-block}.card-group .card+.card{margin-left:0;border-left:0}.card-group .card:first-child .card-img-top{border-top-right-radius:0}.card-group .card:first-child .card-img-bottom{border-bottom-right-radius:0}.card-group .card:last-child .card-img-top{border-top-left-radius:0}.card-group .card:last-child .card-img-bottom{border-bottom-left-radius:0}.card-group .card:not(:first-child):not(:last-child){border-radius:0}.card-group .card:not(:first-child):not(:last-child) .card-img-bottom,.card-group .card:not(:first-child):not(:last-child) .card-img-top{border-radius:0}.breadcrumb,.pagination{border-radius:.25rem;margin-bottom:1rem}.card-columns{-webkit-column-count:3;-moz-column-count:3;column-count:3;-webkit-column-gap:1.25rem;-moz-column-gap:1.25rem;column-gap:1.25rem}.breadcrumb{padding:.75rem 1rem;list-style:none;background-color:#eceeef}.breadcrumb>li+li:before{padding-right:.5rem;padding-left:.5rem;color:#818a91;content:"/ "}.breadcrumb>.active{color:#818a91}.pagination{padding-left:0;margin-top:1rem}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:.5rem .75rem;margin-left:-1px;line-height:1.5;color:#0275d8;text-decoration:none;background-color:#fff;border:1px solid #ddd}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.pagination>li:last-child>a,.pagination>li:last-child>span{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.pagination>li>a:focus,.pagination>li>a:hover,.pagination>li>span:focus,.pagination>li>span:hover{color:#014c8c;background-color:#eceeef;border-color:#ddd}.pagination>.active>a,.pagination>.active>a:focus,.pagination>.active>a:hover,.pagination>.active>span,.pagination>.active>span:focus,.pagination>.active>span:hover{z-index:2;color:#fff;cursor:default;background-color:#0275d8;border-color:#0275d8}.pagination>.disabled>a,.pagination>.disabled>a:focus,.pagination>.disabled>a:hover,.pagination>.disabled>span,.pagination>.disabled>span:focus,.pagination>.disabled>span:hover{color:#818a91;cursor:not-allowed;background-color:#fff;border-color:#ddd}.pagination-lg>li>a,.pagination-lg>li>span{padding:.75rem 1.5rem;font-size:1.25rem;line-height:1.333333}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm>li>a,.pagination-sm>li>span{padding:.275rem .75rem;font-size:.85rem;line-height:1.5}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.pager{padding-left:0;margin-top:1rem;margin-bottom:1rem;list-style:none}.pager:after,.pager:before{display:table;content:" "}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:focus,.pager li>a:hover{text-decoration:none;background-color:#eceeef}.pager .disabled>a,.pager .disabled>a:focus,.pager .disabled>a:hover,.pager .disabled>span{color:#818a91;cursor:not-allowed;background-color:#fff}.pager-next>a,.pager-next>span{float:right}.pager-prev>a,.pager-prev>span{float:left}.label{display:inline-block;padding:.25em .4em;font-size:75%;line-height:1;color:#fff;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.label:empty{display:none}.btn .label{position:relative;top:-1px}a.label:focus,a.label:hover{color:#fff;text-decoration:none;cursor:pointer}.label-pill{padding-right:.6em;padding-left:.6em;border-radius:1rem}.label-default{background-color:#818a91}.label-default[href]:focus,.label-default[href]:hover{background-color:#687077}.label-primary{background-color:#0275d8}.label-primary[href]:focus,.label-primary[href]:hover{background-color:#025aa5}.label-success{background-color:#5cb85c}.label-success[href]:focus,.label-success[href]:hover{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:focus,.label-info[href]:hover{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:focus,.label-warning[href]:hover{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:focus,.label-danger[href]:hover{background-color:#c9302c}.jumbotron{padding:2rem 1rem;margin-bottom:2rem;background-color:#eceeef;border-radius:.3rem}.jumbotron-hr{border-top-color:#d0d5d8}@media (min-width:34em){.jumbotron{padding:4rem 2rem}}.jumbotron-fluid{padding-right:0;padding-left:0;border-radius:0}.alert{padding:15px;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-heading{margin-top:0;color:inherit}.alert-dismissible{padding-right:35px}.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{color:#3c763d;background-color:#dff0d8;border-color:#d0e9c6}.alert-success hr{border-top-color:#c1e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{color:#31708f;background-color:#d9edf7;border-color:#bcdff1}.alert-info hr{border-top-color:#a6d5ec}.alert-info .alert-link{color:#245269}.alert-warning{color:#8a6d3b;background-color:#fcf8e3;border-color:#faf2cc}.alert-warning hr{border-top-color:#f7ecb5}.alert-warning .alert-link{color:#66512c}.alert-danger{color:#a94442;background-color:#f2dede;border-color:#ebcccc}.alert-danger hr{border-top-color:#e4b9b9}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}.progress{display:block;height:1rem;margin-bottom:1rem}.progress[value]{-webkit-appearance:none;color:#0074d9;border:0;-moz-appearance:none;appearance:none}.progress[value]::-webkit-progress-bar{background-color:#eee;border-radius:.25rem}.progress[value]::-webkit-progress-value::before{content:attr(value)}.progress[value]::-webkit-progress-value{background-color:#0074d9;border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.progress[value="100"]::-webkit-progress-value{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}@media screen and (min-width:0 \0){.progress{background-color:#eee;border-radius:.25rem}.progress-bar{display:inline-block;height:1rem;text-indent:-999rem;background-color:#0074d9;border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.progress[width^="0"]{min-width:2rem;color:#818a91;background-color:transparent;background-image:none}.progress[width="100%"]{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}}.progress-striped[value]::-webkit-progress-value{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);-webkit-background-size:1rem 1rem;background-size:1rem 1rem}.progress-striped[value]::-moz-progress-bar{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-animated[value]::-webkit-progress-value{-webkit-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-animated[value]::-moz-progress-bar{animation:progress-bar-stripes 2s linear infinite}.progress-success[value]::-webkit-progress-value{background-color:#5cb85c}.progress-success[value]::-moz-progress-bar{background-color:#5cb85c}@media screen and (min-width:0 \0){.progress-bar-striped{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);-webkit-background-size:1rem 1rem;background-size:1rem 1rem}.progress-animated .progress-bar-striped{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-success .progress-bar{background-color:#5cb85c}}.progress-info[value]::-webkit-progress-value{background-color:#5bc0de}.progress-info[value]::-moz-progress-bar{background-color:#5bc0de}@media screen and (min-width:0 \0){.progress-info .progress-bar{background-color:#5bc0de}}.progress-warning[value]::-webkit-progress-value{background-color:#f0ad4e}.progress-warning[value]::-moz-progress-bar{background-color:#f0ad4e}@media screen and (min-width:0 \0){.progress-warning .progress-bar{background-color:#f0ad4e}}.progress-danger[value]::-webkit-progress-value{background-color:#d9534f}.progress-danger[value]::-moz-progress-bar{background-color:#d9534f}@media screen and (min-width:0 \0){.progress-danger .progress-bar{background-color:#d9534f}}.media{margin-top:15px}.media:first-child{margin-top:0}.media,.media-body{overflow:hidden;zoom:1}.media-body{width:10000px}.media-body,.media-left,.media-right{display:table-cell;vertical-align:top}.media-middle{vertical-align:middle}.media-bottom{vertical-align:bottom}.media-object{display:block}.media-object.img-thumbnail{max-width:none}.media-right{padding-left:10px}.media-left{padding-right:10px}.media-heading{margin-top:0;margin-bottom:5px}.media-list{padding-left:0;list-style:none}.list-group{padding-left:0;margin-bottom:0}.list-group-item{position:relative;display:block;padding:.75rem 1.25rem;margin-bottom:-.0625rem;background-color:#fff;border:.0625rem solid #ddd}.list-group-item:first-child{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.list-group-flush .list-group-item{border-width:.0625rem 0;border-radius:0}a.list-group-item,button.list-group-item{width:100%;color:#555;text-align:inherit}a.list-group-item .list-group-item-heading,button.list-group-item .list-group-item-heading{color:#333}a.list-group-item:focus,a.list-group-item:hover,button.list-group-item:focus,button.list-group-item:hover{color:#555;text-decoration:none;background-color:#f5f5f5}.list-group-item.disabled,.list-group-item.disabled:focus,.list-group-item.disabled:hover{color:#818a91;cursor:not-allowed;background-color:#eceeef}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text{color:#818a91}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{z-index:2;color:#fff;background-color:#0275d8;border-color:#0275d8}.list-group-item.active .list-group-item-heading,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:focus .list-group-item-text,.list-group-item.active:hover .list-group-item-text{color:#a8d6fe}.list-group-item-state{color:#a94442;background-color:#f2dede}a.list-group-item-state,button.list-group-item-state{color:#a94442}a.list-group-item-state .list-group-item-heading,button.list-group-item-state .list-group-item-heading{color:inherit}a.list-group-item-state:focus,a.list-group-item-state:hover,button.list-group-item-state:focus,button.list-group-item-state:hover{color:#a94442;background-color:#ebcccc}a.list-group-item-state.active,a.list-group-item-state.active:focus,a.list-group-item-state.active:hover,button.list-group-item-state.active,button.list-group-item-state.active:focus,button.list-group-item-state.active:hover{color:#fff;background-color:#a94442;border-color:#a94442}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.embed-responsive{position:relative;display:block;height:0;padding:0}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-21by9{padding-bottom:42.857143%}.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive-4by3{padding-bottom:75%}.close{float:right;font-size:1.5rem;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.2}.close:focus,.close:hover{color:#000;text-decoration:none;cursor:pointer;opacity:.5}button.close{-webkit-appearance:none;padding:0;cursor:pointer;background:0 0;border:0}.modal-content,.popover{-webkit-background-clip:padding-box}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;display:none;-webkit-overflow-scrolling:touch;outline:0}.modal-footer:after,.modal-footer:before,.modal-header:after,.modal-header:before{display:table;content:" "}.modal.fade .modal-dialog{-webkit-transition:-webkit-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out;-webkit-transform:translate(0,-25%);-ms-transform:translate(0,-25%);-o-transform:translate(0,-25%);transform:translate(0,-25%)}.modal.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.in{opacity:.5}.modal-header{padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.5}.modal-body{position:relative;padding:15px}.modal-footer{padding:15px;text-align:right;border-top:1px solid #e5e5e5}.popover,.tooltip{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:.85rem;font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;line-break:auto;text-decoration:none}.popover,.tooltip{position:absolute;display:block}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:34em){.modal-dialog{width:600px;margin:30px auto}.modal-sm{width:300px}}@media (min-width:48em){.modal-lg{width:900px}}.tooltip{z-index:1070;text-align:start;opacity:0}.tooltip.in{opacity:.9}.tooltip.bs-tether-element-attached-bottom,.tooltip.tooltip-top{padding:5px 0;margin-top:-3px}.tooltip.bs-tether-element-attached-bottom .tooltip-arrow,.tooltip.tooltip-top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.bs-tether-element-attached-left,.tooltip.tooltip-right{padding:0 5px;margin-left:3px}.tooltip.bs-tether-element-attached-left .tooltip-arrow,.tooltip.tooltip-right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.bs-tether-element-attached-top,.tooltip.tooltip-bottom{padding:5px 0;margin-top:3px}.tooltip.bs-tether-element-attached-top .tooltip-arrow,.tooltip.tooltip-bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bs-tether-element-attached-right,.tooltip.tooltip-left{padding:0 5px;margin-left:-3px}.tooltip.bs-tether-element-attached-right .tooltip-arrow,.tooltip.tooltip-left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.popover{top:0;left:0;z-index:1060;max-width:276px;padding:1px;text-align:start;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.carousel-caption,.carousel-control{color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6)}.text-nowrap,.text-truncate{white-space:nowrap}.popover.bs-tether-element-attached-bottom,.popover.popover-top{margin-top:-10px}.popover.bs-tether-element-attached-bottom .popover-arrow,.popover.popover-top .popover-arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:rgba(0,0,0,.25);border-bottom-width:0}.popover.bs-tether-element-attached-bottom .popover-arrow:after,.popover.popover-top .popover-arrow:after{bottom:1px;margin-left:-10px;content:"";border-top-color:#fff;border-bottom-width:0}.popover.bs-tether-element-attached-left,.popover.popover-right{margin-left:10px}.popover.bs-tether-element-attached-left .popover-arrow,.popover.popover-right .popover-arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:rgba(0,0,0,.25);border-left-width:0}.popover.bs-tether-element-attached-left .popover-arrow:after,.popover.popover-right .popover-arrow:after{bottom:-10px;left:1px;content:"";border-right-color:#fff;border-left-width:0}.popover.bs-tether-element-attached-top,.popover.popover-bottom{margin-top:10px}.popover.bs-tether-element-attached-top .popover-arrow,.popover.popover-bottom .popover-arrow{top:-11px;left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:rgba(0,0,0,.25)}.popover.bs-tether-element-attached-top .popover-arrow:after,.popover.popover-bottom .popover-arrow:after{top:1px;margin-left:-10px;content:"";border-top-width:0;border-bottom-color:#fff}.popover.bs-tether-element-attached-right,.popover.popover-left{margin-left:-10px}.popover.bs-tether-element-attached-right .popover-arrow,.popover.popover-left .popover-arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:rgba(0,0,0,.25)}.popover.bs-tether-element-attached-right .popover-arrow:after,.popover.popover-left .popover-arrow:after{right:1px;bottom:-10px;content:"";border-right-width:0;border-left-color:#fff}.popover-title{padding:8px 14px;margin:0;font-size:1rem;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:-.7rem -.7rem 0 0}.popover-content{padding:9px 14px}.popover-arrow,.popover-arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.carousel,.carousel-inner{position:relative}.popover-arrow{border-width:11px}.popover-arrow:after{content:"";border-width:10px}.carousel-inner{width:100%;overflow:hidden}.carousel-inner>.carousel-item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.carousel-item>a>img,.carousel-inner>.carousel-item>img{line-height:1}@media all and (transform-3d),(-webkit-transform-3d){.carousel-inner>.carousel-item{-webkit-transition:-webkit-transform .6s ease-in-out;-o-transition:-o-transform .6s ease-in-out;transition:transform .6s ease-in-out;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;perspective:1000px}.carousel-inner>.carousel-item.active.right,.carousel-inner>.carousel-item.next{left:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}.carousel-inner>.carousel-item.active.left,.carousel-inner>.carousel-item.prev{left:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}.carousel-inner>.carousel-item.active,.carousel-inner>.carousel-item.next.left,.carousel-inner>.carousel-item.prev.right{left:0;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;bottom:0;left:0;width:15%;font-size:20px;opacity:.5}.carousel-control.left{background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.5)),to(rgba(0,0,0,.0001)));background-image:-webkit-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:linear-gradient(to right,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);background-repeat:repeat-x}.carousel-control.right{right:0;left:auto;background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.0001)),to(rgba(0,0,0,.5)));background-image:-webkit-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:linear-gradient(to right,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);background-repeat:repeat-x}.carousel-control:focus,.carousel-control:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control .icon-next,.carousel-control .icon-prev{position:absolute;top:50%;z-index:5;display:inline-block;width:20px;height:20px;margin-top:-10px;font-family:serif;line-height:1}.carousel-control .icon-prev{left:50%;margin-left:-10px}.carousel-control .icon-next{right:50%;margin-right:-10px}.carousel-control .icon-prev:before{content:"\2039"}.carousel-control .icon-next:before{content:"\203a"}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;padding-left:0;margin-left:-30%;text-align:center;list-style:none}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;cursor:pointer;background-color:transparent;border:1px solid #fff;border-radius:10px}.carousel-indicators .active{width:12px;height:12px;margin:0;background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px}.carousel-caption .btn,.text-hide{text-shadow:none}@media (min-width:34em){.carousel-control .icon-next,.carousel-control .icon-prev{width:30px;height:30px;margin-top:-15px;font-size:30px}.carousel-control .icon-prev{margin-left:-15px}.carousel-control .icon-next{margin-right:-15px}.carousel-caption{right:20%;left:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.clearfix:after,.clearfix:before{display:table;content:" "}.center-block{display:block;margin-right:auto;margin-left:auto}.hidden-xl-down,.hidden-xs-up,.visible-print-block,[hidden]{display:none!important}.pull-right{float:right!important}.pull-left{float:left!important}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.p-r-0,.p-x-0{padding-right:0!important}.p-l-0,.p-x-0{padding-left:0!important}.p-t-0,.p-y-0{padding-top:0!important}.p-b-0,.p-y-0{padding-bottom:0!important}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}.m-r-0,.m-x-0{margin-right:0!important}.m-l-0,.m-x-0{margin-left:0!important}.m-t-0,.m-y-0{margin-top:0!important}.m-b-0,.m-y-0{margin-bottom:0!important}.invisible{visibility:hidden}.text-hide{font:"0/0" a;color:transparent;background-color:transparent;border:0}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-truncate{overflow:hidden;text-overflow:ellipsis}.text-xs-left{text-align:left}.text-xs-right{text-align:right}.text-xs-center{text-align:center}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#818a91}.text-primary{color:#0275d8}a.text-primary:focus,a.text-primary:hover{color:#025aa5}.text-success{color:#5cb85c}a.text-success:focus,a.text-success:hover{color:#449d44}.text-info{color:#5bc0de}a.text-info:focus,a.text-info:hover{color:#31b0d5}.text-warning{color:#f0ad4e}a.text-warning:focus,a.text-warning:hover{color:#ec971f}.text-danger{color:#d9534f}a.text-danger:focus,a.text-danger:hover{color:#c9302c}.bg-inverse{color:#eceeef;background-color:#373a3c}.bg-faded{background-color:#f7f7f9}.bg-primary{color:#fff;background-color:#0275d8}a.bg-primary:focus,a.bg-primary:hover{background-color:#025aa5}.bg-success{color:#fff;background-color:#5cb85c}a.bg-success:focus,a.bg-success:hover{background-color:#449d44}.bg-info{color:#fff;background-color:#5bc0de}a.bg-info:focus,a.bg-info:hover{background-color:#31b0d5}.bg-warning{color:#fff;background-color:#f0ad4e}a.bg-warning:focus,a.bg-warning:hover{background-color:#ec971f}.bg-danger{color:#fff;background-color:#d9534f}a.bg-danger:focus,a.bg-danger:hover{background-color:#c9302c}.m-a-0{margin:0!important}.m-r,.m-x{margin-right:1rem!important}.m-l,.m-x{margin-left:1rem!important}.m-t,.m-y{margin-top:1rem!important}.m-b,.m-y{margin-bottom:1rem!important}.m-a{margin:1rem!important}.m-t-md,.m-y-md{margin-top:1.5rem!important}.m-b-md,.m-y-md{margin-bottom:1.5rem!important}.m-x-auto{margin-right:auto!important;margin-left:auto!important}.m-r-md,.m-x-md{margin-right:1.5rem!important}.m-l-md,.m-x-md{margin-left:1.5rem!important}.m-a-md{margin:1.5rem!important}.m-r-lg,.m-x-lg{margin-right:3rem!important}.m-l-lg,.m-x-lg{margin-left:3rem!important}.m-t-lg,.m-y-lg{margin-top:3rem!important}.m-b-lg,.m-y-lg{margin-bottom:3rem!important}.m-a-lg{margin:3rem!important}.p-a-0{padding:0!important}.p-r,.p-x{padding-right:1rem!important}.p-l,.p-x{padding-left:1rem!important}.p-t,.p-y{padding-top:1rem!important}.p-b,.p-y{padding-bottom:1rem!important}.p-a{padding:1rem!important}.p-r-md,.p-x-md{padding-right:1.5rem!important}.p-l-md,.p-x-md{padding-left:1.5rem!important}.p-t-md,.p-y-md{padding-top:1.5rem!important}.p-b-md,.p-y-md{padding-bottom:1.5rem!important}.p-a-md{padding:1.5rem!important}.p-r-lg,.p-x-lg{padding-right:3rem!important}.p-l-lg,.p-x-lg{padding-left:3rem!important}.p-t-lg,.p-y-lg{padding-top:3rem!important}.p-b-lg,.p-y-lg{padding-bottom:3rem!important}.p-a-lg{padding:3rem!important}.pos-f-t{position:fixed;top:0;right:0;left:0;z-index:1030}@media (max-width:33.9em){.hidden-xs-down{display:none!important}}@media (min-width:34em){.text-sm-left{text-align:left}.text-sm-right{text-align:right}.text-sm-center{text-align:center}.hidden-sm-up{display:none!important}}@media (max-width:47.9em){.hidden-sm-down{display:none!important}}@media (min-width:48em){.text-md-left{text-align:left}.text-md-right{text-align:right}.text-md-center{text-align:center}.hidden-md-up{display:none!important}}@media (max-width:61.9em){.hidden-md-down{display:none!important}}@media (min-width:62em){.text-lg-left{text-align:left}.text-lg-right{text-align:right}.text-lg-center{text-align:center}.hidden-lg-up{display:none!important}}@media (max-width:74.9em){.hidden-lg-down{display:none!important}}@media (min-width:75em){.text-xl-left{text-align:left}.text-xl-right{text-align:right}.text-xl-center{text-align:center}.hidden-xl-up{display:none!important}}@media print{.visible-print-block{display:block!important}}.visible-print-inline{display:none!important}@media print{.visible-print-inline{display:inline!important}}.visible-print-inline-block{display:none!important}@media print{.visible-print-inline-block{display:inline-block!important}.hidden-print .hidden-print{display:none!important}} \ No newline at end of file diff --git a/lib/public/stylesheets/style.css b/lib/public/stylesheets/style.css index b6d9ebe..da1b7fc 100644 --- a/lib/public/stylesheets/style.css +++ b/lib/public/stylesheets/style.css @@ -55,7 +55,6 @@ a.sponsor { a.navbar-brand.twitter { color: #55acee; - font-size: 16px; } a.navbar-brand.twitter:before { diff --git a/lib/views/index.html.ejs b/lib/views/index.html.ejs index a105664..8db6b9d 100644 --- a/lib/views/index.html.ejs +++ b/lib/views/index.html.ejs @@ -5,7 +5,7 @@ Crafatar - + @@ -19,10 +19,11 @@ -Fork me on GitHub + + Fork me on GitHub - From ab64f56c26a8a3910923358df3603e88532d3df2 Mon Sep 17 00:00:00 2001 From: jomo Date: Sat, 17 Oct 2015 20:41:06 +0200 Subject: [PATCH 73/86] s/parameters/modifiers --- lib/views/index.html.ejs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/views/index.html.ejs b/lib/views/index.html.ejs index 69f0902..77879c5 100644 --- a/lib/views/index.html.ejs +++ b/lib/views/index.html.ejs @@ -91,7 +91,7 @@
<%= domain %>/avatars/uuid
-

Accepted parameters: size, helm, default.

+

Accepted modifiers: size, helm, default.

@@ -107,7 +107,7 @@ <%= domain %>/renders/head/uuid

- Accepted parameters: scale, helm, default.
+ Accepted modifiers: scale, helm, default.
Please note renders are still beta and have some issues. New renders are in progress!

@@ -125,7 +125,7 @@ <%= domain %>/renders/body/uuid

- Accepted parameters: scale, helm, default.
+ Accepted modifiers: scale, helm, default.
Please note renders are still beta and have some issues. New renders are in progress!

@@ -142,7 +142,7 @@
<%= domain %>/skins/uuid
-

Accepted parameters: default.

+

Accepted modifiers: default.

@@ -157,7 +157,7 @@
<%= domain %>/capes/uuid
-

Accepted parameters: default.

+

Accepted modifiers: default.

From 3f06992cc1ab4f3083b65a739064874f88f463f8 Mon Sep 17 00:00:00 2001 From: jomo Date: Sat, 17 Oct 2015 20:43:22 +0200 Subject: [PATCH 74/86] fix slogan inconstency --- lib/views/index.html.ejs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/views/index.html.ejs b/lib/views/index.html.ejs index 77879c5..14b090c 100644 --- a/lib/views/index.html.ejs +++ b/lib/views/index.html.ejs @@ -1,12 +1,12 @@ - Crafatar – A blazing fast Minecraft avatar API + Crafatar – A blazing fast API for Minecraft faces! <%# FIXME: Use CDN %> - + @@ -16,7 +16,7 @@ - + From 44a6a4961d76ce25ee249166e2efd13b4dc6dae2 Mon Sep 17 00:00:00 2001 From: jomo Date: Sat, 17 Oct 2015 21:54:11 +0200 Subject: [PATCH 75/86] add famous crafatar users + tools & plugins --- lib/views/index.html.ejs | 407 +++++++++++++++++++++------------------ 1 file changed, 218 insertions(+), 189 deletions(-) diff --git a/lib/views/index.html.ejs b/lib/views/index.html.ejs index 14b090c..0966c83 100644 --- a/lib/views/index.html.ejs +++ b/lib/views/index.html.ejs @@ -56,220 +56,249 @@ -
-
-
-
-
-

Head Renders

-
-
- head -
-
-
- <%= domain %>/renders/head/uuid +
+

Body Renders

+
+
+ body
+
+
+ <%= domain %>/renders/body/uuid +
+

+ Accepted modifiers: scale, helm, default.
+ Please note renders are still beta and have some issues. New renders are in progress! +

+
+
+
+ +
+

Skins

+
+
+ skin +
+
+
+ <%= domain %>/skins/uuid +
+

Accepted modifiers: default.

+
+
+
+ +
+

Capes

+
+
+ cape +
+
+
+ <%= domain %>/capes/uuid +
+

Accepted modifiers: default.

+
+
+
+ +
+ +
+

Meta

+

+ In the examples above, you can generally use usernames instead of uuid. However, apart from the special cases MHF_Steve and MHF_Alex this is discouraged as explained below.
+ You can append .png or any other file extension to the URL path if you like to, but all images are PNG. +

+ +
+

Attribution

- Accepted modifiers: scale, helm, default.
- Please note renders are still beta and have some issues. New renders are in progress! + Attribution is encouraged but not required.
+ If you want to show some support for this (free!) service, place a notice like this somewhere: +

+ Thank you to <a href="https://crafatar.com">Crafatar</a> for providing avatars. +

-
-
-
+ -
-

Body Renders

-
-
- body -
-
-
- <%= domain %>/renders/body/uuid -
+
+

URL Parameters

- Accepted modifiers: scale, helm, default.
- Please note renders are still beta and have some issues. New renders are in progress! + You can tweak images using query string parameters.
+ Example: <%= domain %>/avatars/853c80ef3c3749fdaa49938b674adae6?size=4&default=MHF_Steve&helm

-
-
-
- -
-

Skins

-
-
- skin -
-
-
- <%= domain %>/skins/uuid -
-

Accepted modifiers: default.

-
-
-
- -
-

Capes

-
-
- cape -
-
-
- <%= domain %>/capes/uuid -
-

Accepted modifiers: default.

-
-
-
- -
- -
-

Meta

-

- In the examples above, you can generally use usernames instead of uuid. However, apart from the special cases MHF_Steve and MHF_Alex this is discouraged as explained below.
- You can append .png or any other file extension to the URL path if you like to, but all images are PNG. -

- -
-

Attribution

-

- Attribution is encouraged but not required.
- If you want to show some support for this (free!) service, place a notice like this somewhere: -

- Thank you to <a href="https://crafatar.com">Crafatar</a> for providing avatars. -
+
    +
  • size: The size of the image in pixels. <%= config.avatars.min_size %> - <%= config.avatars.max_size %> +
  • scale: The scale factor renders. <%= config.renders.min_scale %> - <%= config.renders.max_scale %> +
  • helm: Apply the overlay to the avatar. Presence of this parameter implies true. +
  • + default: The fallback to be used when the requested image cannot be served. You can use a custom URL or any uuid.
    + The option defaults to either MHF_Steve or MHF_Alex, depending on the requested UUID. All usernames default to MHF_Steve. +

+ +
+

About UUIDs

+

UUIDs may be any valid Mojang UUID in the blank or dashed format.

+

Malformed UUIDs are rejected.

+
+ +
+

About Usernames

+

+ We strongly advise you to use UUIDs instead of usernames! UUIDs never change while usernames do.
+ Looking up players by username has officially been deprecated by Mojang ever since UUIDs were introduced.
+ Crafatar uses a legacy API to retrieve skins for usernames that updates very slowly.
+ Skins come without any details, including whether a player uses the Alex or Steve skin model.
+ Additionally, Mojang has stated that this legacy interface may be disabled anytime, causing all requests to fail. +

+

Malformed usernames are rejected.

+
+ +
+

About Caching

+

Crafatar caches skins for <%= config.caching.local / 60 %> minutes before checking for skin updates.
+ Images are cached in your browser for <%= config.caching.browser / 60 %> minutes until a new request to Crafatar is made.
+ When you changed your skin you can try clearing your browser cache to see the change faster.

+
+ +
+

CORS

+

Crafatar supports Cross-Origin Resource Sharing, so you can make AJAX request from other sites!

+
+ +
+

HTTP Headers

+

+ Responses come with some custom HTTP headers, useful for debugging.
+ Please note that these headers are cached by CloudFlare (CF-Cache-Status: HIT). +

+ +
    +
  • + X-Storage-Type: Details about how the requested image was stored on the server +
      +
    • none: No external requests. Cached: User has no skin.
    • +
    • cached: No external requests. Skin cached and stored locally.
    • +
    • 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.
    • +
    • downloaded: 2 external requests. First request or skin changed, skin downloaded.
    • +
    • server error: This can happen, for example, when Mojang's servers are down.
      + If possible, a cached image is served instead.
    • +
    • user error: You have done something wrong, such as requesting a malformed uuid.
      + Check the response body for details.
    • +
    +
  • + X-Request-ID: The internal ID assigned to this request.
    + If you think something is wrong with your request, please contact us and provide this ID. +
+
-
-

URL Parameters

-

- You can tweak images using query string parameters.
- Example: <%= domain %>/avatars/853c80ef3c3749fdaa49938b674adae6?size=4&default=MHF_Steve&helm -

+
+

Contact

    -
  • size: The size of the image in pixels. <%= config.avatars.min_size %> - <%= config.avatars.max_size %> -
  • scale: The scale factor renders. <%= config.renders.min_scale %> - <%= config.renders.max_scale %> -
  • helm: Apply the overlay to the avatar. Presence of this parameter implies true. -
  • - default: The fallback to be used when the requested image cannot be served. You can use a custom URL or any uuid.
    - The option defaults to either MHF_Steve or MHF_Alex, depending on the requested UUID. All usernames default to MHF_Steve. -
-

- -
-

About UUIDs

-

UUIDs may be any valid Mojang UUID in the blank or dashed format.

-

Malformed UUIDs are rejected.

-
- -
-

About Usernames

-

- We strongly advise you to use UUIDs instead of usernames! UUIDs never change while usernames do.
- Looking up players by username has officially been deprecated by Mojang ever since UUIDs were introduced.
- Crafatar uses a legacy API to retrieve skins for usernames that updates very slowly.
- Skins come without any details, including whether a player uses the Alex or Steve skin model.
- Additionally, Mojang has stated that this legacy interface may be disabled anytime, causing all requests to fail. -

-

Malformed usernames are rejected.

-
- -
-

About Caching

-

Crafatar caches skins for <%= config.caching.local / 60 %> minutes before checking for skin updates.
- Images are cached in your browser for <%= config.caching.browser / 60 %> minutes until a new request to Crafatar is made.
- When you changed your skin you can try clearing your browser cache to see the change faster.

-
- -
-

CORS

-

Crafatar supports CORS, so you can make AJAX request from other sites!

-
- -
-

HTTP Headers

-

- Responses come with some custom HTTP headers, useful for debugging.
- Please note that these headers are cached by CloudFlare (CF-Cache-Status: HIT). -

- -
    -
  • - X-Storage-Type: Details about how the requested image was stored on the server -
      -
    • none: No external requests. Cached: User has no skin.
    • -
    • cached: No external requests. Skin cached and stored locally.
    • -
    • 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.
    • -
    • downloaded: 2 external requests. First request or skin changed, skin downloaded.
    • -
    • server error: This can happen, for example, when Mojang's servers are down.
      - If possible, a cached image is served instead.
    • -
    • user error: You have done something wrong, such as requesting a malformed uuid.
      - Check the response body for details.
    • -
    -
  • - X-Request-ID: The internal ID assigned to this request.
    - If you think something is wrong with your request, please contact us and provide this ID. +
  • Follow us on twitter @crafatar
  • +
  • Open an issue on GitHub
  • +
  • Join us in #crafatar on irc.esper.net
+
-
-

Contact

- -
- - - +

Crafatar Tools & Plugins

+ +

+ + +
+
+
+

Copyright Crafatar <%= new Date().getFullYear() %>

+
+
\ No newline at end of file From c915c6699f92dfe69ce6e078acbd782885d0e731 Mon Sep 17 00:00:00 2001 From: jomo Date: Sat, 17 Oct 2015 22:02:13 +0200 Subject: [PATCH 76/86] add CloudFlare caching notice --- lib/views/index.html.ejs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/views/index.html.ejs b/lib/views/index.html.ejs index 0966c83..248cab7 100644 --- a/lib/views/index.html.ejs +++ b/lib/views/index.html.ejs @@ -210,7 +210,7 @@

We strongly advise you to use UUIDs instead of usernames! UUIDs never change while usernames do.
Looking up players by username has officially been deprecated by Mojang ever since UUIDs were introduced.
- Crafatar uses a legacy API to retrieve skins for usernames that updates very slowly.
+ Crafatar uses a legacy API to retrieve skins for usernames that updates very slowly.
Skins come without any details, including whether a player uses the Alex or Steve skin model.
Additionally, Mojang has stated that this legacy interface may be disabled anytime, causing all requests to fail.

@@ -219,9 +219,12 @@

About Caching

-

Crafatar caches skins for <%= config.caching.local / 60 %> minutes before checking for skin updates.
+

+ Crafatar caches skins for <%= config.caching.local / 60 %> minutes before checking for skin updates.
Images are cached in your browser for <%= config.caching.browser / 60 %> minutes until a new request to Crafatar is made.
- When you changed your skin you can try clearing your browser cache to see the change faster.

+ In addition, CloudFlare caches up to 2 hours on a per-url basis. +

+

When you changed your skin you can try clearing your browser cache to see the change faster.

@@ -233,7 +236,7 @@

HTTP Headers

Responses come with some custom HTTP headers, useful for debugging.
- Please note that these headers are cached by CloudFlare (CF-Cache-Status: HIT). + Please note that these headers may be cached by CloudFlare.

    From 8a59757345a903393ccb0ff4a29b6f9b743ee733 Mon Sep 17 00:00:00 2001 From: jomo Date: Sat, 17 Oct 2015 22:11:48 +0200 Subject: [PATCH 77/86] fix word-wrap --- lib/public/stylesheets/style.css | 4 ++++ lib/views/index.html.ejs | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/public/stylesheets/style.css b/lib/public/stylesheets/style.css index 3efaa42..b54d0ec 100644 --- a/lib/public/stylesheets/style.css +++ b/lib/public/stylesheets/style.css @@ -116,6 +116,10 @@ h3 { margin-top: 2em; } +code { + word-wrap: break-word; +} + .code { display: block; font-family: monospace; diff --git a/lib/views/index.html.ejs b/lib/views/index.html.ejs index 248cab7..5dbb692 100644 --- a/lib/views/index.html.ejs +++ b/lib/views/index.html.ejs @@ -175,7 +175,7 @@

    Attribution

    - Attribution is encouraged but not required.
    + Attribution is not required, but it is encouraged.
    If you want to show some support for this (free!) service, place a notice like this somewhere:

    Thank you to <a href="https://crafatar.com">Crafatar</a> for providing avatars. From 08d82114689262c9650bd84015829bba849d251c Mon Sep 17 00:00:00 2001 From: jomo Date: Sat, 17 Oct 2015 22:13:52 +0200 Subject: [PATCH 78/86] add irc:// link --- lib/views/index.html.ejs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/views/index.html.ejs b/lib/views/index.html.ejs index 5dbb692..c2ff466 100644 --- a/lib/views/index.html.ejs +++ b/lib/views/index.html.ejs @@ -265,7 +265,7 @@
From 815e9c5ae97246a330962c9d477fd187770fbe60 Mon Sep 17 00:00:00 2001 From: jomo Date: Sat, 17 Oct 2015 22:38:38 +0200 Subject: [PATCH 79/86] word-foo --- lib/public/stylesheets/style.css | 6 +++--- lib/views/index.html.ejs | 32 ++++++++++++++++---------------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/lib/public/stylesheets/style.css b/lib/public/stylesheets/style.css index b54d0ec..416eb74 100644 --- a/lib/public/stylesheets/style.css +++ b/lib/public/stylesheets/style.css @@ -22,10 +22,10 @@ a.forkme { background: #008000; color: #fff; font-weight: bold; - padding: 3px 35px; + padding: 3px 100px; border: 2px solid #006400; - -webkit-transform: rotate(45deg) translate(62px); - transform: rotate(45deg) translate(62px); + -webkit-transform: rotate(45deg) translate(108px, -46px); + transform: rotate(45deg) translate(108px, -46px); } a.forkme:hover { diff --git a/lib/views/index.html.ejs b/lib/views/index.html.ejs index c2ff466..71aedda 100644 --- a/lib/views/index.html.ejs +++ b/lib/views/index.html.ejs @@ -64,7 +64,7 @@
Usernames are deprecated!
You should only use usernames for testing.
Updates are slower, some features are not available, and it may break anytime!
- We strongly advise you to use UUIDs instead of usernames. more info + We strongly advise you to use UUIDs instead of usernames. more info @@ -109,7 +109,7 @@

Accepted modifiers: scale, helm, default.
- Please note renders are still beta and have some issues. New renders are in progress! + Please note that renders are still beta and have some issues. New renders are in progress!

@@ -127,7 +127,7 @@

Accepted modifiers: scale, helm, default.
- Please note renders are still beta and have some issues. New renders are in progress! + Please note that renders are still beta and have some issues. New renders are in progress!

@@ -191,7 +191,7 @@

  • size: The size of the image in pixels. <%= config.avatars.min_size %> - <%= config.avatars.max_size %> -
  • scale: The scale factor renders. <%= config.renders.min_scale %> - <%= config.renders.max_scale %> +
  • scale: The scale factor for renders. <%= config.renders.min_scale %> - <%= config.renders.max_scale %>
  • helm: Apply the overlay to the avatar. Presence of this parameter implies true.
  • default: The fallback to be used when the requested image cannot be served. You can use a custom URL or any uuid.
    @@ -199,28 +199,28 @@

-
-

About UUIDs

+
+

About UUIDs

UUIDs may be any valid Mojang UUID in the blank or dashed format.

Malformed UUIDs are rejected.

-
-

About Usernames

+
+

About Usernames

We strongly advise you to use UUIDs instead of usernames! UUIDs never change while usernames do.
Looking up players by username has officially been deprecated by Mojang ever since UUIDs were introduced.
- Crafatar uses a legacy API to retrieve skins for usernames that updates very slowly.
+ Crafatar uses a legacy API which updates very slowly to retrieve skins for usernames.
Skins come without any details, including whether a player uses the Alex or Steve skin model.
Additionally, Mojang has stated that this legacy interface may be disabled anytime, causing all requests to fail.

Malformed usernames are rejected.

-
-

About Caching

+
+

About Caching

- Crafatar caches skins for <%= config.caching.local / 60 %> minutes before checking for skin updates.
+ Crafatar checks for skin updates every <%= config.caching.local / 60 %> minutes.
Images are cached in your browser for <%= config.caching.browser / 60 %> minutes until a new request to Crafatar is made.
In addition, CloudFlare caches up to 2 hours on a per-url basis.

@@ -243,11 +243,11 @@
  • X-Storage-Type: Details about how the requested image was stored on the server
      -
    • none: No external requests. Cached: User has no skin.
    • -
    • cached: No external requests. Skin cached and stored locally.
    • -
    • checked: 1 external request. Skin cached, checked for updates, no skin downloaded.
      +
    • none: No external requests. Player has no skin (cached)
    • +
    • cached: No external requests. (skin cached)
    • +
    • checked: Requested skin details, skin cached. (1 external request)
      This happens either when the user removed their skin or when it didn't change.
    • -
    • downloaded: 2 external requests. First request or skin changed, skin downloaded.
    • +
    • downloaded: Requested skin details, skin downloaded. (2 external requests)
    • server error: This can happen, for example, when Mojang's servers are down.
      If possible, a cached image is served instead.
    • user error: You have done something wrong, such as requesting a malformed uuid.
      From 1e979042e6e363ff32c2668d22b037e97f0b95cb Mon Sep 17 00:00:00 2001 From: jomo Date: Sat, 17 Oct 2015 23:20:40 +0200 Subject: [PATCH 80/86] fix avatar spacing in header --- lib/public/stylesheets/style.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/public/stylesheets/style.css b/lib/public/stylesheets/style.css index 416eb74..ce51894 100644 --- a/lib/public/stylesheets/style.css +++ b/lib/public/stylesheets/style.css @@ -145,13 +145,14 @@ code { #avatar-wrapper { height: 64px; overflow: hidden; + font-size: 0; } .avatar { width: 64px; height: 64px; display: inline-block; - margin-right: 0.5em; + margin-right: 6px; } .avatar.jomo {background-image: url("/avatars/ae795aa86327408e92ab25c8a59f3ba1?size=64")} From 36cb9f6b673cbc90a4772e5c00ddc847fd50516a Mon Sep 17 00:00:00 2001 From: jomo Date: Sat, 17 Oct 2015 23:22:56 +0200 Subject: [PATCH 81/86] fix famous users list bottom --- lib/views/index.html.ejs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/views/index.html.ejs b/lib/views/index.html.ejs index 71aedda..f0aa09e 100644 --- a/lib/views/index.html.ejs +++ b/lib/views/index.html.ejs @@ -281,8 +281,8 @@ NameMC The Nexus and many more… -

      See also: what users say about Crafatar

      +

      See also: what users say about Crafatar


      Crafatar Tools & Plugins

      From e8ab044d913fbacf71a4361a69bd3d0279d496eb Mon Sep 17 00:00:00 2001 From: jomo Date: Sun, 18 Oct 2015 04:24:35 +0200 Subject: [PATCH 82/86] s/Famous/Popular --- lib/views/index.html.ejs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/views/index.html.ejs b/lib/views/index.html.ejs index f0aa09e..83333e3 100644 --- a/lib/views/index.html.ejs +++ b/lib/views/index.html.ejs @@ -272,7 +272,7 @@
      -

      Famous Crafatar users

      +

      Popular Crafatar users

      Technic Hypixel From d307aec22181786beca187ee991221d94de55ff6 Mon Sep 17 00:00:00 2001 From: jomo Date: Sun, 18 Oct 2015 15:11:17 +0200 Subject: [PATCH 83/86] rename helm to overlay, fixes #127 --- lib/helpers.js | 18 ++-- lib/public/stylesheets/style.css | 40 ++++----- lib/renders.js | 6 +- lib/routes/avatars.js | 4 +- lib/routes/renders.js | 14 +-- lib/views/index.html.ejs | 10 +-- test/bulk.sh | 4 +- test/test.js | 148 +++++++++++++++---------------- 8 files changed, 122 insertions(+), 122 deletions(-) diff --git a/lib/helpers.js b/lib/helpers.js index 14ea921..f2a8d0c 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -256,16 +256,16 @@ exp.get_image_hash = function(rid, userId, type, callback) { // handles requests for +userId+ avatars with +size+ // callback: error, status, image buffer, skin hash -// image is the user's face+helm when helm is true, or the face otherwise +// image is the user's face+overlay when overlay is true, or the face otherwise // for status, see get_image_hash -exp.get_avatar = function(rid, userId, helm, size, callback) { +exp.get_avatar = function(rid, userId, overlay, size, callback) { exp.get_image_hash(rid, userId, "skin", function(err, status, skin_hash) { if (skin_hash) { var facepath = path.join(config.directories.faces, skin_hash + ".png"); var helmpath = path.join(config.directories.helms, skin_hash + ".png"); var filepath = facepath; fs.exists(helmpath, function(exists) { - if (helm && exists) { + if (overlay && exists) { filepath = helmpath; } skins.resize_img(filepath, size, function(img_err, image) { @@ -308,22 +308,22 @@ exp.get_skin = function(rid, userId, callback) { }; // helper method used for file names -// possible returned names based on +helm+ and +body+ are: +// possible returned names based on +overlay+ and +body+ are: // body, bodyhelm, head, headhelm -function get_type(helm, body) { +function get_type(overlay, body) { var text = body ? "body" : "head"; - return helm ? text + "helm" : text; + return overlay ? text + "helm" : text; } // handles creations of 3D renders // callback: error, skin hash, image buffer -exp.get_render = function(rid, userId, scale, helm, body, callback) { +exp.get_render = function(rid, userId, scale, overlay, body, callback) { exp.get_skin(rid, userId, function(err, skin_hash, status, img) { if (!skin_hash) { callback(err, status, skin_hash, null); return; } - var renderpath = path.join(config.directories.renders, [skin_hash, scale, get_type(helm, body)].join("-") + ".png"); + var renderpath = path.join(config.directories.renders, [skin_hash, scale, get_type(overlay, body)].join("-") + ".png"); fs.exists(renderpath, function(exists) { if (exists) { renders.open_render(rid, renderpath, function(render_err, rendered_img) { @@ -335,7 +335,7 @@ exp.get_render = function(rid, userId, scale, helm, body, callback) { callback(err, 0, skin_hash, null); return; } - renders.draw_model(rid, img, scale, helm, body, function(draw_err, drawn_img) { + renders.draw_model(rid, img, scale, overlay, body, function(draw_err, drawn_img) { if (draw_err) { callback(draw_err, -1, skin_hash, null); } else if (!drawn_img) { diff --git a/lib/public/stylesheets/style.css b/lib/public/stylesheets/style.css index ce51894..5dbd90f 100644 --- a/lib/public/stylesheets/style.css +++ b/lib/public/stylesheets/style.css @@ -156,64 +156,64 @@ code { } .avatar.jomo {background-image: url("/avatars/ae795aa86327408e92ab25c8a59f3ba1?size=64")} -.avatar.jomo:hover {background-image: url("/avatars/ae795aa86327408e92ab25c8a59f3ba1?size=64&helm")} +.avatar.jomo:hover {background-image: url("/avatars/ae795aa86327408e92ab25c8a59f3ba1?size=64&overlay")} .avatar.jake_0 {background-image: url("/avatars/2d5aa9cdaeb049189930461fc9b91cc5?size=64")} -.avatar.jake_0:hover {background-image: url("/avatars/2d5aa9cdaeb049189930461fc9b91cc5?size=64&helm")} +.avatar.jake_0:hover {background-image: url("/avatars/2d5aa9cdaeb049189930461fc9b91cc5?size=64&overlay")} .avatar.sk89q {background-image: url("/avatars/0ea8eca3dbf647cc9d1ac64551ca975c?size=64")} -.avatar.sk89q:hover {background-image: url("/avatars/0ea8eca3dbf647cc9d1ac64551ca975c?size=64&helm")} +.avatar.sk89q:hover {background-image: url("/avatars/0ea8eca3dbf647cc9d1ac64551ca975c?size=64&overlay")} .avatar.md_5 {background-image: url("/avatars/af74a02d19cb445bb07f6866a861f783?size=64")} -.avatar.md_5:hover {background-image: url("/avatars/af74a02d19cb445bb07f6866a861f783?size=64&helm")} +.avatar.md_5:hover {background-image: url("/avatars/af74a02d19cb445bb07f6866a861f783?size=64&overlay")} .avatar.jeb {background-image: url("/avatars/853c80ef3c3749fdaa49938b674adae6?size=64")} -.avatar.jeb:hover {background-image: url("/avatars/853c80ef3c3749fdaa49938b674adae6?size=64&helm")} +.avatar.jeb:hover {background-image: url("/avatars/853c80ef3c3749fdaa49938b674adae6?size=64&overlay")} .avatar.notch {background-image: url("/avatars/069a79f444e94726a5befca90e38aaf5?size=64")} -.avatar.notch:hover {background-image: url("/avatars/069a79f444e94726a5befca90e38aaf5?size=64&helm")} +.avatar.notch:hover {background-image: url("/avatars/069a79f444e94726a5befca90e38aaf5?size=64&overlay")} .avatar.dinnerbone {background-image: url("/avatars/61699b2ed3274a019f1e0ea8c3f06bc6?size=64")} -.avatar.dinnerbone:hover {background-image: url("/avatars/61699b2ed3274a019f1e0ea8c3f06bc6?size=64&helm")} +.avatar.dinnerbone:hover {background-image: url("/avatars/61699b2ed3274a019f1e0ea8c3f06bc6?size=64&overlay")} .avatar.ez {background-image: url("/avatars/7d043c7389524696bfba571c05b6aec0?size=64")} -.avatar.ez:hover {background-image: url("/avatars/7d043c7389524696bfba571c05b6aec0?size=64&helm")} +.avatar.ez:hover {background-image: url("/avatars/7d043c7389524696bfba571c05b6aec0?size=64&overlay")} .avatar.grumm {background-image: url("/avatars/e6b5c088068044df9e1b9bf11792291b?size=64")} -.avatar.grumm:hover {background-image: url("/avatars/e6b5c088068044df9e1b9bf11792291b?size=64&helm")} +.avatar.grumm:hover {background-image: url("/avatars/e6b5c088068044df9e1b9bf11792291b?size=64&overlay")} .avatar.themogmimer {background-image: url("/avatars/1c1bd09a6a0f4928a7914102a35d2670?size=64")} -.avatar.themogmimer:hover {background-image: url("/avatars/1c1bd09a6a0f4928a7914102a35d2670?size=64&helm")} +.avatar.themogmimer:hover {background-image: url("/avatars/1c1bd09a6a0f4928a7914102a35d2670?size=64&overlay")} .avatar.marc {background-image: url("/avatars/b05881186e75410db2db4d3066b223f7?size=64")} -.avatar.marc:hover {background-image: url("/avatars/b05881186e75410db2db4d3066b223f7?size=64&helm")} +.avatar.marc:hover {background-image: url("/avatars/b05881186e75410db2db4d3066b223f7?size=64&overlay")} .avatar.searge {background-image: url("/avatars/696a82ce41f44b51aa31b8709b8686f0?size=64")} -.avatar.searge:hover {background-image: url("/avatars/696a82ce41f44b51aa31b8709b8686f0?size=64&helm")} +.avatar.searge:hover {background-image: url("/avatars/696a82ce41f44b51aa31b8709b8686f0?size=64&overlay")} .avatar.xlson {background-image: url("/avatars/b9583ca43e64488a9c8c4ab27e482255?size=64")} -.avatar.xlson:hover {background-image: url("/avatars/b9583ca43e64488a9c8c4ab27e482255?size=64&helm")} +.avatar.xlson:hover {background-image: url("/avatars/b9583ca43e64488a9c8c4ab27e482255?size=64&overlay")} .avatar.minecraftchick {background-image: url("/avatars/c9b54008fd8047428b238787b5f2401c?size=64")} -.avatar.minecraftchick:hover {background-image: url("/avatars/c9b54008fd8047428b238787b5f2401c?size=64&helm")} +.avatar.minecraftchick:hover {background-image: url("/avatars/c9b54008fd8047428b238787b5f2401c?size=64&overlay")} .avatar.kappe {background-image: url("/avatars/d8f9a4340f2d415f9acfcd70341c75ec?size=64")} -.avatar.kappe:hover {background-image: url("/avatars/d8f9a4340f2d415f9acfcd70341c75ec?size=64&helm")} +.avatar.kappe:hover {background-image: url("/avatars/d8f9a4340f2d415f9acfcd70341c75ec?size=64&overlay")} .avatar.krisjelbring {background-image: url("/avatars/7125ba8b1c864508b92bb5c042ccfe2b?size=64")} -.avatar.krisjelbring:hover {background-image: url("/avatars/7125ba8b1c864508b92bb5c042ccfe2b?size=64&helm")} +.avatar.krisjelbring:hover {background-image: url("/avatars/7125ba8b1c864508b92bb5c042ccfe2b?size=64&overlay")} .avatar.thinkofdeath {background-image: url("/avatars/4566e69fc90748ee8d71d7ba5aa00d20?size=64")} -.avatar.thinkofdeath:hover {background-image: url("/avatars/4566e69fc90748ee8d71d7ba5aa00d20?size=64&helm")} +.avatar.thinkofdeath:hover {background-image: url("/avatars/4566e69fc90748ee8d71d7ba5aa00d20?size=64&overlay")} .avatar.evilseph {background-image: url("/avatars/020242a17b9441799eff511eea1221da?size=64")} -.avatar.evilseph:hover {background-image: url("/avatars/020242a17b9441799eff511eea1221da?size=64&helm")} +.avatar.evilseph:hover {background-image: url("/avatars/020242a17b9441799eff511eea1221da?size=64&overlay")} .avatar.mollstam {background-image: url("/avatars/9769ecf6331448f3ace67ae06cec64a3?size=64")} -.avatar.mollstam:hover {background-image: url("/avatars/9769ecf6331448f3ace67ae06cec64a3?size=64&helm")} +.avatar.mollstam:hover {background-image: url("/avatars/9769ecf6331448f3ace67ae06cec64a3?size=64&overlay")} .avatar.mollstam {background-image: url("/avatars/f8cdb6839e9043eea81939f85d9c5d69?size=64")} -.avatar.mollstam:hover {background-image: url("/avatars/f8cdb6839e9043eea81939f85d9c5d69?size=64&helm")} +.avatar.mollstam:hover {background-image: url("/avatars/f8cdb6839e9043eea81939f85d9c5d69?size=64&overlay")} .avatar.flipped { -webkit-transform: rotate(180deg); diff --git a/lib/renders.js b/lib/renders.js index af7940c..7073c6f 100644 --- a/lib/renders.js +++ b/lib/renders.js @@ -155,9 +155,9 @@ exp.draw_body = function(rid, skin_canvas, model_ctx, scale) { // sets up the necessary components to draw the skin model // uses the +img+ skin with options of drawing -// the +helm+ and the +body+ +// the +overlay+ and the +body+ // callback: error, image buffer -exp.draw_model = function(rid, img, scale, helm, body, callback) { +exp.draw_model = function(rid, img, scale, overlay, body, callback) { var image = new Image(); image.onerror = function(err) { @@ -180,7 +180,7 @@ exp.draw_model = function(rid, img, scale, helm, body, callback) { exp.draw_body(rid, skin_canvas, model_ctx, scale); } exp.draw_head(skin_canvas, model_ctx, scale); - if (helm) { + if (overlay) { exp.draw_helmet(skin_canvas, model_ctx, scale); } diff --git a/lib/routes/avatars.js b/lib/routes/avatars.js index e018465..5146638 100644 --- a/lib/routes/avatars.js +++ b/lib/routes/avatars.js @@ -50,7 +50,7 @@ module.exports = function(req, callback) { var userId = (req.url.path_list[1] || "").split(".")[0]; var size = parseInt(req.url.query.size) || config.avatars.default_size; var def = req.url.query.default; - var helm = req.url.query.hasOwnProperty("helm"); + var overlay = req.url.query.hasOwnProperty("overlay") || req.url.query.hasOwnProperty("helm"); // check for extra paths if (req.url.path_list.length > 2) { @@ -83,7 +83,7 @@ module.exports = function(req, callback) { userId = userId.replace(/-/g, ""); try { - helpers.get_avatar(req.id, userId, helm, size, function(err, status, image, hash) { + helpers.get_avatar(req.id, userId, overlay, size, function(err, status, image, hash) { if (err) { if (err.code === "ENOENT") { // no such file diff --git a/lib/routes/renders.js b/lib/routes/renders.js index 561788a..3ef4724 100644 --- a/lib/routes/renders.js +++ b/lib/routes/renders.js @@ -9,8 +9,8 @@ var url = require("url"); var fs = require("fs"); // valid types: head, body -// helmet is query param -function handle_default(rid, scale, helm, body, img_status, userId, size, def, req, err, callback) { +// overlay is query param +function handle_default(rid, scale, overlay, body, img_status, userId, size, def, req, err, callback) { def = def || skins.default_skin(userId); if (def !== "steve" && def !== "mhf_steve" && def !== "alex" && def !== "mhf_alex") { if (helpers.id_valid(def)) { @@ -40,7 +40,7 @@ function handle_default(rid, scale, helm, body, img_status, userId, size, def, r } fs.readFile(path.join(__dirname, "..", "public", "images", def + "_skin.png"), function (fs_err, buf) { // we render the default skins, but not custom images - renders.draw_model(rid, buf, scale, helm, body, function(render_err, def_img) { + renders.draw_model(rid, buf, scale, overlay, body, function(render_err, def_img) { callback({ status: img_status, body: def_img, @@ -61,7 +61,7 @@ module.exports = function(req, callback) { var userId = (req.url.path_list[2] || "").split(".")[0]; var def = req.url.query.default; var scale = parseInt(req.url.query.scale) || config.renders.default_scale; - var helm = req.url.query.hasOwnProperty("helm"); + var overlay = req.url.query.hasOwnProperty("overlay") || req.url.query.hasOwnProperty("helm"); // check for extra paths if (req.url.path_list.length > 3) { @@ -100,7 +100,7 @@ module.exports = function(req, callback) { userId = userId.replace(/-/g, ""); try { - helpers.get_render(rid, userId, scale, helm, body, function(err, status, hash, image) { + helpers.get_render(rid, userId, scale, overlay, body, function(err, status, hash, image) { if (err) { if (err.code === "ENOENT") { // no such file @@ -117,10 +117,10 @@ module.exports = function(req, callback) { }); } else { logging.debug(rid, "image not found, using default."); - handle_default(rid, scale, helm, body, status, userId, scale, def, req, err, callback); + handle_default(rid, scale, overlay, body, status, userId, scale, def, req, err, callback); } }); } catch(e) { - handle_default(rid, scale, helm, body, -1, userId, scale, def, req, e, callback); + handle_default(rid, scale, overlay, body, -1, userId, scale, def, req, e, callback); } }; \ No newline at end of file diff --git a/lib/views/index.html.ejs b/lib/views/index.html.ejs index 83333e3..e23f8df 100644 --- a/lib/views/index.html.ejs +++ b/lib/views/index.html.ejs @@ -92,7 +92,7 @@
      <%= domain %>/avatars/uuid
      -

      Accepted modifiers: size, helm, default.

      +

      Accepted modifiers: size, overlay, default.

  • @@ -108,7 +108,7 @@ <%= domain %>/renders/head/uuid

    - Accepted modifiers: scale, helm, default.
    + Accepted modifiers: scale, overlay, default.
    Please note that renders are still beta and have some issues. New renders are in progress!

    @@ -126,7 +126,7 @@ <%= domain %>/renders/body/uuid

    - Accepted modifiers: scale, helm, default.
    + Accepted modifiers: scale, overlay, default.
    Please note that renders are still beta and have some issues. New renders are in progress!

    @@ -187,12 +187,12 @@

    URL Parameters

    You can tweak images using query string parameters.
    - Example: <%= domain %>/avatars/853c80ef3c3749fdaa49938b674adae6?size=4&default=MHF_Steve&helm + Example: <%= domain %>/avatars/853c80ef3c3749fdaa49938b674adae6?size=4&default=MHF_Steve&overlay

    • size: The size of the image in pixels. <%= config.avatars.min_size %> - <%= config.avatars.max_size %>
    • scale: The scale factor for renders. <%= config.renders.min_scale %> - <%= config.renders.max_scale %> -
    • helm: Apply the overlay to the avatar. Presence of this parameter implies true. +
    • overlay: Apply the overlay to the avatar. Presence of this parameter implies true. This option was previously known as helm.
    • default: The fallback to be used when the requested image cannot be served. You can use a custom URL or any uuid.
      The option defaults to either MHF_Steve or MHF_Alex, depending on the requested UUID. All usernames default to MHF_Steve. diff --git a/test/bulk.sh b/test/bulk.sh index 239b32c..a975c83 100755 --- a/test/bulk.sh +++ b/test/bulk.sh @@ -25,9 +25,9 @@ bulk() { trap return INT echo "$ids" | while read id; do if [ -z "$async" ]; then - curl -sSL -o /dev/null -w "%{url_effective} %{http_code} %{time_total}s\\n" -- "$host/avatars/$id?helm" + curl -sSL -o /dev/null -w "%{url_effective} %{http_code} %{time_total}s\\n" -- "$host/avatars/$id?overlay" else - curl -sSL -o /dev/null -w "%{url_effective} %{http_code} %{time_total}s\\n" -- "$host/avatars/$id?helm" & + curl -sSL -o /dev/null -w "%{url_effective} %{http_code} %{time_total}s\\n" -- "$host/avatars/$id?overlay" & sleep "$interval" fi done diff --git a/test/test.js b/test/test.js index f909cdc..7272664 100644 --- a/test/test.js +++ b/test/test.js @@ -366,33 +366,33 @@ describe("Crafatar", function() { crc32: 0, redirect: "http://example.com/CaseSensitive" }, - "helm avatar with existing username": { - url: "http://localhost:3000/avatars/jeb_?size=16&helm", + "overlay avatar with existing username": { + url: "http://localhost:3000/avatars/jeb_?size=16&overlay", etag: '"a846b82963"', crc32: 646871998 }, - "helm avatar with non-existent username": { - url: "http://localhost:3000/avatars/0?size=16&helm", + "overlay avatar with non-existent username": { + url: "http://localhost:3000/avatars/0?size=16&overlay", etag: '"mhf_steve"', crc32: [2416827277, 1243826040] }, - "helm avatar with non-existent username defaulting to alex": { - url: "http://localhost:3000/avatars/0?size=16&helm&default=mhf_alex", + "overlay avatar with non-existent username defaulting to alex": { + url: "http://localhost:3000/avatars/0?size=16&overlay&default=mhf_alex", etag: '"mhf_alex"', crc32: [862751081, 809395677] }, - "helm avatar with non-existent username defaulting to username": { - url: "http://localhost:3000/avatars/0?size=16&helm&default=jeb_", + "overlay avatar with non-existent username defaulting to username": { + url: "http://localhost:3000/avatars/0?size=16&overlay&default=jeb_", crc32: 0, - redirect: "/avatars/jeb_?size=16&helm=" + redirect: "/avatars/jeb_?size=16&overlay=" }, - "helm avatar with non-existent username defaulting to uuid": { - url: "http://localhost:3000/avatars/0?size=16&helm&default=853c80ef3c3749fdaa49938b674adae6", + "overlay avatar with non-existent username defaulting to uuid": { + url: "http://localhost:3000/avatars/0?size=16&overlay&default=853c80ef3c3749fdaa49938b674adae6", crc32: 0, - redirect: "/avatars/853c80ef3c3749fdaa49938b674adae6?size=16&helm=" + redirect: "/avatars/853c80ef3c3749fdaa49938b674adae6?size=16&overlay=" }, - "helm avatar with non-existent username defaulting to url": { - url: "http://localhost:3000/avatars/0?size=16&helm&default=http%3A%2F%2Fexample.com%2FCaseSensitive", + "overlay avatar with non-existent username defaulting to url": { + url: "http://localhost:3000/avatars/0?size=16&overlay&default=http%3A%2F%2Fexample.com%2FCaseSensitive", crc32: 0, redirect: "http://example.com/CaseSensitive" }, @@ -426,33 +426,33 @@ describe("Crafatar", function() { crc32: 0, redirect: "http://example.com/CaseSensitive" }, - "helm avatar with existing uuid": { - url: "http://localhost:3000/avatars/853c80ef3c3749fdaa49938b674adae6?size=16&helm", + "overlay avatar with existing uuid": { + url: "http://localhost:3000/avatars/853c80ef3c3749fdaa49938b674adae6?size=16&overlay", etag: '"a846b82963"', crc32: 646871998 }, - "helm avatar with non-existent uuid": { - url: "http://localhost:3000/avatars/00000000000000000000000000000000?size=16&helm", + "overlay avatar with non-existent uuid": { + url: "http://localhost:3000/avatars/00000000000000000000000000000000?size=16&overlay", etag: '"mhf_steve"', crc32: [2416827277, 1243826040] }, - "helm avatar with non-existent uuid defaulting to alex": { - url: "http://localhost:3000/avatars/00000000000000000000000000000000?size=16&helm&default=mhf_alex", + "overlay avatar with non-existent uuid defaulting to alex": { + url: "http://localhost:3000/avatars/00000000000000000000000000000000?size=16&overlay&default=mhf_alex", etag: '"mhf_alex"', crc32: [862751081, 809395677] }, - "helm avatar with non-existent uuid defaulting to username": { + "overlay avatar with non-existent uuid defaulting to username": { url: "http://localhost:3000/avatars/00000000000000000000000000000000?size=16&default=jeb_", crc32: 0, redirect: "/avatars/jeb_?size=16" }, - "helm avatar with non-existent uuid defaulting to uuid": { + "overlay avatar with non-existent uuid defaulting to uuid": { url: "http://localhost:3000/avatars/00000000000000000000000000000000?size=16&default=853c80ef3c3749fdaa49938b674adae6", crc32: 0, redirect: "/avatars/853c80ef3c3749fdaa49938b674adae6?size=16" }, - "helm avatar with non-existent uuid defaulting to url": { - url: "http://localhost:3000/avatars/00000000000000000000000000000000?size=16&helm&default=http%3A%2F%2Fexample.com%2FCaseSensitive", + "overlay avatar with non-existent uuid defaulting to url": { + url: "http://localhost:3000/avatars/00000000000000000000000000000000?size=16&overlay&default=http%3A%2F%2Fexample.com%2FCaseSensitive", crc32: 0, redirect: "http://example.com/CaseSensitive" }, @@ -574,33 +574,33 @@ describe("Crafatar", function() { crc32: 0, redirect: "http://example.com/CaseSensitive" }, - "helm head render with existing username": { - url: "http://localhost:3000/renders/head/jeb_?scale=2&helm", + "overlay head render with existing username": { + url: "http://localhost:3000/renders/head/jeb_?scale=2&overlay", etag: '"a846b82963"', crc32: [4178514320, 2340078566, 3980890516] }, - "helm head render with non-existent username": { - url: "http://localhost:3000/renders/head/0?scale=2&helm", + "overlay head render with non-existent username": { + url: "http://localhost:3000/renders/head/0?scale=2&overlay", etag: '"mhf_steve"', crc32: [507497693, 3868868707, 7372195] }, - "helm head render with non-existent username defaulting to alex": { - url: "http://localhost:3000/renders/head/0?scale=2&helm&default=mhf_alex", + "overlay head render with non-existent username defaulting to alex": { + url: "http://localhost:3000/renders/head/0?scale=2&overlay&default=mhf_alex", etag: '"mhf_alex"', crc32: [891113664, 1785326216, 622500655] }, - "helm head render with non-existent username defaulting to username": { - url: "http://localhost:3000/renders/head/0?scale=2&helm&default=jeb_", + "overlay head render with non-existent username defaulting to username": { + url: "http://localhost:3000/renders/head/0?scale=2&overlay&default=jeb_", crc32: 0, - redirect: "/renders/head/jeb_?scale=2&helm=" + redirect: "/renders/head/jeb_?scale=2&overlay=" }, - "helm head render with non-existent username defaulting to uuid": { - url: "http://localhost:3000/renders/head/0?scale=2&helm&default=853c80ef3c3749fdaa49938b674adae6", + "overlay head render with non-existent username defaulting to uuid": { + url: "http://localhost:3000/renders/head/0?scale=2&overlay&default=853c80ef3c3749fdaa49938b674adae6", crc32: 0, - redirect: "/renders/head/853c80ef3c3749fdaa49938b674adae6?scale=2&helm=" + redirect: "/renders/head/853c80ef3c3749fdaa49938b674adae6?scale=2&overlay=" }, - "helm head render with non-existent username defaulting to url": { - url: "http://localhost:3000/renders/head/0?scale=2&helm&default=http%3A%2F%2Fexample.com%2FCaseSensitive", + "overlay head render with non-existent username defaulting to url": { + url: "http://localhost:3000/renders/head/0?scale=2&overlay&default=http%3A%2F%2Fexample.com%2FCaseSensitive", crc32: 0, redirect: "http://example.com/CaseSensitive" }, @@ -634,33 +634,33 @@ describe("Crafatar", function() { crc32: 0, redirect: "http://example.com/CaseSensitive" }, - "helm head render with existing uuid": { - url: "http://localhost:3000/renders/head/853c80ef3c3749fdaa49938b674adae6?scale=2&helm", + "overlay head render with existing uuid": { + url: "http://localhost:3000/renders/head/853c80ef3c3749fdaa49938b674adae6?scale=2&overlay", etag: '"a846b82963"', crc32: [4178514320, 2340078566, 3980890516] }, - "helm head render with non-existent uuid": { - url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&helm", + "overlay head render with non-existent uuid": { + url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&overlay", etag: '"mhf_steve"', crc32: [507497693, 3868868707, 7372195] }, - "helm head render with non-existent uuid defaulting to alex": { - url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&helm&default=mhf_alex", + "overlay head render with non-existent uuid defaulting to alex": { + url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&overlay&default=mhf_alex", etag: '"mhf_alex"', crc32: [891113664, 1785326216, 622500655] }, - "helm head with non-existent uuid defaulting to username": { - url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&helm&default=jeb_", + "overlay head with non-existent uuid defaulting to username": { + url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&overlay&default=jeb_", crc32: 0, - redirect: "/renders/head/jeb_?scale=2&helm=" + redirect: "/renders/head/jeb_?scale=2&overlay=" }, - "helm head with non-existent uuid defaulting to uuid": { - url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&helm&default=853c80ef3c3749fdaa49938b674adae6", + "overlay head with non-existent uuid defaulting to uuid": { + url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&overlay&default=853c80ef3c3749fdaa49938b674adae6", crc32: 0, - redirect: "/renders/head/853c80ef3c3749fdaa49938b674adae6?scale=2&helm=" + redirect: "/renders/head/853c80ef3c3749fdaa49938b674adae6?scale=2&overlay=" }, - "helm head render with non-existent uuid defaulting to url": { - url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&helm&default=http%3A%2F%2Fexample.com%2FCaseSensitive", + "overlay head render with non-existent uuid defaulting to url": { + url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&overlay&default=http%3A%2F%2Fexample.com%2FCaseSensitive", crc32: 0, redirect: "http://example.com/CaseSensitive" }, @@ -694,33 +694,33 @@ describe("Crafatar", function() { crc32: 0, redirect: "http://example.com/CaseSensitive" }, - "helm body render with existing username": { - url: "http://localhost:3000/renders/body/jeb_?scale=2&helm", + "overlay body render with existing username": { + url: "http://localhost:3000/renders/body/jeb_?scale=2&overlay", etag: '"a846b82963"', crc32: [3476579592, 97705180, 3086172613] }, - "helm body render with non-existent username": { - url: "http://localhost:3000/renders/body/0?scale=2&helm", + "overlay body render with non-existent username": { + url: "http://localhost:3000/renders/body/0?scale=2&overlay", etag: '"mhf_steve"', crc32: [3992841063, 1025743887, 1906839968] }, - "helm body render with non-existent username defaulting to alex": { - url: "http://localhost:3000/renders/body/0?scale=2&helm&default=mhf_alex", + "overlay body render with non-existent username defaulting to alex": { + url: "http://localhost:3000/renders/body/0?scale=2&overlay&default=mhf_alex", etag: '"mhf_alex"', crc32: [3317518715, 3621585514, 294661951] }, - "helm body render with non-existent username defaulting to username": { - url: "http://localhost:3000/renders/body/0?scale=2&helm&default=jeb_", + "overlay body render with non-existent username defaulting to username": { + url: "http://localhost:3000/renders/body/0?scale=2&overlay&default=jeb_", crc32: 0, - redirect: "/renders/body/jeb_?scale=2&helm=" + redirect: "/renders/body/jeb_?scale=2&overlay=" }, - "helm body render with non-existent username defaulting to uuid": { - url: "http://localhost:3000/renders/body/0?scale=2&helm&default=853c80ef3c3749fdaa49938b674adae6", + "overlay body render with non-existent username defaulting to uuid": { + url: "http://localhost:3000/renders/body/0?scale=2&overlay&default=853c80ef3c3749fdaa49938b674adae6", crc32: 0, - redirect: "/renders/body/853c80ef3c3749fdaa49938b674adae6?scale=2&helm=" + redirect: "/renders/body/853c80ef3c3749fdaa49938b674adae6?scale=2&overlay=" }, - "helm body render with non-existent username defaulting to url": { - url: "http://localhost:3000/renders/body/0?scale=2&helm&default=http%3A%2F%2Fexample.com%2FCaseSensitive", + "overlay body render with non-existent username defaulting to url": { + url: "http://localhost:3000/renders/body/0?scale=2&overlay&default=http%3A%2F%2Fexample.com%2FCaseSensitive", crc32: 0, redirect: "http://example.com/CaseSensitive" }, @@ -754,23 +754,23 @@ describe("Crafatar", function() { crc32: 0, redirect: "http://example.com/CaseSensitive" }, - "helm body render with existing uuid": { - url: "http://localhost:3000/renders/body/853c80ef3c3749fdaa49938b674adae6?scale=2&helm", + "overlay body render with existing uuid": { + url: "http://localhost:3000/renders/body/853c80ef3c3749fdaa49938b674adae6?scale=2&overlay", etag: '"a846b82963"', crc32: [3476579592, 97705180, 3086172613] }, - "helm body render with non-existent uuid": { - url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2&helm", + "overlay body render with non-existent uuid": { + url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2&overlay", etag: '"mhf_steve"', crc32: [3992841063, 1025743887, 1906839968] }, - "helm body render with non-existent uuid defaulting to alex": { - url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2&helm&default=mhf_alex", + "overlay body render with non-existent uuid defaulting to alex": { + url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2&overlay&default=mhf_alex", etag: '"mhf_alex"', crc32: [3317518715, 3621585514, 294661951] }, - "helm body render with non-existent uuid defaulting to url": { - url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2&helm&default=http%3A%2F%2Fexample.com%2FCaseSensitive", + "overlay body render with non-existent uuid defaulting to url": { + url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2&overlay&default=http%3A%2F%2Fexample.com%2FCaseSensitive", crc32: 0, redirect: "http://example.com/CaseSensitive" }, From fb0c70d6488fbf783c70bbb807ab1b3b91710fd9 Mon Sep 17 00:00:00 2001 From: jomo Date: Wed, 21 Oct 2015 01:02:57 +0200 Subject: [PATCH 84/86] return HTTPERROR on 429 or 5xx, fixes #151 otherwise 429 or 5xx would be overwriting cached value with null for $config minutes --- lib/cache.js | 2 +- lib/networking.js | 12 +++++++++++- lib/response.js | 2 +- test/test.js | 2 +- 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/lib/cache.js b/lib/cache.js index 61310c1..b089589 100644 --- a/lib/cache.js +++ b/lib/cache.js @@ -75,7 +75,7 @@ exp.info = function(callback) { // these 60 seconds match the duration of Mojang's rate limit ban // callback: error exp.update_timestamp = function(rid, userId, temp, callback) { - logging.debug(rid, "updating cache timestamp"); + logging.debug(rid, "updating cache timestamp (" + temp + ")"); var sub = temp ? config.caching.local - 60 : 0; var time = Date.now() - sub; // store userId in lower case if not null diff --git a/lib/networking.js b/lib/networking.js index 0cf7c60..704f590 100644 --- a/lib/networking.js +++ b/lib/networking.js @@ -77,6 +77,11 @@ exp.get_from_options = function(rid, url, options, callback) { var logfunc = code && code < 405 ? logging.debug : logging.warn; logfunc(rid, url, code || error && error.code, http_code[code]); + // not necessarily used + var e = new Error(code); + e.name = "HTTP"; + e.code = "HTTPERROR"; + switch (code) { case 200: case 301: @@ -85,13 +90,17 @@ exp.get_from_options = function(rid, url, options, callback) { case 308: // never seen, but mojang might use it in future // these are okay break; + case 204: // no content, used like 404 by mojang. making sure it really has no content case 404: - case 204: + // can be cached as null + body = null; + break; case 429: // this shouldn't usually happen, but occasionally does case 500: case 503: case 504: // we don't want to cache this + error = error || e; body = null; break; default: @@ -99,6 +108,7 @@ exp.get_from_options = function(rid, url, options, callback) { // Probably 500 or the likes logging.error(rid, "Unexpected response:", code, body); } + error = error || e; body = null; break; } diff --git a/lib/response.js b/lib/response.js index e78947b..561d58f 100644 --- a/lib/response.js +++ b/lib/response.js @@ -13,7 +13,7 @@ var human_status = { // print these, but without stacktrace -var silent_errors = ["ETIMEDOUT", "ESOCKETTIMEDOUT", "ECONNRESET", "EHOSTUNREACH", "ECONNREFUSED"]; +var silent_errors = ["ETIMEDOUT", "ESOCKETTIMEDOUT", "ECONNRESET", "EHOSTUNREACH", "ECONNREFUSED", "HTTPERROR"]; // handles HTTP responses // +request+ a http.IncomingMessage diff --git a/test/test.js b/test/test.js index 7272664..3b04a8d 100644 --- a/test/test.js +++ b/test/test.js @@ -1020,7 +1020,7 @@ describe("Crafatar", function() { it("uuid should be rate limited", function(done) { networking.get_profile(rid, id, function() { networking.get_profile(rid, id, function(err, profile) { - assert.ifError(err); + assert.strictEqual(err.toString(), "HTTP: 429"); assert.strictEqual(profile, null); done(); }); From 4302a95f6c040a4236247eb4099b187c6b12e692 Mon Sep 17 00:00:00 2001 From: jomo Date: Sun, 15 Nov 2015 20:06:40 +0100 Subject: [PATCH 85/86] clarify `size` is only used for avatars. Related: crafatar/setup#5 --- lib/views/index.html.ejs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/views/index.html.ejs b/lib/views/index.html.ejs index e23f8df..b6161bd 100644 --- a/lib/views/index.html.ejs +++ b/lib/views/index.html.ejs @@ -190,7 +190,7 @@ Example: <%= domain %>/avatars/853c80ef3c3749fdaa49938b674adae6?size=4&default=MHF_Steve&overlay

        -
      • size: The size of the image in pixels. <%= config.avatars.min_size %> - <%= config.avatars.max_size %> +
      • size: The size for avatars in pixels. <%= config.avatars.min_size %> - <%= config.avatars.max_size %>
      • scale: The scale factor for renders. <%= config.renders.min_scale %> - <%= config.renders.max_scale %>
      • overlay: Apply the overlay to the avatar. Presence of this parameter implies true. This option was previously known as helm.
      • From 6e500d86520d35baaf7b347746a4155f368fb9c4 Mon Sep 17 00:00:00 2001 From: jomo Date: Sun, 22 Nov 2015 07:22:56 +0100 Subject: [PATCH 86/86] fix HTML from https://validator.w3.org: > Document checking completed. No errors or warnings to show. --- lib/views/index.html.ejs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/lib/views/index.html.ejs b/lib/views/index.html.ejs index b6161bd..bd4630f 100644 --- a/lib/views/index.html.ejs +++ b/lib/views/index.html.ejs @@ -9,7 +9,7 @@ - + @@ -143,7 +143,7 @@
        <%= domain %>/skins/uuid
        -

        Accepted modifiers: default.

        +

        Accepted modifiers: default.

    @@ -158,7 +158,7 @@
    <%= domain %>/capes/uuid
    -

    Accepted modifiers: default.

    +

    Accepted modifiers: default.

    @@ -177,9 +177,9 @@

    Attribution is not required, but it is encouraged.
    If you want to show some support for this (free!) service, place a notice like this somewhere: -

    + Thank you to <a href="https://crafatar.com">Crafatar</a> for providing avatars. -
    +

    @@ -197,7 +197,7 @@ default: The fallback to be used when the requested image cannot be served. You can use a custom URL or any uuid.
    The option defaults to either MHF_Steve or MHF_Alex, depending on the requested UUID. All usernames default to MHF_Steve. -

    +

    About UUIDs

    @@ -293,7 +293,6 @@ wither (Slack) and many more… -