mirror of
https://github.com/azures04/crafatar.git
synced 2026-05-06 11:00:39 +02:00
mv modules/ lib/
that's what all the cool kids do
This commit is contained in:
165
lib/cache.js
Normal file
165
lib/cache.js
Normal file
@@ -0,0 +1,165 @@
|
||||
var logging = require("./logging");
|
||||
var node_redis = require("redis");
|
||||
var config = require("./config");
|
||||
var url = require("url");
|
||||
var fs = require("fs");
|
||||
|
||||
var redis = null;
|
||||
|
||||
// sets up redis connection
|
||||
// flushes redis when running on heroku (files aren't kept between pushes)
|
||||
function connect_redis() {
|
||||
logging.log("connecting to redis...");
|
||||
// parse redis env
|
||||
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";
|
||||
// connect to redis
|
||||
redis = node_redis.createClient(redis_url.port, redis_url.hostname);
|
||||
if (redis_url.auth) {
|
||||
redis.auth(redis_url.auth.split(":")[1]);
|
||||
}
|
||||
redis.on("ready", function() {
|
||||
logging.log("Redis connection established.");
|
||||
if(process.env.HEROKU) {
|
||||
logging.log("Running on heroku, flushing redis");
|
||||
redis.flushall();
|
||||
}
|
||||
});
|
||||
redis.on("error", function (err) {
|
||||
logging.error(err);
|
||||
});
|
||||
redis.on("end", function () {
|
||||
logging.warn("Redis connection lost!");
|
||||
});
|
||||
}
|
||||
|
||||
// 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 path = config.faces_dir + skin_hash + ".png";
|
||||
fs.exists(path, function(exists) {
|
||||
if (exists) {
|
||||
var date = new Date();
|
||||
fs.utimes(path, date, date, function(err){
|
||||
if (err) {
|
||||
logging.error(rid + "Error: " + err.stack);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
logging.error(rid + "tried to update " + path + " date, but it does not exist");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var exp = {};
|
||||
|
||||
// returns the redis instance
|
||||
exp.get_redis = function() {
|
||||
return redis;
|
||||
};
|
||||
|
||||
|
||||
// updates the redis instance's server_info object
|
||||
// callback contains error, info object
|
||||
exp.info = function(callback) {
|
||||
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) {
|
||||
var parts = line.split(":");
|
||||
if (parts[1]) {
|
||||
obj[parts[0]] = parts[1];
|
||||
}
|
||||
});
|
||||
obj.versions = [];
|
||||
if( obj.redis_version ){
|
||||
obj.redis_version.split(".").forEach(function(num) {
|
||||
obj.versions.push(+num);
|
||||
});
|
||||
}
|
||||
redis.server_info = obj;
|
||||
|
||||
callback(err, redis.server_info);
|
||||
});
|
||||
};
|
||||
|
||||
// sets the timestamp for +userId+ and its face file's (+hash+) date to the current time
|
||||
// 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+ contains error
|
||||
exp.update_timestamp = function(rid, userId, hash, temp, callback) {
|
||||
logging.log(rid + "cache: updating timestamp");
|
||||
sub = temp ? (config.local_cache_time - 60) : 0;
|
||||
var time = new Date().getTime() - sub;
|
||||
// store userId in lower case if not null
|
||||
userId = userId && userId.toLowerCase();
|
||||
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
|
||||
// if either +skin_hash+ or +cape_hash+ are undefined, they will not be stored
|
||||
// this feature can be used to write both cape and skin at separate times
|
||||
// +callback+ contans error
|
||||
exp.save_hash = function(rid, userId, skin_hash, cape_hash, callback) {
|
||||
logging.log(rid + "cache: saving skin:" + skin_hash + " cape:" + cape_hash);
|
||||
var time = new Date().getTime();
|
||||
// store shorter null byte instead of "null"
|
||||
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) {
|
||||
redis.hmset(userId, "c", cape_hash, "t", time, function(err){
|
||||
callback(err);
|
||||
});
|
||||
} else if (cape_hash === undefined) {
|
||||
redis.hmset(userId, "s", skin_hash, "t", time, function(err){
|
||||
callback(err);
|
||||
});
|
||||
} else {
|
||||
redis.hmset(userId, "s", skin_hash, "c", cape_hash, "t", time, function(err){
|
||||
callback(err);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// removes the hash for +userId+ from the cache
|
||||
exp.remove_hash = function(rid, userId) {
|
||||
logging.log(rid + "cache: deleting hash");
|
||||
redis.del(userId.toLowerCase(), "h", "t");
|
||||
};
|
||||
|
||||
// get a details object for +userId+
|
||||
// {skin: "0123456789abcdef", cape: "gs1gds1g5d1g5ds1", time: 1414881524512}
|
||||
// +callback+ contains error, details
|
||||
// details is null when userId not cached
|
||||
exp.get_details = function(userId, callback) {
|
||||
// get userId in lower case if not null
|
||||
userId = userId && userId.toLowerCase();
|
||||
redis.hgetall(userId, function(err, data) {
|
||||
var details = null;
|
||||
if (data) {
|
||||
details = {
|
||||
skin: data.s === "" ? null : data.s,
|
||||
cape: data.c === "" ? null : data.c,
|
||||
time: Number(data.t)
|
||||
};
|
||||
}
|
||||
callback(err, details);
|
||||
});
|
||||
};
|
||||
|
||||
connect_redis();
|
||||
module.exports = exp;
|
||||
100
lib/cleaner.js
Normal file
100
lib/cleaner.js
Normal file
@@ -0,0 +1,100 @@
|
||||
var logging = require("./logging");
|
||||
var config = require("./config");
|
||||
var cache = require("./cache");
|
||||
var df = require("node-df");
|
||||
var fs = require("fs");
|
||||
|
||||
var redis = cache.get_redis();
|
||||
var exp = {};
|
||||
|
||||
// compares redis' used_memory with cleaning_redis_limit
|
||||
// callback contains error, true|false
|
||||
function should_clean_redis(callback) {
|
||||
cache.info(function(err, info) {
|
||||
if (err) {
|
||||
callback(err, false);
|
||||
} 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.cleaning_redis_limit);
|
||||
} catch(e) {
|
||||
callback(e, false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// uses `df` to get the available fisk space
|
||||
// callback contains error, true|false
|
||||
function should_clean_disk(callback) {
|
||||
df({
|
||||
file: __dirname + "/../" + config.faces_dir,
|
||||
prefixMultiplier: "KiB",
|
||||
isDisplayPrefixMultiplier: false,
|
||||
precision: 2
|
||||
}, function (err, response) {
|
||||
if (err) {
|
||||
callback(err, false);
|
||||
} else {
|
||||
var available = response[0].available;
|
||||
logging.log("DiskCleaner: " + available + "KB available");
|
||||
callback(err, available < config.cleaning_disk_limit);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// check if redis limit reached, then flush redis
|
||||
// check if disk limit reached, then delete images
|
||||
exp.run = function() {
|
||||
should_clean_redis(function(err, clean) {
|
||||
if (err) {
|
||||
logging.error("Failed to run RedisCleaner");
|
||||
logging.error(err);
|
||||
} else if (clean) {
|
||||
logging.warn("RedisCleaner: Redis limit reached! flushing now");
|
||||
redis.flushall();
|
||||
} else {
|
||||
logging.log("RedisCleaner: Nothing to clean");
|
||||
}
|
||||
});
|
||||
|
||||
should_clean_disk(function(err, clean) {
|
||||
if (err) {
|
||||
logging.error("Failed to run DiskCleaner");
|
||||
logging.error(err);
|
||||
} else if (clean) {
|
||||
logging.warn("DiskCleaner: Disk limit reached! Cleaning images now");
|
||||
var facesdir = __dirname + "/../" + config.faces_dir;
|
||||
var helmdir = __dirname + "/../" + config.helms_dir;
|
||||
var renderdir = __dirname + "/../" + config.renders_dir;
|
||||
var skindir = __dirname + "/../" + config.skins_dir;
|
||||
fs.readdir(facesdir, function (err, files) {
|
||||
for (var i = 0, l = Math.min(files.length, config.cleaning_amount); i < l; i++) {
|
||||
var filename = files[i];
|
||||
if (filename[0] !== ".") {
|
||||
fs.unlink(facesdir + filename, nil);
|
||||
fs.unlink(helmdir + filename, nil);
|
||||
fs.unlink(skindir + filename, nil);
|
||||
}
|
||||
}
|
||||
});
|
||||
fs.readdir(renderdir, function (err, files) {
|
||||
for (var j = 0, l = Math.min(files.length, config.cleaning_amount); j < l; j++) {
|
||||
var filename = files[j];
|
||||
if (filename[0] !== ".") {
|
||||
fs.unlink(renderdir + filename, nil);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
logging.log("DiskCleaner: Nothing to clean");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
function nil () {}
|
||||
|
||||
module.exports = exp;
|
||||
25
lib/config.example.js
Normal file
25
lib/config.example.js
Normal file
@@ -0,0 +1,25 @@
|
||||
var config = {
|
||||
min_size: 1, // for avatars
|
||||
max_size: 512, // for avatars; too big values might lead to slow response time or DoS
|
||||
default_size: 160, // for avatars; size to be used when no size given
|
||||
min_scale: 1, // for 3D renders
|
||||
max_scale: 10, // for 3D renders; too big values might lead to slow response time or DoS
|
||||
default_scale: 6, // for 3D renders; scale to be used when no scale given
|
||||
local_cache_time: 1200, // seconds until we will check if the image changed. should be > 60 to prevent mojang 429 response
|
||||
browser_cache_time: 3600, // seconds until browser will request image again
|
||||
cleaning_interval: 1800, // seconds interval: deleting images if disk size at limit
|
||||
cleaning_disk_limit: 10240, // min allowed available KB on disk to trigger cleaning
|
||||
cleaning_redis_limit: 24576, // max allowed used KB on redis to trigger redis flush
|
||||
cleaning_amount: 50000, // amount of avatar (and their helm) files to clean
|
||||
http_timeout: 1000, // ms until connection to mojang is dropped
|
||||
debug_enabled: false, // enables logging.debug
|
||||
faces_dir: "images/faces/", // directory where faces are kept. should have trailing "/"
|
||||
helms_dir: "images/helms/", // directory where helms are kept. should have trailing "/"
|
||||
skins_dir: "images/skins/", // directory where skins are kept. should have trailing "/"
|
||||
renders_dir: "images/renders/", // Directory where rendered skins are kept. should have trailing "/"
|
||||
capes_dir: "images/capes/", // directory where capes are kept. should have trailing "/"
|
||||
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
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
365
lib/helpers.js
Normal file
365
lib/helpers.js
Normal file
@@ -0,0 +1,365 @@
|
||||
var networking = require("./networking");
|
||||
var logging = require("./logging");
|
||||
var config = require("./config");
|
||||
var cache = require("./cache");
|
||||
var skins = require("./skins");
|
||||
var renders = require("./renders");
|
||||
var fs = require("fs");
|
||||
|
||||
// 0098cb60-fa8e-427c-b299-793cbd302c9a
|
||||
var valid_user_id = /^([0-9a-f-A-F-]{32,36}|[a-zA-Z0-9_]{1,16})$/; // uuid|username
|
||||
var hash_pattern = /[0-9a-f]+$/;
|
||||
|
||||
// gets the hash from the textures.minecraft.net +url+
|
||||
function get_hash(url) {
|
||||
return hash_pattern.exec(url)[0].toLowerCase();
|
||||
}
|
||||
|
||||
function store_skin(rid, userId, profile, details, callback) {
|
||||
networking.get_skin_url(rid, userId, profile, function(err, url) {
|
||||
if (!err && url) {
|
||||
var skin_hash = get_hash(url);
|
||||
if (details && details.skin === skin_hash) {
|
||||
cache.update_timestamp(rid, userId, skin_hash, false, function(err) {
|
||||
callback(err, skin_hash);
|
||||
});
|
||||
} else {
|
||||
logging.log(rid + "new skin hash: " + skin_hash);
|
||||
var facepath = __dirname + "/../" + config.faces_dir + skin_hash + ".png";
|
||||
var helmpath = __dirname + "/../" + config.helms_dir + skin_hash + ".png";
|
||||
fs.exists(facepath, function(exists) {
|
||||
if (exists) {
|
||||
logging.log(rid + "skin already exists, not downloading");
|
||||
callback(null, skin_hash);
|
||||
} else {
|
||||
networking.get_from(rid, url, function(img, response, err1) {
|
||||
if (err1 || !img) {
|
||||
callback(err1, 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");
|
||||
skins.extract_helm(rid, facepath, img, helmpath, function(err3) {
|
||||
logging.debug(rid + "helm extracted");
|
||||
logging.debug(rid + helmpath);
|
||||
callback(err3, skin_hash);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
callback(err, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function store_cape(rid, userId, profile, details, callback) {
|
||||
networking.get_cape_url(rid, userId, profile, function(err, url) {
|
||||
if (!err && url) {
|
||||
var cape_hash = get_hash(url);
|
||||
if (details && details.cape === cape_hash) {
|
||||
cache.update_timestamp(rid, userId, cape_hash, false, function(err) {
|
||||
callback(err, cape_hash);
|
||||
});
|
||||
} else {
|
||||
logging.log(rid + "new cape hash: " + cape_hash);
|
||||
var capepath = __dirname + "/../" + config.capes_dir + cape_hash + ".png";
|
||||
fs.exists(capepath, function(exists) {
|
||||
if (exists) {
|
||||
logging.log(rid + "cape already exists, not downloading");
|
||||
callback(null, cape_hash);
|
||||
} else {
|
||||
networking.get_from(rid, url, function(img, response, err) {
|
||||
if (err || !img) {
|
||||
logging.error(rid + err.stack);
|
||||
callback(err, null);
|
||||
} else {
|
||||
skins.save_image(img, capepath, function(err) {
|
||||
logging.debug(rid + "cape saved");
|
||||
callback(err, cape_hash);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
callback(err, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// used by store_images to queue simultaneous requests for identical userId
|
||||
// the first request has to be completed until all others are continued
|
||||
var currently_running = [];
|
||||
// calls back all queued requests that match userId and type
|
||||
function callback_for(userId, type, err, hash) {
|
||||
var req_count = 0;
|
||||
for (var i = 0; i < currently_running.length; i++) {
|
||||
var current = currently_running[i];
|
||||
if (current.userid === userId && current.type === type) {
|
||||
req_count++;
|
||||
if (req_count !== 1) {
|
||||
// otherwise this would show up on single/first requests, too
|
||||
logging.debug(current.rid + "queued " + type + " request continued");
|
||||
}
|
||||
currently_running.splice(i, 1); // remove from array
|
||||
current.callback(err, hash);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
if (req_count > 1) {
|
||||
logging.debug(req_count + " simultaneous requests for " + userId);
|
||||
}
|
||||
}
|
||||
|
||||
// returns true if any object in +arr+ has +value+ as +property+
|
||||
function deep_property_check(arr, property, value) {
|
||||
for (var i = 0; i < arr.length; i++) {
|
||||
if (arr[i][property] === value) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// downloads the images for +userId+ while checking the cache
|
||||
// status based on +details+. +type+ specifies which
|
||||
// image type should be called back on
|
||||
// +callback+ contains error, image hash
|
||||
function store_images(rid, userId, details, type, callback) {
|
||||
var is_uuid = userId.length > 16;
|
||||
var new_hash = {
|
||||
rid: rid,
|
||||
userid: userId,
|
||||
type: type,
|
||||
callback: callback
|
||||
};
|
||||
if (!deep_property_check(currently_running, "userid", userId)) {
|
||||
currently_running.push(new_hash);
|
||||
networking.get_profile(rid, (is_uuid ? userId : null), function(err, profile) {
|
||||
if (err || (is_uuid && !profile)) {
|
||||
// error or uuid without profile
|
||||
if (!err && !profile) {
|
||||
// no error, but uuid without profile
|
||||
cache.save_hash(rid, userId, null, null, function(cache_err) {
|
||||
// we have no profile, so we have neither skin nor cape
|
||||
callback_for(userId, "skin", cache_err, null);
|
||||
callback_for(userId, "cape", cache_err, null);
|
||||
});
|
||||
} else {
|
||||
// an error occured, not caching. we can try in 60 seconds
|
||||
callback_for(userId, type, err, null);
|
||||
}
|
||||
} else {
|
||||
// no error and we have a profile (if it's a uuid)
|
||||
store_skin(rid, userId, profile, details, function(err, skin_hash) {
|
||||
if (err && !skin_hash) {
|
||||
// an error occured, not caching. we can try in 60 seconds
|
||||
callback_for(userId, "skin", err, null);
|
||||
} else {
|
||||
cache.save_hash(rid, userId, skin_hash, null, function(cache_err) {
|
||||
callback_for(userId, "skin", (err || cache_err), skin_hash);
|
||||
});
|
||||
}
|
||||
});
|
||||
store_cape(rid, userId, profile, details, function(err, cape_hash) {
|
||||
if (err && !cape_hash) {
|
||||
// an error occured, not caching. we can try in 60 seconds
|
||||
callback_for(userId, "cape", (err || cache_err), cape_hash);
|
||||
} else {
|
||||
cache.save_hash(rid, userId, undefined, cape_hash, function(cache_err) {
|
||||
callback_for(userId, "cape", (err || cache_err), cape_hash);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
logging.log(rid + "ID already being processed, adding to queue");
|
||||
currently_running.push(new_hash);
|
||||
}
|
||||
}
|
||||
|
||||
var exp = {};
|
||||
|
||||
// returns true if the +userId+ is a valid userId or username
|
||||
// the userId may be not exist, however
|
||||
exp.id_valid = function(userId) {
|
||||
return valid_user_id.test(userId);
|
||||
};
|
||||
|
||||
// decides whether to get a +type+ image for +userId+ from disk or to download it
|
||||
// callback contains error, status, hash
|
||||
// the status gives information about how the image was received
|
||||
// -1: "error"
|
||||
// 0: "none" - cached as null
|
||||
// 1: "cached" - found on disk
|
||||
// 2: "downloaded" - profile downloaded, skin downloaded from mojang servers
|
||||
// 3: "checked" - profile re-downloaded (was too old), but it has either not changed or has no skin
|
||||
exp.get_image_hash = function(rid, userId, type, callback) {
|
||||
cache.get_details(userId, function(err, details) {
|
||||
var cached_hash = (details !== null) ? (type === "skin" ? details.skin : details.cape) : null;
|
||||
if (err) {
|
||||
callback(err, -1, null);
|
||||
} else {
|
||||
if (details && details[type] !== undefined && details.time + config.local_cache_time * 1000 >= new Date().getTime()) {
|
||||
// use cached image
|
||||
logging.log(rid + "userId cached & recently updated");
|
||||
callback(null, (cached_hash ? 1 : 0), cached_hash);
|
||||
} else {
|
||||
// download image
|
||||
if (details) {
|
||||
logging.log(rid + "userId cached, but too old");
|
||||
} else {
|
||||
logging.log(rid + "userId not cached");
|
||||
}
|
||||
store_images(rid, userId, details, type, function(err, new_hash) {
|
||||
if (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) {
|
||||
callback(err2 || err, -1, details && cached_hash);
|
||||
});
|
||||
} else {
|
||||
var status = details && (cached_hash === new_hash) ? 3 : 2;
|
||||
logging.debug(rid + "cached hash: " + (details && cached_hash));
|
||||
logging.log(rid + "new hash: " + new_hash);
|
||||
callback(null, status, new_hash);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// handles requests for +userId+ avatars with +size+
|
||||
// callback contains error, status, image buffer, skin hash
|
||||
// image is the user's face+helm when helm is true, or the face otherwise
|
||||
// for status, see get_image_hash
|
||||
exp.get_avatar = function(rid, userId, helm, size, callback) {
|
||||
exp.get_image_hash(rid, userId, "skin", function(err, status, skin_hash) {
|
||||
if (skin_hash) {
|
||||
var facepath = __dirname + "/../" + config.faces_dir + skin_hash + ".png";
|
||||
var helmpath = __dirname + "/../" + config.helms_dir + skin_hash + ".png";
|
||||
var filepath = facepath;
|
||||
fs.exists(helmpath, function(exists) {
|
||||
if (helm && exists) {
|
||||
filepath = helmpath;
|
||||
}
|
||||
skins.resize_img(filepath, size, function(img_err, image) {
|
||||
if (img_err) {
|
||||
callback(img_err, -1, null, skin_hash);
|
||||
} else {
|
||||
callback(err, (err ? -1 : status), image, skin_hash);
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// hash is null when userId has no skin
|
||||
callback(err, status, null, null);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// handles requests for +userId+ skins
|
||||
// callback contains error, skin hash, image buffer
|
||||
exp.get_skin = function(rid, userId, callback) {
|
||||
exp.get_image_hash(rid, userId, "skin", function(err, status, skin_hash) {
|
||||
var skinpath = __dirname + "/../" + config.skins_dir + skin_hash + ".png";
|
||||
fs.exists(skinpath, function(exists) {
|
||||
if (exists) {
|
||||
logging.log(rid + "skin already exists, not downloading");
|
||||
skins.open_skin(rid, skinpath, function(err, img) {
|
||||
callback(err, skin_hash, img);
|
||||
});
|
||||
} else {
|
||||
networking.save_texture(rid, skin_hash, skinpath, function(err, response, img) {
|
||||
callback(err, skin_hash, img);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
function get_type(helm, body) {
|
||||
var text = body ? "body" : "head";
|
||||
return helm ? text + "helm" : text;
|
||||
}
|
||||
|
||||
// handles creations of 3D renders
|
||||
// callback contains error, skin hash, image buffer
|
||||
exp.get_render = function(rid, userId, scale, helm, body, callback) {
|
||||
exp.get_skin(rid, userId, function(err, skin_hash, img) {
|
||||
if (!skin_hash) {
|
||||
callback(err, -1, skin_hash, null);
|
||||
return;
|
||||
}
|
||||
var renderpath = __dirname + "/../" + config.renders_dir + skin_hash + "-" + scale + "-" + get_type(helm, body) + ".png";
|
||||
fs.exists(renderpath, function(exists) {
|
||||
if (exists) {
|
||||
renders.open_render(rid, renderpath, function(err, img) {
|
||||
callback(err, 1, skin_hash, img);
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
if (!img) {
|
||||
callback(err, 0, skin_hash, null);
|
||||
return;
|
||||
}
|
||||
renders.draw_model(rid, img, scale, helm, body, function(err, img) {
|
||||
if (err) {
|
||||
callback(err, -1, skin_hash, null);
|
||||
} else if (!img) {
|
||||
callback(null, 0, skin_hash, null);
|
||||
} else {
|
||||
fs.writeFile(renderpath, img, "binary", function(err) {
|
||||
if (err) {
|
||||
logging.error(rid + err.stack);
|
||||
}
|
||||
callback(null, 2, skin_hash, img);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// handles requests for +userId+ capes
|
||||
// callback contains error, cape hash, image buffer
|
||||
exp.get_cape = function(rid, userId, callback) {
|
||||
exp.get_image_hash(rid, userId, "cape", function(err, status, cape_hash) {
|
||||
if (!cape_hash) {
|
||||
callback(err, null, null);
|
||||
return;
|
||||
}
|
||||
var capepath = __dirname + "/../" + config.capes_dir + cape_hash + ".png";
|
||||
fs.exists(capepath, function(exists) {
|
||||
if (exists) {
|
||||
logging.log(rid + "cape already exists, not downloading");
|
||||
skins.open_skin(rid, capepath, function(err, img) {
|
||||
callback(err, cape_hash, img);
|
||||
});
|
||||
} else {
|
||||
networking.save_texture(rid, cape_hash, capepath, function(err, response, img) {
|
||||
if (response && response.statusCode === 404) {
|
||||
callback(err, cape_hash, null);
|
||||
} else {
|
||||
callback(err, cape_hash, img);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = exp;
|
||||
45
lib/logging.js
Normal file
45
lib/logging.js
Normal file
@@ -0,0 +1,45 @@
|
||||
var cluster = require("cluster");
|
||||
var config = require("./config");
|
||||
|
||||
var exp = {};
|
||||
|
||||
function split_args(args) {
|
||||
var text = "";
|
||||
for (var i = 0, l = args.length; i < l; i++) {
|
||||
if (i > 0) {
|
||||
text += " " + args[i];
|
||||
} else {
|
||||
text += args[i];
|
||||
}
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
function log(level, args, logger) {
|
||||
logger = logger || console.log;
|
||||
var time = config.log_time ? new Date().toISOString() + " " : "";
|
||||
var clid = (cluster.worker && cluster.worker.id || "M");
|
||||
var lines = split_args(args).split("\n");
|
||||
for (var i = 0, l = lines.length; i < l; i++) {
|
||||
logger(time + clid + " " + level + ": " + lines[i]);
|
||||
}
|
||||
}
|
||||
|
||||
exp.log = function() {
|
||||
log(" INFO", arguments);
|
||||
};
|
||||
exp.warn = function() {
|
||||
log(" WARN", arguments, console.warn);
|
||||
};
|
||||
exp.error = function() {
|
||||
log("ERROR", arguments, console.error);
|
||||
};
|
||||
if (config.debug_enabled || process.env.DEBUG === "true") {
|
||||
exp.debug = function() {
|
||||
log("DEBUG", arguments);
|
||||
};
|
||||
} else {
|
||||
exp.debug = function(){};
|
||||
}
|
||||
|
||||
module.exports = exp;
|
||||
179
lib/networking.js
Normal file
179
lib/networking.js
Normal file
@@ -0,0 +1,179 @@
|
||||
var http_code = require("http").STATUS_CODES;
|
||||
var logging = require("./logging");
|
||||
var request = require("request");
|
||||
var config = require("./config");
|
||||
var fs = require("fs");
|
||||
|
||||
var session_url = "https://sessionserver.mojang.com/session/minecraft/profile/";
|
||||
var skins_url = "https://skins.minecraft.net/MinecraftSkins/";
|
||||
var capes_url = "https://skins.minecraft.net/MinecraftCloaks/";
|
||||
var textures_url = "http://textures.minecraft.net/texture/";
|
||||
var mojang_urls = [skins_url, capes_url];
|
||||
|
||||
var exp = {};
|
||||
|
||||
function extract_url(profile, property) {
|
||||
var url = null;
|
||||
if (profile && profile.properties) {
|
||||
profile.properties.forEach(function(prop) {
|
||||
if (prop.name === "textures") {
|
||||
var json = new Buffer(prop.value, "base64").toString();
|
||||
var props = JSON.parse(json);
|
||||
url = props && props.textures && props.textures[property] && props.textures[property].url || null;
|
||||
}
|
||||
});
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
// exracts the skin url of a +profile+ object
|
||||
// returns null when no url found (user has no skin)
|
||||
exp.extract_skin_url = function(profile) {
|
||||
return extract_url(profile, 'SKIN');
|
||||
};
|
||||
|
||||
// exracts the cape url of a +profile+ object
|
||||
// returns null when no url found (user has no cape)
|
||||
exp.extract_cape_url = function(profile) {
|
||||
return extract_url(profile, 'CAPE');
|
||||
};
|
||||
|
||||
// makes a GET request to the +url+
|
||||
// +options+ hash includes these options:
|
||||
// encoding (string), default is to return a buffer
|
||||
// +callback+ contains the body, response,
|
||||
// and error buffer. get_from helper method is available
|
||||
exp.get_from_options = function(rid, url, options, callback) {
|
||||
request.get({
|
||||
url: url,
|
||||
headers: {
|
||||
"User-Agent": "https://crafatar.com"
|
||||
},
|
||||
timeout: config.http_timeout,
|
||||
followRedirect: false,
|
||||
encoding: (options.encoding || null),
|
||||
}, function(error, response, body) {
|
||||
// log url + code + description
|
||||
var code = response && response.statusCode;
|
||||
if (!error) {
|
||||
var logfunc = code && code < 405 ? logging.log : logging.warn;
|
||||
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) {
|
||||
logging.error(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 {
|
||||
logging.error(rid + " Unknown reply:");
|
||||
logging.error(rid + JSON.stringify(response));
|
||||
callback(body || null, response, error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// helper method for get_from_options, no options required
|
||||
exp.get_from = function(rid, url, callback) {
|
||||
exp.get_from_options(rid, url, {}, function(body, response, err) {
|
||||
callback(body, response, err);
|
||||
});
|
||||
};
|
||||
|
||||
// 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(rid, name, type, callback) {
|
||||
exp.get_from(rid, mojang_urls[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 === 0) {
|
||||
url = exp.extract_skin_url(profile);
|
||||
} else if (type === 1) {
|
||||
url = exp.extract_cape_url(profile);
|
||||
}
|
||||
callback(url || null);
|
||||
};
|
||||
|
||||
// make a request to sessionserver for +uuid+
|
||||
// +callback+ contains error, profile
|
||||
exp.get_profile = function(rid, uuid, callback) {
|
||||
if (!uuid) {
|
||||
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));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// get the skin URL for +userId+
|
||||
// +profile+ is used if +userId+ is a uuid
|
||||
exp.get_skin_url = function(rid, userId, profile, callback) {
|
||||
get_url(rid, userId, profile, 0, function(err, url) {
|
||||
callback(err, url);
|
||||
});
|
||||
};
|
||||
|
||||
// get the cape URL for +userId+
|
||||
// +profile+ is used if +userId+ is a uuid
|
||||
exp.get_cape_url = function(rid, userId, profile, callback) {
|
||||
get_url(rid, userId, profile, 1, function(err, url) {
|
||||
callback(err, url);
|
||||
});
|
||||
};
|
||||
|
||||
function get_url(rid, userId, profile, type, callback) {
|
||||
if (userId.length <= 16) {
|
||||
//username
|
||||
exp.get_username_url(rid, userId, type, function(err, url) {
|
||||
callback(err, url || null);
|
||||
});
|
||||
} else {
|
||||
exp.get_uuid_url(profile, type, function(url) {
|
||||
callback(null, url || null);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
exp.save_texture = function(rid, tex_hash, outpath, callback) {
|
||||
if (tex_hash) {
|
||||
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 {
|
||||
fs.writeFile(outpath, img, "binary", function(err) {
|
||||
if (err) {
|
||||
logging.error(rid + "error: " + err.stack);
|
||||
}
|
||||
callback(err, response, img);
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
callback(null, null, null);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = exp;
|
||||
198
lib/renders.js
Normal file
198
lib/renders.js
Normal file
@@ -0,0 +1,198 @@
|
||||
// Skin locations are based on the work of Confuser, with 1.8 updates by Jake0oo0
|
||||
// https://github.com/confuser/serverless-mc-skin-viewer
|
||||
// Permission to use & distribute https://github.com/confuser/serverless-mc-skin-viewer/blob/master/LICENSE
|
||||
|
||||
var logging = require("./logging");
|
||||
var fs = require("fs");
|
||||
var Canvas = require("canvas");
|
||||
var Image = Canvas.Image;
|
||||
var exp = {};
|
||||
|
||||
// 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) {
|
||||
//Helmet - Front
|
||||
model_ctx.setTransform(1,-0.5,0,1.2,0,0);
|
||||
model_ctx.drawImage(skin_canvas, 40*scale, 8*scale, 8*scale, 8*scale, 10*scale, 13/1.2*scale, 8*scale, 8*scale);
|
||||
//Helmet - Right
|
||||
model_ctx.setTransform(1,0.5,0,1.2,0,0);
|
||||
model_ctx.drawImage(skin_canvas, 32*scale, 8*scale, 8*scale, 8*scale, 2*scale, 3/1.2*scale, 8*scale, 8*scale);
|
||||
//Helmet - Top
|
||||
model_ctx.setTransform(-1,0.5,1,0.5,0,0);
|
||||
model_ctx.scale(-1,1);
|
||||
model_ctx.drawImage(skin_canvas, 40*scale, 0, 8*scale, 8*scale, -3*scale, 5*scale, 8*scale, 8*scale);
|
||||
};
|
||||
|
||||
// draws the head on to the +skin_canvas+
|
||||
// using the skin from the +model_ctx+ at the +scale+
|
||||
exp.draw_head = function(skin_canvas, model_ctx, scale) {
|
||||
//Head - Front
|
||||
model_ctx.setTransform(1,-0.5,0,1.2,0,0);
|
||||
model_ctx.drawImage(skin_canvas, 8*scale, 8*scale, 8*scale, 8*scale, 10*scale, 13/1.2*scale, 8*scale, 8*scale);
|
||||
//Head - Right
|
||||
model_ctx.setTransform(1,0.5,0,1.2,0,0);
|
||||
model_ctx.drawImage(skin_canvas, 0, 8*scale, 8*scale, 8*scale, 2*scale, 3/1.2*scale, 8*scale, 8*scale);
|
||||
//Head - Top
|
||||
model_ctx.setTransform(-1,0.5,1,0.5,0,0);
|
||||
model_ctx.scale(-1,1);
|
||||
model_ctx.drawImage(skin_canvas, 8*scale, 0, 8*scale, 8*scale, -3*scale, 5*scale, 8*scale, 8*scale);
|
||||
};
|
||||
|
||||
// draws the body on to the +skin_canvas+
|
||||
// using the skin from the +model_ctx+ at the +scale+
|
||||
// parts are labeled as if drawn from the skin's POV
|
||||
exp.draw_body = function(rid, skin_canvas, model_ctx, scale) {
|
||||
if (skin_canvas.height === 32 * scale) {
|
||||
logging.debug(rid + "uses old skin format");
|
||||
//Left Leg
|
||||
//Left Leg - Front
|
||||
model_ctx.setTransform(1,-0.5,0,1.2,0,0);
|
||||
model_ctx.scale(-1,1);
|
||||
model_ctx.drawImage(skin_canvas, 4*scale, 20*scale, 4*scale, 12*scale, -16*scale, 34.4/1.2*scale, 4*scale, 12*scale);
|
||||
|
||||
//Right Leg
|
||||
//Right Leg - Right
|
||||
model_ctx.setTransform(1,0.5,0,1.2,0,0);
|
||||
model_ctx.drawImage(skin_canvas, 0*scale, 20*scale, 4*scale, 12*scale, 4*scale, 26.4/1.2*scale, 4*scale, 12*scale);
|
||||
//Right Leg - Front
|
||||
model_ctx.setTransform(1,-0.5,0,1.2,0,0);
|
||||
model_ctx.drawImage(skin_canvas, 4*scale, 20*scale, 4*scale, 12*scale, 8*scale, 34.4/1.2*scale, 4*scale, 12*scale);
|
||||
|
||||
//Arm Left
|
||||
//Arm Left - Front
|
||||
model_ctx.setTransform(1,-0.5,0,1.2,0,0);
|
||||
model_ctx.scale(-1,1);
|
||||
model_ctx.drawImage(skin_canvas, 44*scale, 20*scale, 4*scale, 12*scale, -20*scale, 20/1.2*scale, 4*scale, 12*scale);
|
||||
//Arm Left - Top
|
||||
model_ctx.setTransform(-1,0.5,1,0.5,0,0);
|
||||
model_ctx.drawImage(skin_canvas, 44*scale, 16*scale, 4*scale, 4*scale, 0, 16*scale, 4*scale, 4*scale);
|
||||
|
||||
//Body
|
||||
//Body - Front
|
||||
model_ctx.setTransform(1,-0.5,0,1.2,0,0);
|
||||
model_ctx.drawImage(skin_canvas, 20*scale, 20*scale, 8*scale, 12*scale, 8*scale, 20/1.2*scale, 8*scale, 12*scale);
|
||||
|
||||
//Arm Right
|
||||
//Arm Right - Right
|
||||
model_ctx.setTransform(1,0.5,0,1.2,0,0);
|
||||
model_ctx.drawImage(skin_canvas, 40*scale, 20*scale, 4*scale, 12*scale, 0, 16/1.2*scale, 4*scale, 12*scale);
|
||||
//Arm Right - Front
|
||||
model_ctx.setTransform(1,-0.5,0,1.2,0,0);
|
||||
model_ctx.drawImage(skin_canvas, 44*scale, 20*scale, 4*scale, 12*scale, 4*scale, 20/1.2*scale, 4*scale, 12*scale);
|
||||
//Arm Right - Top
|
||||
model_ctx.setTransform(-1,0.5,1,0.5,0,0);
|
||||
model_ctx.scale(-1,1);
|
||||
model_ctx.drawImage(skin_canvas, 44*scale, 16*scale, 4*scale, 4*scale, -16*scale, 16*scale, 4*scale, 4*scale);
|
||||
} else {
|
||||
logging.debug(rid + "uses new skin format");
|
||||
//Left Leg
|
||||
//Left Leg - Front
|
||||
model_ctx.setTransform(1,-0.5,0,1.2,0,0);
|
||||
model_ctx.drawImage(skin_canvas, 20*scale, 52*scale, 4*scale, 12*scale, 12*scale, 34.4/1.2*scale, 4*scale, 12*scale);
|
||||
|
||||
//Right Leg
|
||||
//Right Leg - Right
|
||||
model_ctx.setTransform(1,0.5,0,1.2,0,0);
|
||||
model_ctx.drawImage(skin_canvas, 0, 20*scale, 4*scale, 12*scale, 4*scale, 26.4/1.2*scale, 4*scale, 12*scale);
|
||||
//Right Leg - Front
|
||||
model_ctx.setTransform(1,-0.5,0,1.2,0,0);
|
||||
model_ctx.drawImage(skin_canvas, 4*scale, 20*scale, 4*scale, 12*scale, 8*scale, 34.4/1.2*scale, 4*scale, 12*scale);
|
||||
|
||||
//Arm Left
|
||||
//Arm Left - Front
|
||||
model_ctx.setTransform(1,-0.5,0,1.2,0,0);
|
||||
model_ctx.drawImage(skin_canvas, 36*scale, 52*scale, 4*scale, 12*scale, 16*scale, 20/1.2*scale, 4*scale, 12*scale);
|
||||
//Arm Left - Top
|
||||
model_ctx.setTransform(-1,0.5,1,0.5,0,0);
|
||||
model_ctx.drawImage(skin_canvas, 36*scale, 48*scale, 4*scale, 4*scale, 0, 16*scale, 4*scale, 4*scale);
|
||||
|
||||
//Body
|
||||
//Body - Front
|
||||
model_ctx.setTransform(1,-0.5,0,1.2,0,0);
|
||||
model_ctx.drawImage(skin_canvas, 20*scale, 20*scale, 8*scale, 12*scale, 8*scale, 20/1.2*scale, 8*scale, 12*scale);
|
||||
|
||||
//Arm Right
|
||||
//Arm Right - Right
|
||||
model_ctx.setTransform(1,0.5,0,1.2,0,0);
|
||||
model_ctx.drawImage(skin_canvas, 40*scale, 20*scale, 4*scale, 12*scale, 0, 16/1.2*scale, 4*scale, 12*scale);
|
||||
//Arm Right - Front
|
||||
model_ctx.setTransform(1,-0.5,0,1.2,0,0);
|
||||
model_ctx.drawImage(skin_canvas, 44*scale, 20*scale, 4*scale, 12*scale, 4*scale, 20/1.2*scale, 4*scale, 12*scale);
|
||||
//Arm Right - Top
|
||||
model_ctx.setTransform(-1,0.5,1,0.5,0,0);
|
||||
model_ctx.scale(-1,1);
|
||||
model_ctx.drawImage(skin_canvas, 44*scale, 16*scale, 4*scale, 4*scale, -16*scale, 16*scale, 4*scale, 4*scale);
|
||||
}
|
||||
};
|
||||
|
||||
// sets up the necessary components to draw the skin model
|
||||
// uses the +img+ skin with options of drawing
|
||||
// the +helm+ and the +body+
|
||||
// callback contains error, image buffer
|
||||
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);
|
||||
};
|
||||
|
||||
image.onload = function() {
|
||||
var width = 64 * scale;
|
||||
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);
|
||||
var model_ctx = model_canvas.getContext("2d");
|
||||
var skin_ctx = skin_canvas.getContext("2d");
|
||||
|
||||
skin_ctx.drawImage(image,0,0,64,original_height);
|
||||
//Scale it
|
||||
scale_image(skin_ctx.getImageData(0,0,64,original_height), skin_ctx, 0, 0, scale);
|
||||
if (body) {
|
||||
exp.draw_body(rid, skin_canvas, model_ctx, scale);
|
||||
}
|
||||
exp.draw_head(skin_canvas, model_ctx, scale);
|
||||
if (helm) {
|
||||
exp.draw_helmet(skin_canvas, model_ctx, scale);
|
||||
}
|
||||
|
||||
model_canvas.toBuffer(function(err, buf){
|
||||
if (err) {
|
||||
logging.error(rid + "error creating buffer: " + err);
|
||||
}
|
||||
callback(err, buf);
|
||||
});
|
||||
};
|
||||
|
||||
image.src = img;
|
||||
};
|
||||
|
||||
// helper method to open a render from +renderpath+
|
||||
// callback contains 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);
|
||||
});
|
||||
};
|
||||
|
||||
// scales an image from the +imagedata+ onto the +context+
|
||||
// scaled by a factor of +scale+ with options +d_x+ and +d_y+
|
||||
function scale_image(imageData, context, d_x, d_y, scale) {
|
||||
var width = imageData.width;
|
||||
var height = imageData.height;
|
||||
context.clearRect(0,0,width,height); //Clear the spot where it originated from
|
||||
for(var y = 0; y < height; y++) { // height original
|
||||
for(var x = 0; x < width; x++) { // width original
|
||||
//Gets original colour, then makes a scaled square of the same colour
|
||||
var index = (x + y * width) * 4;
|
||||
context.fillStyle = "rgba(" + imageData.data[index+0] + "," + imageData.data[index+1] + "," + imageData.data[index+2] + "," + imageData.data[index+3] + ")";
|
||||
context.fillRect(d_x + x*scale, d_y + y*scale, scale, scale);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exp;
|
||||
137
lib/skins.js
Normal file
137
lib/skins.js
Normal file
@@ -0,0 +1,137 @@
|
||||
var logging = require("./logging");
|
||||
var lwip = require("lwip");
|
||||
var fs = require("fs");
|
||||
|
||||
var exp = {};
|
||||
|
||||
// extracts the face from an image +buffer+
|
||||
// result is saved to a file called +outname+
|
||||
// +callback+ contains error
|
||||
exp.extract_face = function(buffer, outname, callback) {
|
||||
lwip.open(buffer, "png", function(err, image) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
} else {
|
||||
image.batch()
|
||||
.crop(8, 8, 15, 15) // face
|
||||
.writeFile(outname, function(err) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
} else {
|
||||
callback(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// extracts the helm from an image +buffer+ and lays it over a +facefile+
|
||||
// +facefile+ is the filename of an image produced by extract_face
|
||||
// result is saved to a file called +outname+
|
||||
// +callback+ contains error
|
||||
exp.extract_helm = function(rid, facefile, buffer, outname, callback) {
|
||||
lwip.open(buffer, "png", function(err, skin_img) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
} else {
|
||||
lwip.open(facefile, function(err, face_img) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
} else {
|
||||
face_img.toBuffer("png", { compression: "none" }, function(err, face_buffer) {
|
||||
skin_img.crop(40, 8, 47, 15, function(err, helm_img) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
} else {
|
||||
face_img.paste(0, 0, helm_img, function(err, face_helm_img) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
} else {
|
||||
face_helm_img.toBuffer("png", {compression: "none"}, function(err, face_helm_buffer) {
|
||||
if (face_helm_buffer.toString() !== face_buffer.toString()) {
|
||||
face_helm_img.writeFile(outname, function(err) {
|
||||
callback(err);
|
||||
});
|
||||
} else {
|
||||
logging.log(rid + "helm img == face img, not storing!");
|
||||
callback(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// resizes the image file +inname+ to +size+ by +size+ pixels
|
||||
// +callback+ contains error, image buffer
|
||||
exp.resize_img = function(inname, size, callback) {
|
||||
lwip.open(inname, function(err, image) {
|
||||
if (err) {
|
||||
callback(err, null);
|
||||
} else {
|
||||
image.batch()
|
||||
.resize(size, size, "nearest-neighbor") // nearest-neighbor doesn't blur
|
||||
.toBuffer("png", function(err, buffer) {
|
||||
callback(null, buffer);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// returns "alex" or "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";
|
||||
} else {
|
||||
// great thanks to Minecrell for research into Minecraft and Java's UUID hashing!
|
||||
// https://git.io/xJpV
|
||||
// MC uses `uuid.hashCode() & 1` for alex
|
||||
// that can be compacted to counting the LSBs of every 4th byte in the UUID
|
||||
// an odd sum means alex, an even sum means steve
|
||||
// XOR-ing all the LSBs gives us 1 for alex and 0 for steve
|
||||
var lsbs_even = parseInt(uuid[07], 16) ^
|
||||
parseInt(uuid[15], 16) ^
|
||||
parseInt(uuid[23], 16) ^
|
||||
parseInt(uuid[31], 16);
|
||||
return lsbs_even ? "alex" : "steve";
|
||||
}
|
||||
};
|
||||
|
||||
// helper method for opening a skin file from +skinpath+
|
||||
// callback contains error, image buffer
|
||||
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);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
exp.save_image = function(buffer, outpath, callback) {
|
||||
lwip.open(buffer, "png", function(err, image) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
} else {
|
||||
image.batch()
|
||||
.writeFile(outpath, function(err) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
} else {
|
||||
callback(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = exp;
|
||||
Reference in New Issue
Block a user