mirror of
https://github.com/azures04/crafatar.git
synced 2026-03-22 07:51:17 +01:00
Merge pull request #52 from Jake0oo0/capes
Capes Support + Network changes - Initial commits from @navarr
This commit is contained in:
commit
74f1618aa3
1
.gitignore
vendored
1
.gitignore
vendored
@ -5,3 +5,4 @@ node_modules/
|
|||||||
*.rdb
|
*.rdb
|
||||||
coverage/
|
coverage/
|
||||||
modules/config.js
|
modules/config.js
|
||||||
|
undefined*.png
|
||||||
@ -1,4 +1,4 @@
|
|||||||
# Crafatar [](https://travis-ci.org/Jake0oo0/crafatar/) [](https://coveralls.io/r/Jake0oo0/crafatar)
|
# Crafatar [](https://travis-ci.org/Jake0oo0/crafatar/) [](https://coveralls.io/r/Jake0oo0/crafatar) [](https://codeclimate.com/github/Jake0oo0/crafatar)
|
||||||
|
|
||||||
https://crafatar.com
|
https://crafatar.com
|
||||||
|
|
||||||
|
|||||||
0
images/capes/.gitkeep
Normal file
0
images/capes/.gitkeep
Normal file
@ -82,7 +82,7 @@ exp.info = function(callback) {
|
|||||||
});
|
});
|
||||||
obj.versions = [];
|
obj.versions = [];
|
||||||
if( obj.redis_version ){
|
if( obj.redis_version ){
|
||||||
obj.redis_version.split(".").forEach(function (num) {
|
obj.redis_version.split(".").forEach(function(num) {
|
||||||
obj.versions.push(+num);
|
obj.versions.push(+num);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -103,14 +103,16 @@ exp.update_timestamp = function(uuid, hash) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// create the key +uuid+, store +hash+ and time
|
// create the key +uuid+, store +hash+ and time
|
||||||
exp.save_hash = function(uuid, hash) {
|
exp.save_hash = function(uuid, skin, cape) {
|
||||||
logging.log(uuid + " cache: saving hash");
|
logging.log(uuid + " cache: saving hash");
|
||||||
|
logging.log("skin:" + skin + " cape:" + cape);
|
||||||
var time = new Date().getTime();
|
var time = new Date().getTime();
|
||||||
// store shorter null byte instead of "null"
|
// store shorter null byte instead of "null"
|
||||||
hash = hash || ".";
|
skin = skin || ".";
|
||||||
|
cape = cape || ".";
|
||||||
// store uuid in lower case if not null
|
// store uuid in lower case if not null
|
||||||
uuid = uuid && uuid.toLowerCase();
|
uuid = uuid && uuid.toLowerCase();
|
||||||
redis.hmset(uuid, "h", hash, "t", time);
|
redis.hmset(uuid, "s", skin, "c", cape, "t", time);
|
||||||
};
|
};
|
||||||
|
|
||||||
exp.remove_hash = function(uuid) {
|
exp.remove_hash = function(uuid) {
|
||||||
@ -119,7 +121,7 @@ exp.remove_hash = function(uuid) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// get a details object for +uuid+
|
// get a details object for +uuid+
|
||||||
// {hash: "0123456789abcdef", time: 1414881524512}
|
// {skin: "0123456789abcdef", cape: "gs1gds1g5d1g5ds1", time: 1414881524512}
|
||||||
// null when uuid unkown
|
// null when uuid unkown
|
||||||
exp.get_details = function(uuid, callback) {
|
exp.get_details = function(uuid, callback) {
|
||||||
// get uuid in lower case if not null
|
// get uuid in lower case if not null
|
||||||
@ -128,7 +130,8 @@ exp.get_details = function(uuid, callback) {
|
|||||||
var details = null;
|
var details = null;
|
||||||
if (data) {
|
if (data) {
|
||||||
details = {
|
details = {
|
||||||
hash: (data.h == "." ? null : data.h),
|
skin: (data.s === "." ? null : data.s),
|
||||||
|
cape: (data.c === "." ? null : data.c),
|
||||||
time: Number(data.t)
|
time: Number(data.t)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,7 +15,7 @@ function should_clean_redis(callback) {
|
|||||||
callback(err, false);
|
callback(err, false);
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
logging.debug(info);
|
//logging.debug(info.toString());
|
||||||
logging.debug("used mem:" + info.used_memory);
|
logging.debug("used mem:" + info.used_memory);
|
||||||
var used = parseInt(info.used_memory) / 1024;
|
var used = parseInt(info.used_memory) / 1024;
|
||||||
logging.log("RedisCleaner: " + used + "KB used");
|
logging.log("RedisCleaner: " + used + "KB used");
|
||||||
@ -71,7 +71,6 @@ exp.run = function() {
|
|||||||
var helmdir = __dirname + "/../" + config.helms_dir;
|
var helmdir = __dirname + "/../" + config.helms_dir;
|
||||||
var renderdir = __dirname + "/../" + config.renders_dir;
|
var renderdir = __dirname + "/../" + config.renders_dir;
|
||||||
var skindir = __dirname + "/../" + config.skins_dir;
|
var skindir = __dirname + "/../" + config.skins_dir;
|
||||||
|
|
||||||
fs.readdir(facesdir, function (err, files) {
|
fs.readdir(facesdir, function (err, files) {
|
||||||
for (var i = 0, l = Math.min(files.length, config.cleaning_amount); i < l; i++) {
|
for (var i = 0, l = Math.min(files.length, config.cleaning_amount); i < l; i++) {
|
||||||
var filename = files[i];
|
var filename = files[i];
|
||||||
|
|||||||
@ -2,6 +2,9 @@ var config = {
|
|||||||
min_size: 1, // for avatars
|
min_size: 1, // for avatars
|
||||||
max_size: 512, // for avatars; too big values might lead to slow response time or DoS
|
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
|
default_size: 160, // for avatars; size to be used when no size given
|
||||||
|
min_scale: 1, // for renders
|
||||||
|
max_scale: 10, // for renders; too big values might lead to slow response time or DoS
|
||||||
|
default_scale: 6, // for renders; scale to be used when no scale given
|
||||||
local_cache_time: 1200, // seconds until we will check if the image changed. should be > 60 to prevent mojang 429 response
|
local_cache_time: 1200, // seconds until we will check if the image changed. should be > 60 to prevent mojang 429 response
|
||||||
browser_cache_time: 3600, // seconds until browser will request image again
|
browser_cache_time: 3600, // seconds until browser will request image again
|
||||||
cleaning_interval: 1800, // seconds interval: deleting images if disk size at limit
|
cleaning_interval: 1800, // seconds interval: deleting images if disk size at limit
|
||||||
@ -9,14 +12,12 @@ var config = {
|
|||||||
cleaning_redis_limit: 24576, // max allowed used KB on redis to trigger redis flush
|
cleaning_redis_limit: 24576, // max allowed used KB on redis to trigger redis flush
|
||||||
cleaning_amount: 50000, // amount of avatar (and their helm) files to clean
|
cleaning_amount: 50000, // amount of avatar (and their helm) files to clean
|
||||||
http_timeout: 1000, // ms until connection to mojang is dropped
|
http_timeout: 1000, // ms until connection to mojang is dropped
|
||||||
|
debug_enabled: false, // enables logging.debug
|
||||||
faces_dir: "images/faces/", // directory where faces are kept. should have trailing "/"
|
faces_dir: "images/faces/", // directory where faces are kept. should have trailing "/"
|
||||||
helms_dir: "images/helms/", // directory where helms are kept. should have trailing "/"
|
helms_dir: "images/helms/", // directory where helms are kept. should have trailing "/"
|
||||||
skins_dir: "images/skins/", // directory where skins are kept. should have trailing "/"
|
skins_dir: "images/skins/", // directory where skins are kept. should have trailing "/"
|
||||||
renders_dir: "images/renders/",// Directory where rendered skins are kept. should have trailing "/"
|
renders_dir: "images/renders/",// Directory where rendered skins are kept. should have trailing "/"
|
||||||
debug_enabled: false, // enables logging.debug
|
capes_dir: "images/capes/", // directory where capes are kept. should have trailing "/"
|
||||||
min_scale: 1, // for renders
|
|
||||||
max_scale: 10, // for renders; too big values might lead to slow response time or DoS
|
|
||||||
default_scale: 6 // for renders; scale to be used when no scale given
|
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = config;
|
module.exports = config;
|
||||||
@ -10,73 +10,138 @@ var fs = require("fs");
|
|||||||
var valid_uuid = /^([0-9a-f-A-F-]{32,36}|[a-zA-Z0-9_]{1,16})$/; // uuid|username
|
var valid_uuid = /^([0-9a-f-A-F-]{32,36}|[a-zA-Z0-9_]{1,16})$/; // uuid|username
|
||||||
var hash_pattern = /[0-9a-f]+$/;
|
var hash_pattern = /[0-9a-f]+$/;
|
||||||
|
|
||||||
|
// gets the hash from the textures.minecraft.net +url+
|
||||||
function get_hash(url) {
|
function get_hash(url) {
|
||||||
return hash_pattern.exec(url)[0].toLowerCase();
|
return hash_pattern.exec(url)[0].toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
// requests skin for +uuid+ and extracts face/helm if image hash in +details+ changed
|
function store_skin(uuid, profile, details, callback) {
|
||||||
// callback contains error, image hash
|
networking.get_skin_url(uuid, profile, function(url) {
|
||||||
function store_images(uuid, details, callback) {
|
if (url) {
|
||||||
// get skin_url for +uuid+
|
var hash = get_hash(url);
|
||||||
networking.get_skin_url(uuid, function(err, skin_url) {
|
if (details && details.skin === hash) {
|
||||||
if (err) {
|
cache.update_timestamp(uuid, hash);
|
||||||
callback(err, null);
|
callback(null, hash);
|
||||||
} else {
|
|
||||||
if (skin_url) {
|
|
||||||
logging.log(uuid + " " + skin_url);
|
|
||||||
// set file paths
|
|
||||||
var hash = get_hash(skin_url);
|
|
||||||
if (details && details.hash == hash) {
|
|
||||||
// hash hasn't changed
|
|
||||||
logging.log(uuid + " hash has not changed");
|
|
||||||
cache.update_timestamp(uuid, hash);
|
|
||||||
callback(null, hash);
|
|
||||||
} else {
|
|
||||||
// hash has changed
|
|
||||||
logging.log(uuid + " new hash: " + hash);
|
|
||||||
var facepath = __dirname + "/../" + config.faces_dir + hash + ".png";
|
|
||||||
var helmpath = __dirname + "/../" + config.helms_dir + hash + ".png";
|
|
||||||
|
|
||||||
fs.exists(facepath, function (exists) {
|
|
||||||
if (exists) {
|
|
||||||
logging.log(uuid + " Avatar already exists, not downloading");
|
|
||||||
cache.save_hash(uuid, hash);
|
|
||||||
callback(null, hash);
|
|
||||||
} else {
|
|
||||||
// download skin
|
|
||||||
networking.get_skin(skin_url, uuid, function(err, img) {
|
|
||||||
if (err || !img) {
|
|
||||||
callback(err, null);
|
|
||||||
} else {
|
|
||||||
// extract face / helm
|
|
||||||
skins.extract_face(img, facepath, function(err) {
|
|
||||||
if (err) {
|
|
||||||
callback(err);
|
|
||||||
} else {
|
|
||||||
logging.log(uuid + " face extracted");
|
|
||||||
logging.debug(uuid + " " + facepath);
|
|
||||||
skins.extract_helm(uuid, facepath, img, helmpath, function(err) {
|
|
||||||
logging.log(uuid + " helm extracted");
|
|
||||||
logging.debug(uuid + " " + helmpath);
|
|
||||||
cache.save_hash(uuid, hash);
|
|
||||||
callback(err, hash);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// profile found, but has no skin
|
logging.log(uuid + " new skin hash: " + hash);
|
||||||
cache.save_hash(uuid, null);
|
var facepath = __dirname + "/../" + config.faces_dir + hash + ".png";
|
||||||
callback(null, null);
|
var helmpath = __dirname + "/../" + config.helms_dir + hash + ".png"
|
||||||
|
fs.exists(facepath, function(exists) {
|
||||||
|
if (exists) {
|
||||||
|
logging.log(uuid + " skin already exists, not downloading");
|
||||||
|
callback(null, hash);
|
||||||
|
} else {
|
||||||
|
networking.get_from(url, function(img, response, err) {
|
||||||
|
if (err || !img) {
|
||||||
|
callback(err, null);
|
||||||
|
} else {
|
||||||
|
skins.extract_face(img, facepath, function(err) {
|
||||||
|
if (err) {
|
||||||
|
logging.error(err);
|
||||||
|
callback(err, null);
|
||||||
|
} else {
|
||||||
|
logging.log(uuid + " face extracted");
|
||||||
|
skins.extract_helm(uuid, facepath, img, helmpath, function(err) {
|
||||||
|
logging.log(uuid + " helm extracted");
|
||||||
|
logging.debug(helmpath);
|
||||||
|
callback(err, hash);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
callback(null, null);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function store_cape(uuid, profile, details, callback) {
|
||||||
|
networking.get_cape_url(uuid, profile, function(url) {
|
||||||
|
if (url) {
|
||||||
|
var hash = get_hash(url);
|
||||||
|
if (details && details.cape === hash) {
|
||||||
|
cache.update_timestamp(uuid, hash);
|
||||||
|
callback(null, hash);
|
||||||
|
} else {
|
||||||
|
logging.log(uuid + " new cape hash: " + hash);
|
||||||
|
var capepath = __dirname + "/../" + config.capes_dir + hash + ".png";
|
||||||
|
fs.exists(capepath, function(exists) {
|
||||||
|
if (exists) {
|
||||||
|
logging.log(uuid + " cape already exists, not downloading");
|
||||||
|
callback(null, hash);
|
||||||
|
} else {
|
||||||
|
networking.get_from(url, function(img, response, err) {
|
||||||
|
if (err || !img) {
|
||||||
|
logging.error(err);
|
||||||
|
callback(err, null);
|
||||||
|
} else {
|
||||||
|
skins.save_image(img, capepath, function(err) {
|
||||||
|
logging.log(uuid + " cape saved");
|
||||||
|
callback(err, hash);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
callback(null, null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// downloads the images for +uuid+ while checking the cache
|
||||||
|
// status based on +details+. +whichhash+ specifies which
|
||||||
|
// image is more important, and should be called back on
|
||||||
|
// +callback+ contains the error buffer and image hash
|
||||||
|
var currently_running = [];
|
||||||
|
function callback_for(uuid, which, err, cape_hash, skin_hash) {
|
||||||
|
for (var i = 0; i < currently_running.length; i++) {
|
||||||
|
if (currently_running[i] && currently_running[i].uuid === uuid && (currently_running[i].which === which || which === null)) {
|
||||||
|
var will_call = currently_running[i];
|
||||||
|
will_call.callback(err, will_call.which === 'skin' ? skin_hash : cape_hash);
|
||||||
|
//remove_from_array(currently_running, i);
|
||||||
|
delete(currently_running[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function array_has_hash(arr, property, value) {
|
||||||
|
for (var i = 0; i < arr.length; i++) {
|
||||||
|
if (arr[i] && arr[i][property] === value) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function store_images(uuid, details, whichhash, callback) {
|
||||||
|
var isUUID = uuid.length > 16;
|
||||||
|
var new_hash = { 'uuid': uuid, 'which': whichhash, 'callback': callback };
|
||||||
|
if (!array_has_hash(currently_running, 'uuid', uuid)) {
|
||||||
|
currently_running.push(new_hash);
|
||||||
|
networking.get_profile((isUUID ? uuid : null), function(err, profile) {
|
||||||
|
if (err || (isUUID && !profile)) {
|
||||||
|
callback_for(uuid, err, null, null);
|
||||||
|
} else {
|
||||||
|
store_skin(uuid, profile, details, function(err, skin_hash) {
|
||||||
|
cache.save_hash(uuid, skin_hash, null);
|
||||||
|
callback_for(uuid, 'skin', err, null, skin_hash);
|
||||||
|
store_cape(uuid, profile, details, function(err, cape_hash) {
|
||||||
|
cache.save_hash(uuid, skin_hash, cape_hash);
|
||||||
|
callback_for(uuid, 'cape', err, cape_hash, skin_hash);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
currently_running.push(new_hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var exp = {};
|
var exp = {};
|
||||||
|
|
||||||
@ -86,7 +151,6 @@ exp.uuid_valid = function(uuid) {
|
|||||||
return valid_uuid.test(uuid);
|
return valid_uuid.test(uuid);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// decides whether to get an image from disk or to download it
|
// decides whether to get an image from disk or to download it
|
||||||
// callback contains error, status, hash
|
// callback contains error, status, hash
|
||||||
// the status gives information about how the image was received
|
// the status gives information about how the image was received
|
||||||
@ -95,29 +159,27 @@ exp.uuid_valid = function(uuid) {
|
|||||||
// 1: "cached" - found on disk
|
// 1: "cached" - found on disk
|
||||||
// 2: "downloaded" - profile downloaded, skin downloaded from mojang servers
|
// 2: "downloaded" - profile downloaded, skin downloaded from mojang servers
|
||||||
// 3: "checked" - profile re-downloaded (was too old), but it has either not changed or has no skin
|
// 3: "checked" - profile re-downloaded (was too old), but it has either not changed or has no skin
|
||||||
exp.get_image_hash = function(uuid, callback) {
|
exp.get_image_hash = function(uuid, raw_type, callback) {
|
||||||
cache.get_details(uuid, function(err, details) {
|
cache.get_details(uuid, function(err, details) {
|
||||||
|
var type = (details !== null ? (raw_type === "skin" ? details.skin : details.cape) : null);
|
||||||
if (err) {
|
if (err) {
|
||||||
callback(err, -1, null);
|
callback(err, -1, null);
|
||||||
} else {
|
} else {
|
||||||
if (details && details.time + config.local_cache_time * 1000 >= new Date().getTime()) {
|
if (details && details.time + config.local_cache_time * 1000 >= new Date().getTime()) {
|
||||||
// uuid known + recently updated
|
|
||||||
logging.log(uuid + " uuid cached & recently updated");
|
logging.log(uuid + " uuid cached & recently updated");
|
||||||
callback(null, (details.hash ? 1 : 0), details.hash);
|
callback(null, (type ? 1 : 0), type);
|
||||||
} else {
|
} else {
|
||||||
if (details) {
|
if (details) {
|
||||||
logging.log(uuid + " uuid cached, but too old");
|
logging.log(uuid + " uuid cached, but too old");
|
||||||
} else {
|
} else {
|
||||||
logging.log(uuid + " uuid not cached");
|
logging.log(uuid + " uuid not cached");
|
||||||
}
|
}
|
||||||
store_images(uuid, details, function(err, hash) {
|
store_images(uuid, details, raw_type, function(err, hash) {
|
||||||
if (err) {
|
if (err) {
|
||||||
callback(err, -1, details && details.hash);
|
callback(err, -1, details && type);
|
||||||
} else {
|
} else {
|
||||||
// skin is only checked (3) when uuid known AND hash didn't change
|
var status = details && (type === hash) ? 3 : 2;
|
||||||
// in all other cases the skin is downloaded (2)
|
logging.debug(uuid + " old hash: " + (details && type));
|
||||||
var status = details && (details.hash == hash) ? 3 : 2;
|
|
||||||
logging.debug(uuid + " old hash: " + (details && details.hash));
|
|
||||||
logging.log(uuid + " hash: " + hash);
|
logging.log(uuid + " hash: " + hash);
|
||||||
callback(null, status, hash);
|
callback(null, status, hash);
|
||||||
}
|
}
|
||||||
@ -133,17 +195,16 @@ exp.get_image_hash = function(uuid, callback) {
|
|||||||
// image is the user's face+helm when helm is true, or the face otherwise
|
// image is the user's face+helm when helm is true, or the face otherwise
|
||||||
// for status, see get_image_hash
|
// for status, see get_image_hash
|
||||||
exp.get_avatar = function(uuid, helm, size, callback) {
|
exp.get_avatar = function(uuid, helm, size, callback) {
|
||||||
exp.get_image_hash(uuid, function(err, status, hash) {
|
logging.log("request: " + uuid);
|
||||||
|
exp.get_image_hash(uuid, "skin", function(err, status, hash) {
|
||||||
if (hash) {
|
if (hash) {
|
||||||
var facepath = __dirname + "/../" + config.faces_dir + hash + ".png";
|
var facepath = __dirname + "/../" + config.faces_dir + hash + ".png";
|
||||||
var helmpath = __dirname + "/../" + config.helms_dir + hash + ".png";
|
var helmpath = __dirname + "/../" + config.helms_dir + hash + ".png";
|
||||||
var filepath = facepath;
|
var filepath = facepath;
|
||||||
|
fs.exists(helmpath, function(exists) {
|
||||||
fs.exists(helmpath, function (exists) {
|
|
||||||
if (helm && exists) {
|
if (helm && exists) {
|
||||||
filepath = helmpath;
|
filepath = helmpath;
|
||||||
}
|
}
|
||||||
|
|
||||||
skins.resize_img(filepath, size, function(img_err, result) {
|
skins.resize_img(filepath, size, function(img_err, result) {
|
||||||
if (img_err) {
|
if (img_err) {
|
||||||
callback(img_err, -1, null, hash);
|
callback(img_err, -1, null, hash);
|
||||||
@ -164,7 +225,8 @@ exp.get_avatar = function(uuid, helm, size, callback) {
|
|||||||
// handles requests for +uuid+ skins
|
// handles requests for +uuid+ skins
|
||||||
// callback contains error, hash, image buffer
|
// callback contains error, hash, image buffer
|
||||||
exp.get_skin = function(uuid, callback) {
|
exp.get_skin = function(uuid, callback) {
|
||||||
exp.get_image_hash(uuid, function(err, status, hash) {
|
logging.log(uuid + " skin request");
|
||||||
|
exp.get_image_hash(uuid, 'skin', function(err, status, hash) {
|
||||||
var skinpath = __dirname + "/../" + config.skins_dir + hash + ".png";
|
var skinpath = __dirname + "/../" + config.skins_dir + hash + ".png";
|
||||||
fs.exists(skinpath, function (exists) {
|
fs.exists(skinpath, function (exists) {
|
||||||
if (exists) {
|
if (exists) {
|
||||||
@ -173,7 +235,7 @@ exp.get_skin = function(uuid, callback) {
|
|||||||
callback(err, hash, img);
|
callback(err, hash, img);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
networking.save_skin(uuid, hash, skinpath, function(err, img) {
|
networking.save_texture(uuid, hash, skinpath, function(err, img) {
|
||||||
callback(err, hash, img);
|
callback(err, hash, img);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -183,7 +245,7 @@ exp.get_skin = function(uuid, callback) {
|
|||||||
|
|
||||||
function get_type(helm, body) {
|
function get_type(helm, body) {
|
||||||
var text = body ? "body" : "head";
|
var text = body ? "body" : "head";
|
||||||
return helm ? text+"helm" : text;
|
return helm ? text + "helm" : text;
|
||||||
}
|
}
|
||||||
|
|
||||||
// handles creations of skin renders
|
// handles creations of skin renders
|
||||||
@ -195,31 +257,83 @@ exp.get_render = function(uuid, scale, helm, body, callback) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var renderpath = __dirname + "/../" + config.renders_dir + hash + "-" + scale + "-" + get_type(helm, body) + ".png";
|
var renderpath = __dirname + "/../" + config.renders_dir + hash + "-" + scale + "-" + get_type(helm, body) + ".png";
|
||||||
fs.exists(renderpath, function (exists) {
|
fs.exists(renderpath, function(exists) {
|
||||||
if (exists) {
|
if (exists) {
|
||||||
renders.open_render(uuid, renderpath, function(err, img) {
|
renders.open_render(uuid, renderpath, function(err, img) {
|
||||||
callback(err, 1, hash, img);
|
callback(err, 1, hash, img);
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
} else {
|
||||||
if (!img) {
|
if (!img) {
|
||||||
callback(err, 0, hash, null);
|
callback(err, 0, hash, null);
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
renders.draw_model(uuid, img, scale, helm, body, function(err, img) {
|
|
||||||
if (err) {
|
|
||||||
callback(err, -1, hash, null);
|
|
||||||
} else if (!img) {
|
|
||||||
callback(null, 0, hash, null);
|
|
||||||
} else {
|
|
||||||
fs.writeFile(renderpath, img, "binary", function(err){
|
|
||||||
if (err) {
|
|
||||||
logging.log(uuid + " error: " + err);
|
|
||||||
}
|
|
||||||
callback(null, 2, hash, img);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
renders.draw_model(uuid, img, scale, helm, body, function(err, img) {
|
||||||
|
if (err) {
|
||||||
|
callback(err, -1, hash, null);
|
||||||
|
} else if (!img) {
|
||||||
|
callback(null, 0, hash, null);
|
||||||
|
} else {
|
||||||
|
fs.writeFile(renderpath, img, "binary", function(err) {
|
||||||
|
if (err) {
|
||||||
|
logging.log(err);
|
||||||
|
}
|
||||||
|
callback(null, 2, hash, img);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// handles requests for +uuid+ skins
|
||||||
|
// callback contains error, hash, image buffer
|
||||||
|
exp.get_skin = function(uuid, callback) {
|
||||||
|
logging.log(uuid + " skin request");
|
||||||
|
exp.get_image_hash(uuid, "skin", function(err, status, hash) {
|
||||||
|
var skinpath = __dirname + "/../" + config.skins_dir + hash + ".png";
|
||||||
|
fs.exists(skinpath, function(exists) {
|
||||||
|
if (exists) {
|
||||||
|
logging.log("skin already exists, not downloading");
|
||||||
|
skins.open_skin(uuid, skinpath, function(err, img) {
|
||||||
|
callback(err, hash, img);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
networking.save_texture(uuid, hash, skinpath, function(err, response, img) {
|
||||||
|
callback(err, hash, img);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// handles requests for +uuid+ capes
|
||||||
|
// callback contains error, hash, image buffer
|
||||||
|
exp.get_cape = function(uuid, callback) {
|
||||||
|
logging.log(uuid + " cape request");
|
||||||
|
exp.get_image_hash(uuid, "cape", function(err, status, hash) {
|
||||||
|
if (!hash) {
|
||||||
|
callback(err, null, null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var capepath = __dirname + "/../" + config.capes_path + hash + ".png";
|
||||||
|
fs.exists(capepath, function(exists) {
|
||||||
|
if (exists) {
|
||||||
|
logging.log("cape already exists, not downloading");
|
||||||
|
skins.open_skin(uuid, capepath, function(err, img) {
|
||||||
|
callback(err, hash, img);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
networking.save_texture(uuid, hash, capepath, function(err, response, img) {
|
||||||
|
if (response && response.statusCode === 404) {
|
||||||
|
callback(err, hash, null);
|
||||||
|
} else {
|
||||||
|
callback(err, hash, img);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,165 +1,193 @@
|
|||||||
var logging = require("./logging");
|
var logging = require("./logging");
|
||||||
var request = require("request");
|
var request = require("request");
|
||||||
var config = require("./config");
|
var config = require("./config");
|
||||||
var skins = require("./skins");
|
|
||||||
var fs = require("fs");
|
var fs = require("fs");
|
||||||
|
|
||||||
var session_url = "https://sessionserver.mojang.com/session/minecraft/profile/";
|
var session_url = "https://sessionserver.mojang.com/session/minecraft/profile/";
|
||||||
var skins_url = "https://skins.minecraft.net/MinecraftSkins/";
|
var skins_url = "https://skins.minecraft.net/MinecraftSkins/";
|
||||||
|
var capes_url = "https://skins.minecraft.net/MinecraftCloaks/";
|
||||||
|
|
||||||
// exracts the skin url of a +profile+ object
|
var exp = {};
|
||||||
// returns null when no url found (user has no skin)
|
|
||||||
function extract_skin_url(profile) {
|
function extract_url(profile, property) {
|
||||||
var url = null;
|
var url = null;
|
||||||
if (profile && profile.properties) {
|
if (profile && profile.properties) {
|
||||||
profile.properties.forEach(function(prop) {
|
profile.properties.forEach(function(prop) {
|
||||||
if (prop.name == "textures") {
|
if (prop.name === "textures") {
|
||||||
var json = Buffer(prop.value, "base64").toString();
|
var json = new Buffer(prop.value, "base64").toString();
|
||||||
var props = JSON.parse(json);
|
var props = JSON.parse(json);
|
||||||
url = props && props.textures && props.textures.SKIN && props.textures.SKIN.url || null;
|
url = props && props.textures && props.textures[property] && props.textures[property].url || null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return url;
|
return url;
|
||||||
}
|
|
||||||
|
|
||||||
// make a request to skins.miencraft.net
|
|
||||||
// the skin url is taken from the HTTP redirect
|
|
||||||
var get_username_url = function(name, callback) {
|
|
||||||
request.get({
|
|
||||||
url: skins_url + name + ".png",
|
|
||||||
headers: {
|
|
||||||
"User-Agent": "https://crafatar.com"
|
|
||||||
},
|
|
||||||
timeout: config.http_timeout,
|
|
||||||
followRedirect: false
|
|
||||||
}, function(error, response, body) {
|
|
||||||
if (!error && response.statusCode == 301) {
|
|
||||||
// skin_url received successfully
|
|
||||||
logging.log(name + " skin url received");
|
|
||||||
callback(null, response.headers.location);
|
|
||||||
} else if (error) {
|
|
||||||
callback(error, null);
|
|
||||||
} else if (response.statusCode == 404) {
|
|
||||||
// skin (or user) doesn't exist
|
|
||||||
logging.log(name + " has no skin");
|
|
||||||
callback(null, null);
|
|
||||||
} else if (response.statusCode == 429) {
|
|
||||||
// Too Many Requests
|
|
||||||
// Never got this, seems like skins aren't limited
|
|
||||||
logging.warn(name + body || "Too many requests");
|
|
||||||
callback(null, null);
|
|
||||||
} else {
|
|
||||||
logging.error(name + " Unknown error:");
|
|
||||||
logging.error(name + " " + response);
|
|
||||||
callback(body || "Unknown error", null);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// make a request to sessionserver
|
// exracts the skin url of a +profile+ object
|
||||||
// the skin_url is taken from the profile
|
// returns null when no url found (user has no skin)
|
||||||
var get_uuid_url = function(uuid, callback) {
|
exp.extract_skin_url = function(profile) {
|
||||||
request.get({
|
return extract_url(profile, 'SKIN');
|
||||||
url: session_url + uuid,
|
|
||||||
headers: {
|
|
||||||
"User-Agent": "https://crafatar.com"
|
|
||||||
},
|
|
||||||
timeout: config.http_timeout // ms
|
|
||||||
}, function (error, response, body) {
|
|
||||||
if (!error && response.statusCode == 200) {
|
|
||||||
// profile downloaded successfully
|
|
||||||
logging.log(uuid + " profile downloaded");
|
|
||||||
callback(null, extract_skin_url(JSON.parse(body)));
|
|
||||||
} else if (error) {
|
|
||||||
callback(error, null);
|
|
||||||
} else if (response.statusCode == 204 || response.statusCode == 404) {
|
|
||||||
// we get 204 No Content when UUID doesn't exist (including 404 in case they change that)
|
|
||||||
logging.log(uuid + " uuid does not exist");
|
|
||||||
callback(null, null);
|
|
||||||
} else if (response.statusCode == 429) {
|
|
||||||
// Too Many Requests
|
|
||||||
callback(body || "Too many requests", null);
|
|
||||||
} else {
|
|
||||||
logging.error(uuid + " Unknown error:");
|
|
||||||
logging.error(uuid + " " + response);
|
|
||||||
callback(body || "Unknown error", null);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
var exp = {};
|
// exracts the cape url of a +profile+ object
|
||||||
|
// returns null when no url found (user has no cape)
|
||||||
// download skin_url for +uuid+ (name or uuid)
|
exp.extract_cape_url = function(profile) {
|
||||||
// callback contains error, skin_url
|
return extract_url(profile, 'CAPE');
|
||||||
exp.get_skin_url = function(uuid, callback) {
|
|
||||||
if (uuid.length <= 16) {
|
|
||||||
get_username_url(uuid, function(err, url) {
|
|
||||||
callback(err, url);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
get_uuid_url(uuid, function(err, url) {
|
|
||||||
callback(err, url);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// downloads skin file from +url+
|
// makes a GET request to the +url+
|
||||||
// callback contains error, image
|
// +options+ hash includes various options for
|
||||||
exp.get_skin = function(url, uuid, callback) {
|
// encoding and timeouts, defaults are already
|
||||||
|
// specified. +callback+ contains the body, response,
|
||||||
|
// and error buffer. get_from helper method is available
|
||||||
|
exp.get_from_options = function(url, options, callback) {
|
||||||
request.get({
|
request.get({
|
||||||
url: url,
|
url: url,
|
||||||
headers: {
|
headers: {
|
||||||
"User-Agent": "https://crafatar.com"
|
"User-Agent": "https://crafatar.com"
|
||||||
},
|
},
|
||||||
encoding: null, // encoding must be null so we get a buffer
|
timeout: (options.timeout || config.http_timeout),
|
||||||
timeout: config.http_timeout // ms
|
encoding: (options.encoding || null),
|
||||||
}, function (error, response, body) {
|
followRedirect: (options.folow_redirect || false),
|
||||||
if (!error && response.statusCode == 200) {
|
maxAttempts: (options.max_attempts || 2)
|
||||||
// skin downloaded successfully
|
}, function(error, response, body) {
|
||||||
logging.log(uuid + " downloaded skin");
|
// 200 or 301 depending on content type
|
||||||
logging.debug(uuid + " " + url);
|
if (!error && (response.statusCode === 200 || response.statusCode === 301)) {
|
||||||
callback(null, body);
|
// response received successfully
|
||||||
|
logging.log(url + " url received");
|
||||||
|
callback(body, response, null);
|
||||||
|
} else if (error) {
|
||||||
|
callback(body || null, response, error);
|
||||||
|
} else if (response.statusCode === 404) {
|
||||||
|
// page does not exist
|
||||||
|
logging.log(url + " url does not exist");
|
||||||
|
callback(null, response, null);
|
||||||
|
} else if (response.statusCode === 429) {
|
||||||
|
// Too Many Requests exception - code 429
|
||||||
|
logging.warn(body || "Too many requests");
|
||||||
|
callback(body || null, response, error);
|
||||||
} else {
|
} else {
|
||||||
if (error) {
|
logging.error(url + " Unknown error:");
|
||||||
logging.error(uuid + " error downloading '" + url + "': " + error);
|
//logging.error(response);
|
||||||
} else if (response.statusCode == 404) {
|
callback(body || null, response, error);
|
||||||
logging.warn(uuid + " texture not found (404): " + url);
|
|
||||||
} else if (response.statusCode == 429) {
|
|
||||||
// Too Many Requests
|
|
||||||
// Never got this, seems like textures aren't limited
|
|
||||||
logging.warn(uuid + " too many requests for " + url);
|
|
||||||
logging.warn(uuid + " " + body);
|
|
||||||
} else {
|
|
||||||
logging.error(uuid + " unknown error for " + url);
|
|
||||||
logging.error(uuid + " " + response);
|
|
||||||
logging.error(uuid + " " + body);
|
|
||||||
error = "unknown error"; // Error needs to be set, otherwise null in callback
|
|
||||||
}
|
|
||||||
callback(error, null);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
exp.save_skin = function(uuid, hash, outpath, callback) {
|
// helper method for get_from_options, no options required
|
||||||
|
exp.get_from = function(url, callback) {
|
||||||
|
exp.get_from_options(url, {}, function(body, response, err) {
|
||||||
|
callback(body, response, err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// specifies which numbers identify what url
|
||||||
|
var mojang_url_types = {
|
||||||
|
1: skins_url,
|
||||||
|
2: capes_url
|
||||||
|
};
|
||||||
|
|
||||||
|
// make a request to skins.miencraft.net
|
||||||
|
// the skin url is taken from the HTTP redirect
|
||||||
|
// type reference is above
|
||||||
|
exp.get_username_url = function(name, type, callback) {
|
||||||
|
exp.get_from(mojang_url_types[type] + name + ".png", function(body, response, err) {
|
||||||
|
if (!err) {
|
||||||
|
callback(err, response ? (response.statusCode === 404 ? null : response.headers.location) : null);
|
||||||
|
} else {
|
||||||
|
callback(err, null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// gets the URL for a skin/cape from the profile
|
||||||
|
// +type+ specifies which to retrieve
|
||||||
|
exp.get_uuid_url = function(profile, type, callback) {
|
||||||
|
var url = null;
|
||||||
|
if (type === 1) {
|
||||||
|
url = exp.extract_skin_url(profile);
|
||||||
|
} else if (type === 2) {
|
||||||
|
url = exp.extract_cape_url(profile);
|
||||||
|
}
|
||||||
|
callback(url || null);
|
||||||
|
};
|
||||||
|
|
||||||
|
// make a request to sessionserver
|
||||||
|
// profile is returned as json
|
||||||
|
exp.get_profile = function(uuid, callback) {
|
||||||
|
if (!uuid) {
|
||||||
|
callback(null, null);
|
||||||
|
} else {
|
||||||
|
exp.get_from_options(session_url + uuid, { encoding: "utf8" }, function(body, response, err) {
|
||||||
|
callback(err || null, (body !== null ? JSON.parse(body) : null));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// +uuid+ is likely a username and if so
|
||||||
|
// +uuid+ is used to get the url, otherwise
|
||||||
|
// +profile+ will be used to get the url
|
||||||
|
exp.get_skin_url = function(uuid, profile, callback) {
|
||||||
|
getUrl(uuid, profile, 1, function(url) {
|
||||||
|
callback(url);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// +uuid+ is likely a username and if so
|
||||||
|
// +uuid+ is used to get the url, otherwise
|
||||||
|
// +profile+ will be used to get the url
|
||||||
|
exp.get_cape_url = function(uuid, profile, callback) {
|
||||||
|
getUrl(uuid, profile, 2, function(url) {
|
||||||
|
callback(url);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function getUrl(uuid, profile, type, callback) {
|
||||||
|
if (uuid.length <= 16) {
|
||||||
|
//username
|
||||||
|
exp.get_username_url(uuid, type, function(err, url) {
|
||||||
|
callback(url || null);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
exp.get_uuid_url(profile, type, function(url) {
|
||||||
|
callback(url || null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// downloads skin file from +url+
|
||||||
|
// callback contains error, image
|
||||||
|
exp.get_skin = function(url, uuid, callback) {
|
||||||
|
exp.get_from(url, function(body, response, err) {
|
||||||
|
callback(body, err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exp.save_texture = function(uuid, hash, outpath, callback) {
|
||||||
if (hash) {
|
if (hash) {
|
||||||
var skinurl = "http://textures.minecraft.net/texture/" + hash;
|
var textureurl = "http://textures.minecraft.net/texture/" + hash;
|
||||||
exp.get_skin(skinurl, uuid, function(err, img) {
|
exp.get_from(textureurl, function(img, response, err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
logging.error(uuid + " error while downloading skin");
|
logging.error(uuid + "error while downloading texture");
|
||||||
callback(err, null);
|
callback(err, response, null);
|
||||||
} else {
|
} else {
|
||||||
fs.writeFile(outpath, img, "binary", function(err){
|
fs.writeFile(outpath, img, "binary", function(err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
logging.log(uuid + " error: " + err);
|
logging.log(uuid + " error: " + err);
|
||||||
}
|
}
|
||||||
callback(null, img);
|
callback(err, response, img);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
callback(null, null);
|
callback(null, null, null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
exp.get_cape = function(url, callback) {
|
||||||
|
exp.get_from(url, function(body, response, err) {
|
||||||
|
callback(err, body);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = exp;
|
module.exports = exp;
|
||||||
@ -140,7 +140,7 @@ exp.draw_model = function(uuid, img, scale, helm, body, callback) {
|
|||||||
|
|
||||||
image.onload = function() {
|
image.onload = function() {
|
||||||
var width = 64 * scale;
|
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 height = original_height * scale;
|
||||||
var model_canvas = new Canvas(20 * scale, (body ? 44.8 : 17.6) * scale);
|
var model_canvas = new Canvas(20 * scale, (body ? 44.8 : 17.6) * scale);
|
||||||
var skin_canvas = new Canvas(width, height);
|
var skin_canvas = new Canvas(width, height);
|
||||||
|
|||||||
@ -62,10 +62,10 @@ exp.extract_helm = function(uuid, facefile, buffer, outname, callback) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// resizes the image file +inname+ to +size+ by +size+ pixels
|
// resizes the image file +inname+ to +size+ by +size+ pixels
|
||||||
@ -96,11 +96,30 @@ exp.default_skin = function(uuid) {
|
|||||||
// helper method for opening a skin file from +skinpath+
|
// helper method for opening a skin file from +skinpath+
|
||||||
// callback contains error, image buffer
|
// callback contains error, image buffer
|
||||||
exp.open_skin = function(uuid, skinpath, callback) {
|
exp.open_skin = function(uuid, skinpath, callback) {
|
||||||
fs.readFile(skinpath, function (err, buf) {
|
fs.readFile(skinpath, function(err, buf) {
|
||||||
if (err) {
|
if (err) {
|
||||||
logging.error(uuid + " error while opening skin file: " + err);
|
logging.error(uuid + " error while opening skin file: " + err);
|
||||||
|
callback(err, null)
|
||||||
|
} else {
|
||||||
|
callback(null, buf);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exp.save_image = function(buffer, outpath, callback) {
|
||||||
|
lwip.open(buffer, "png", function(err, image) {
|
||||||
|
if (err) {
|
||||||
|
callback(err);
|
||||||
|
} else {
|
||||||
|
image.batch()
|
||||||
|
.writeFile(outpath, function(err) {
|
||||||
|
if (err) {
|
||||||
|
callback(err);
|
||||||
|
} else {
|
||||||
|
callback(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
callback(err, buf);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -29,10 +29,10 @@
|
|||||||
"canvas": "1.1.6",
|
"canvas": "1.1.6",
|
||||||
"jade": "~1.8.2",
|
"jade": "~1.8.2",
|
||||||
"lwip": "0.0.6",
|
"lwip": "0.0.6",
|
||||||
"redis": "0.12.1",
|
"mime": "1.2.11",
|
||||||
"request": "2.51.0",
|
|
||||||
"node-df": "0.1.1",
|
"node-df": "0.1.1",
|
||||||
"mime": "1.2.11"
|
"redis": "0.12.1",
|
||||||
|
"request": "^2.51.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"coveralls": "^2.11.2",
|
"coveralls": "^2.11.2",
|
||||||
|
|||||||
@ -195,6 +195,13 @@ h4 {
|
|||||||
background-image: url("/skins/0?default=alex");
|
background-image: url("/skins/0?default=alex");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#cape-example-1:hover .preview {
|
||||||
|
background-image: url("/capes/Dinnerbone");
|
||||||
|
}
|
||||||
|
#cape-example-2:hover .preview {
|
||||||
|
background-image: url("/capes/md_5");
|
||||||
|
}
|
||||||
|
|
||||||
img.preload {
|
img.preload {
|
||||||
/*
|
/*
|
||||||
preload hover images
|
preload hover images
|
||||||
|
|||||||
@ -54,7 +54,7 @@ module.exports = function(req, res) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
etag = image && hash && hash.substr(0, 32) || "none";
|
etag = image && hash && hash.substr(0, 32) || "none";
|
||||||
var matches = req.headers["if-none-match"] == '"' + etag + '"';
|
var matches = req.headers["if-none-match"] === '"' + etag + '"';
|
||||||
if (image) {
|
if (image) {
|
||||||
var http_status = 200;
|
var http_status = 200;
|
||||||
if (matches) {
|
if (matches) {
|
||||||
@ -103,6 +103,6 @@ module.exports = function(req, res) {
|
|||||||
"Access-Control-Allow-Origin": "*",
|
"Access-Control-Allow-Origin": "*",
|
||||||
"Etag": '"' + etag + '"'
|
"Etag": '"' + etag + '"'
|
||||||
});
|
});
|
||||||
res.end(http_status == 304 ? null : image);
|
res.end(http_status === 304 ? null : image);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
79
routes/capes.js
Normal file
79
routes/capes.js
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
var logging = require("../modules/logging");
|
||||||
|
var helpers = require("../modules/helpers");
|
||||||
|
var config = require("../modules/config");
|
||||||
|
|
||||||
|
var human_status = {
|
||||||
|
0: "none",
|
||||||
|
1: "cached",
|
||||||
|
2: "downloaded",
|
||||||
|
3: "checked",
|
||||||
|
"-1": "error"
|
||||||
|
};
|
||||||
|
|
||||||
|
// GET cape request
|
||||||
|
module.exports = function(req, res) {
|
||||||
|
var start = new Date();
|
||||||
|
var uuid = (req.url.pathname.split("/")[2] || "").split(".")[0];
|
||||||
|
var etag = null;
|
||||||
|
|
||||||
|
if (!helpers.uuid_valid(uuid)) {
|
||||||
|
res.writeHead(422, {
|
||||||
|
"Content-Type": "text/plain",
|
||||||
|
"Response-Time": new Date() - start
|
||||||
|
});
|
||||||
|
res.end("Invalid ID");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// strip dashes
|
||||||
|
uuid = uuid.replace(/-/g, "");
|
||||||
|
|
||||||
|
try {
|
||||||
|
helpers.get_cape(uuid, function(err, status, image, hash) {
|
||||||
|
logging.log(uuid + " - " + human_status[status]);
|
||||||
|
if (err) {
|
||||||
|
logging.error(uuid + " " + err);
|
||||||
|
}
|
||||||
|
etag = hash && hash.substr(0, 32) || "none";
|
||||||
|
var matches = req.headers["if-none-match"] === '"' + etag + '"';
|
||||||
|
if (image) {
|
||||||
|
var http_status = 200;
|
||||||
|
if (matches) {
|
||||||
|
http_status = 304;
|
||||||
|
} else if (err) {
|
||||||
|
http_status = 503;
|
||||||
|
}
|
||||||
|
logging.debug("Etag: " + req.headers["if-none-match"]);
|
||||||
|
logging.debug("matches: " + matches);
|
||||||
|
logging.log("status: " + http_status);
|
||||||
|
sendimage(http_status, status, image);
|
||||||
|
} else {
|
||||||
|
res.writeHead(404, {
|
||||||
|
"Content-Type": "text/plain",
|
||||||
|
"Response-Time": new Date() - start
|
||||||
|
});
|
||||||
|
res.end("404 not found");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch(e) {
|
||||||
|
logging.error(uuid + " error:");
|
||||||
|
logging.error(e);
|
||||||
|
res.writeHead(500, {
|
||||||
|
"Content-Type": "text/plain",
|
||||||
|
"Response-Time": new Date() - start
|
||||||
|
});
|
||||||
|
res.end("500 server error");
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendimage(http_status, img_status, image) {
|
||||||
|
res.writeHead(http_status, {
|
||||||
|
"Content-Type": "image/png",
|
||||||
|
"Cache-Control": "max-age=" + config.browser_cache_time + ", public",
|
||||||
|
"Response-Time": new Date() - start,
|
||||||
|
"X-Storage-Type": human_status[img_status],
|
||||||
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
"Etag": '"' + etag + '"'
|
||||||
|
});
|
||||||
|
res.end(http_status === 304 ? null : image);
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -23,7 +23,7 @@ module.exports = function(req, res) {
|
|||||||
var raw_type = (req.url.path_list[2] || "");
|
var raw_type = (req.url.path_list[2] || "");
|
||||||
|
|
||||||
// validate type
|
// validate type
|
||||||
if (raw_type != "body" && raw_type != "head") {
|
if (raw_type !== "body" && raw_type !== "head") {
|
||||||
res.writeHead(422, {
|
res.writeHead(422, {
|
||||||
"Content-Type": "text/plain",
|
"Content-Type": "text/plain",
|
||||||
"Response-Time": new Date() - start
|
"Response-Time": new Date() - start
|
||||||
@ -32,7 +32,7 @@ module.exports = function(req, res) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var body = raw_type == "body";
|
var body = raw_type === "body";
|
||||||
var uuid = (req.url.path_list[3] || "").split(".")[0];
|
var uuid = (req.url.path_list[3] || "").split(".")[0];
|
||||||
var def = req.url.query.default;
|
var def = req.url.query.default;
|
||||||
var scale = parseInt(req.url.query.scale) || config.default_scale;
|
var scale = parseInt(req.url.query.scale) || config.default_scale;
|
||||||
@ -65,7 +65,7 @@ module.exports = function(req, res) {
|
|||||||
logging.error(uuid + " " + err);
|
logging.error(uuid + " " + err);
|
||||||
}
|
}
|
||||||
etag = hash && hash.substr(0, 32) || "none";
|
etag = hash && hash.substr(0, 32) || "none";
|
||||||
var matches = req.headers["if-none-match"] == '"' + etag + '"';
|
var matches = req.headers["if-none-match"] === '"' + etag + '"';
|
||||||
if (image) {
|
if (image) {
|
||||||
var http_status = 200;
|
var http_status = 200;
|
||||||
if (matches) {
|
if (matches) {
|
||||||
@ -128,6 +128,6 @@ module.exports = function(req, res) {
|
|||||||
"Access-Control-Allow-Origin": "*",
|
"Access-Control-Allow-Origin": "*",
|
||||||
"Etag": '"' + etag + '"'
|
"Etag": '"' + etag + '"'
|
||||||
});
|
});
|
||||||
res.end(http_status == 304 ? null : image);
|
res.end(http_status === 304 ? null : image);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -31,7 +31,7 @@ module.exports = function(req, res) {
|
|||||||
logging.error(uuid + " " + err);
|
logging.error(uuid + " " + err);
|
||||||
}
|
}
|
||||||
etag = hash && hash.substr(0, 32) || "none";
|
etag = hash && hash.substr(0, 32) || "none";
|
||||||
var matches = req.headers["if-none-match"] == '"' + etag + '"';
|
var matches = req.headers["if-none-match"] === '"' + etag + '"';
|
||||||
if (image) {
|
if (image) {
|
||||||
var http_status = 200;
|
var http_status = 200;
|
||||||
if (matches) {
|
if (matches) {
|
||||||
@ -82,6 +82,6 @@ module.exports = function(req, res) {
|
|||||||
"Access-Control-Allow-Origin": "*",
|
"Access-Control-Allow-Origin": "*",
|
||||||
"Etag": '"' + etag + '"'
|
"Etag": '"' + etag + '"'
|
||||||
});
|
});
|
||||||
res.end(http_status == 304 ? null : image);
|
res.end(http_status === 304 ? null : image);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -5,7 +5,6 @@ var config = require("./modules/config");
|
|||||||
var clean = require("./modules/cleaner");
|
var clean = require("./modules/cleaner");
|
||||||
var http = require("http");
|
var http = require("http");
|
||||||
var mime = require("mime");
|
var mime = require("mime");
|
||||||
var path = require("path");
|
|
||||||
var url = require("url");
|
var url = require("url");
|
||||||
var fs = require("fs");
|
var fs = require("fs");
|
||||||
|
|
||||||
@ -13,7 +12,8 @@ var routes = {
|
|||||||
index: require("./routes/index"),
|
index: require("./routes/index"),
|
||||||
avatars: require("./routes/avatars"),
|
avatars: require("./routes/avatars"),
|
||||||
skins: require("./routes/skins"),
|
skins: require("./routes/skins"),
|
||||||
renders: require("./routes/renders")
|
renders: require("./routes/renders"),
|
||||||
|
capes: require("./routes/capes")
|
||||||
};
|
};
|
||||||
|
|
||||||
function asset_request(req, res) {
|
function asset_request(req, res) {
|
||||||
@ -37,7 +37,7 @@ function requestHandler(req, res) {
|
|||||||
request.url.query = request.url.query || {};
|
request.url.query = request.url.query || {};
|
||||||
|
|
||||||
// remove trailing and double slashes + other junk
|
// remove trailing and double slashes + other junk
|
||||||
var path_list = path.resolve(request.url.pathname).split("/");
|
var path_list = request.url.pathname.split("/");
|
||||||
for (var i = 0; i < path_list.length; i++) {
|
for (var i = 0; i < path_list.length; i++) {
|
||||||
// URL decode
|
// URL decode
|
||||||
path_list[i] = querystring.unescape(path_list[i]);
|
path_list[i] = querystring.unescape(path_list[i]);
|
||||||
@ -61,6 +61,9 @@ function requestHandler(req, res) {
|
|||||||
case "renders":
|
case "renders":
|
||||||
routes.renders(request, res);
|
routes.renders(request, res);
|
||||||
break;
|
break;
|
||||||
|
case "capes":
|
||||||
|
routes.capes(request, res);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
asset_request(request, res);
|
asset_request(request, res);
|
||||||
}
|
}
|
||||||
|
|||||||
123
test/test.js
123
test/test.js
@ -8,12 +8,13 @@ var config = require("../modules/config");
|
|||||||
var skins = require("../modules/skins");
|
var skins = require("../modules/skins");
|
||||||
var cache = require("../modules/cache");
|
var cache = require("../modules/cache");
|
||||||
var renders = require("../modules/renders");
|
var renders = require("../modules/renders");
|
||||||
|
var cleaner = require("../modules/cleaner")
|
||||||
|
|
||||||
// we don't want tests to fail because of slow internet
|
// we don't want tests to fail because of slow internet
|
||||||
config.http_timeout *= 3;
|
config.http_timeout *= 3;
|
||||||
|
|
||||||
// no spam
|
// no spam
|
||||||
logging.log = function(){};
|
logging.log = function() {};
|
||||||
|
|
||||||
var uuids = fs.readFileSync("test/uuids.txt").toString().split(/\r?\n/);
|
var uuids = fs.readFileSync("test/uuids.txt").toString().split(/\r?\n/);
|
||||||
var names = fs.readFileSync("test/usernames.txt").toString().split(/\r?\n/);
|
var names = fs.readFileSync("test/usernames.txt").toString().split(/\r?\n/);
|
||||||
@ -22,10 +23,14 @@ var names = fs.readFileSync("test/usernames.txt").toString().split(/\r?\n/);
|
|||||||
var uuid = uuids[Math.round(Math.random() * (uuids.length - 1))];
|
var uuid = uuids[Math.round(Math.random() * (uuids.length - 1))];
|
||||||
var name = names[Math.round(Math.random() * (names.length - 1))];
|
var name = names[Math.round(Math.random() * (names.length - 1))];
|
||||||
|
|
||||||
|
function getRandomInt(min, max) {
|
||||||
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||||
|
}
|
||||||
|
|
||||||
var ids = [
|
var ids = [
|
||||||
uuid.toLowerCase(),
|
uuid.toLowerCase(),
|
||||||
uuid.toUpperCase(),
|
|
||||||
name.toLowerCase(),
|
name.toLowerCase(),
|
||||||
|
uuid.toUpperCase(),
|
||||||
name.toUpperCase()
|
name.toUpperCase()
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -35,6 +40,7 @@ describe("Crafatar", function() {
|
|||||||
|
|
||||||
before(function() {
|
before(function() {
|
||||||
cache.get_redis().flushall();
|
cache.get_redis().flushall();
|
||||||
|
cleaner.run();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("UUID/username", function() {
|
describe("UUID/username", function() {
|
||||||
@ -79,13 +85,14 @@ describe("Crafatar", function() {
|
|||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
it("should not exist (uuid)", function(done) {
|
it("should not exist (uuid)", function(done) {
|
||||||
networking.get_skin_url("00000000000000000000000000000000", function(err, profile) {
|
var number = getRandomInt(0, 9).toString();
|
||||||
assert.strictEqual(err, null);
|
networking.get_profile(Array(33).join(number), function(err, profile) {
|
||||||
|
assert.strictEqual(profile, null);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it("should not exist (username)", function(done) {
|
it("should not exist (username)", function(done) {
|
||||||
networking.get_skin_url("Steve", function(err, profile) {
|
networking.get_username_url("Steve", 1, function(err, profile) {
|
||||||
assert.strictEqual(err, null);
|
assert.strictEqual(err, null);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
@ -99,10 +106,13 @@ describe("Crafatar", function() {
|
|||||||
var steven_uuid = "b8ffc3d37dbf48278f69475f6690aabd";
|
var steven_uuid = "b8ffc3d37dbf48278f69475f6690aabd";
|
||||||
|
|
||||||
it("uuid's account should exist, but skin should not", function(done) {
|
it("uuid's account should exist, but skin should not", function(done) {
|
||||||
helpers.get_avatar(alex_uuid, false, 160, function(err, status, image) {
|
networking.get_profile(alex_uuid, function(err, profile) {
|
||||||
assert.strictEqual(status, 2);
|
assert.notStrictEqual(profile, null);
|
||||||
done();
|
networking.get_uuid_url(profile, 1, function(url) {
|
||||||
});
|
assert.strictEqual(url, null);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
})
|
||||||
});
|
});
|
||||||
it("odd UUID should default to Alex", function(done) {
|
it("odd UUID should default to Alex", function(done) {
|
||||||
assert.strictEqual(skins.default_skin(alex_uuid), "alex");
|
assert.strictEqual(skins.default_skin(alex_uuid), "alex");
|
||||||
@ -117,7 +127,7 @@ describe("Crafatar", function() {
|
|||||||
it("should time out on uuid info download", function(done) {
|
it("should time out on uuid info download", function(done) {
|
||||||
var original_timeout = config.http_timeout;
|
var original_timeout = config.http_timeout;
|
||||||
config.http_timeout = 1;
|
config.http_timeout = 1;
|
||||||
networking.get_skin_url("069a79f444e94726a5befca90e38aaf5", function(err, skin_url) {
|
networking.get_profile("069a79f444e94726a5befca90e38aaf5", function(err, profile) {
|
||||||
assert.strictEqual(err.code, "ETIMEDOUT");
|
assert.strictEqual(err.code, "ETIMEDOUT");
|
||||||
config.http_timeout = original_timeout;
|
config.http_timeout = original_timeout;
|
||||||
done();
|
done();
|
||||||
@ -126,7 +136,7 @@ describe("Crafatar", function() {
|
|||||||
it("should time out on username info download", function(done) {
|
it("should time out on username info download", function(done) {
|
||||||
var original_timeout = config.http_timeout;
|
var original_timeout = config.http_timeout;
|
||||||
config.http_timeout = 1;
|
config.http_timeout = 1;
|
||||||
networking.get_skin_url("redstone_sheep", function(err, skin_url) {
|
networking.get_username_url("redstone_sheep", 1, function(err, url) {
|
||||||
assert.strictEqual(err.code, "ETIMEDOUT");
|
assert.strictEqual(err.code, "ETIMEDOUT");
|
||||||
config.http_timeout = original_timeout;
|
config.http_timeout = original_timeout;
|
||||||
done();
|
done();
|
||||||
@ -135,15 +145,15 @@ describe("Crafatar", function() {
|
|||||||
it("should time out on skin download", function(done) {
|
it("should time out on skin download", function(done) {
|
||||||
var original_timeout = config.http_timeout;
|
var original_timeout = config.http_timeout;
|
||||||
config.http_timeout = 1;
|
config.http_timeout = 1;
|
||||||
networking.get_skin("http://textures.minecraft.net/texture/477be35554684c28bdeee4cf11c591d3c88afb77e0b98da893fd7bc318c65184", uuid, function(err, img) {
|
networking.get_from("http://textures.minecraft.net/texture/477be35554684c28bdeee4cf11c591d3c88afb77e0b98da893fd7bc318c65184", function(body, res, error) {
|
||||||
assert.strictEqual(err.code, "ETIMEDOUT");
|
assert.strictEqual(error.code, "ETIMEDOUT");
|
||||||
config.http_timeout = original_timeout;
|
config.http_timeout = original_timeout;
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it("should not find the skin", function(done) {
|
it("should not find the skin", function(done) {
|
||||||
assert.doesNotThrow(function() {
|
assert.doesNotThrow(function() {
|
||||||
networking.get_skin("http://textures.minecraft.net/texture/this-does-not-exist", uuid, function(err, img) {
|
networking.get_from("http://textures.minecraft.net/texture/this-does-not-exist", function(img, response, err) {
|
||||||
assert.strictEqual(err, null); // no error here, but it shouldn't throw exceptions
|
assert.strictEqual(err, null); // no error here, but it shouldn't throw exceptions
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
@ -155,8 +165,73 @@ describe("Crafatar", function() {
|
|||||||
});
|
});
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
it("should not find the file", function(done) {
|
||||||
|
skins.open_skin("TestUUID", 'non/existant/path', function(err, img) {
|
||||||
|
assert.notStrictEqual(err, null);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// we have to make sure that we test both a 32x64 and 64x64 skin
|
||||||
|
describe("Networking: Render", function() {
|
||||||
|
it("should not fail (username, 32x64 skin)", function(done) {
|
||||||
|
helpers.get_render("md_5", 6, true, true, function(err, hash, img) {
|
||||||
|
assert.strictEqual(err, null);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it("should not fail (username, 64x64 skin)", function(done) {
|
||||||
|
helpers.get_render("Jake0oo0", 6, true, true, function(err, hash, img) {
|
||||||
|
assert.strictEqual(err, null);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Networking: Cape", function() {
|
||||||
|
it("should not fail (guaranteed cape)", function(done) {
|
||||||
|
helpers.get_cape("Dinnerbone", function(err, hash, img) {
|
||||||
|
assert.strictEqual(err, null);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it("should already exist", function(done) {
|
||||||
|
before(function() {
|
||||||
|
cache.get_redis().flushall();
|
||||||
|
});
|
||||||
|
helpers.get_cape("Dinnerbone", function(err, hash, img) {
|
||||||
|
assert.strictEqual(err, null);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it("should not be found", function(done) {
|
||||||
|
helpers.get_cape("Jake0oo0", function(err, hash, img) {
|
||||||
|
assert.strictEqual(img, null);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Networking: Skin", function() {
|
||||||
|
it("should not fail", function(done) {
|
||||||
|
helpers.get_cape("Jake0oo0", function(err, hash, img) {
|
||||||
|
assert.strictEqual(err, null);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it("should already exist", function(done) {
|
||||||
|
before(function() {
|
||||||
|
cache.get_redis().flushall();
|
||||||
|
});
|
||||||
|
helpers.get_cape("Jake0oo0", function(err, hash, img) {
|
||||||
|
assert.strictEqual(err, null);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
// DRY with uuid and username tests
|
// DRY with uuid and username tests
|
||||||
for (var i in ids) {
|
for (var i in ids) {
|
||||||
var id = ids[i];
|
var id = ids[i];
|
||||||
@ -206,17 +281,23 @@ describe("Crafatar", function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("Networking: Render", function() {
|
describe("Networking: Render", function() {
|
||||||
it("should not fail (username, 64x64 skin)", function(done) {
|
it("should not fail (full body)", function(done) {
|
||||||
helpers.get_render("Jake0oo0", 6, true, true, function(err, hash, img) {
|
helpers.get_render(id, 6, true, true, function(err, hash, img) {
|
||||||
|
assert.strictEqual(err, null);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it("should not fail (only head)", function(done) {
|
||||||
|
helpers.get_render(id, 6, true, false, function(err, hash, img) {
|
||||||
assert.strictEqual(err, null);
|
assert.strictEqual(err, null);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Networking: Render", function() {
|
describe("Networking: Cape", function() {
|
||||||
it("should not fail (username, 32x64 skin)", function(done) {
|
it("should not fail (possible cape)", function(done) {
|
||||||
helpers.get_render("md_5", 6, true, true, function(err, hash, img) {
|
helpers.get_cape(id, function(err, hash, img) {
|
||||||
assert.strictEqual(err, null);
|
assert.strictEqual(err, null);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
@ -231,8 +312,8 @@ describe("Crafatar", function() {
|
|||||||
|
|
||||||
if (id_type == "uuid") {
|
if (id_type == "uuid") {
|
||||||
it("uuid should be rate limited", function(done) {
|
it("uuid should be rate limited", function(done) {
|
||||||
helpers.get_avatar(id, false, 160, function(err, status, image) {
|
networking.get_profile(id, function(err, profile) {
|
||||||
assert.strictEqual(JSON.parse(err).error, "TooManyRequestsException");
|
assert.strictEqual(profile.error, "TooManyRequestsException");
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -220,6 +220,34 @@ block content
|
|||||||
| Hover over the examples for a preview!
|
| Hover over the examples for a preview!
|
||||||
.preview-background
|
.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.<br>
|
||||||
|
| Replace
|
||||||
|
mark.green id
|
||||||
|
| with a Mojang <b>UUID</b> or <b>username</b> to get the related cape.<br>
|
||||||
|
| The user's cape is returned, otherwise a 404 is thrown.<br>
|
||||||
|
.code
|
||||||
|
| #{domain}/skins/
|
||||||
|
mark.green id
|
||||||
|
|
||||||
|
section
|
||||||
|
a(id="cape-examples", class="anchor")
|
||||||
|
a(href="#cape-examples")
|
||||||
|
h4 Cape Examples
|
||||||
|
.code
|
||||||
|
#cape-example-1.example-wrapper
|
||||||
|
.example #{domain}/capes/Dinnerbone
|
||||||
|
p.preview Dinnerbone's Cape <i>Mojang capes are not transparent...</i>
|
||||||
|
#cape-example-2.example-wrapper
|
||||||
|
.example #{domain}/capes/md_5
|
||||||
|
p.preview md_5's Cape
|
||||||
|
p.preview-placeholder
|
||||||
|
| Hover over the examples for a preview!
|
||||||
|
.preview-background
|
||||||
|
|
||||||
section
|
section
|
||||||
a(id="meta" class="anchor")
|
a(id="meta" class="anchor")
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user