Merge branch 'master' of github.com:Jake0oo0/crafatar

This commit is contained in:
Jake 2014-11-05 21:32:33 -06:00
commit 55031236b8
15 changed files with 90 additions and 58 deletions

View File

@ -8,3 +8,4 @@ notifications:
skip_join: true skip_join: true
services: services:
- redis-server - redis-server
skip_join: true

View File

@ -1,8 +1,12 @@
# Crafatar # Crafatar [![travis](https://api.travis-ci.org/Jake0oo0/Spongy.svg)](https://travis-ci.org/Jake0oo0/Spongy/)
Crafatar serves Minecraft skins and heads for use in external applications. https://crafatar.com
Crafatar serves Minecraft avatars based on the skin for use in external applications.
Inspired by [Gravatar](https://gravatar.com) (hence the name) and [Minotar](https://minotar.net). Inspired by [Gravatar](https://gravatar.com) (hence the name) and [Minotar](https://minotar.net).
Image manipulation is done by [lwip](https://github.com/EyalAr/lwip)
## Usage ## Usage
See the [API Usage](https://crafatar.com) See the [API Usage](https://crafatar.com)

View File

@ -1,19 +1,20 @@
var config = require("./config"); var config = require("./config");
var redis = null; var redis = null;
var fs = require("fs");
function connect_redis() { function connect_redis() {
console.log("connecting to redis"); console.log("connecting to redis...");
if (process.env.REDISCLOUD_URL) { if (process.env.REDISCLOUD_URL) {
var redisURL = require("url").parse(process.env.REDISCLOUD_URL); var redisURL = require("url").parse(process.env.REDISCLOUD_URL);
redis = require("redis").createClient(redisURL.port, redisURL.hostname); redis = require("redis").createClient(redisURL.port, redisURL.hostname);
redis.auth(redisURL.auth.split(":")[1]); redis.auth(redisURL.auth.split(":")[1]);
redis.flushall();
} else { } else {
redis = require("redis").createClient(); redis = require("redis").createClient();
} }
redis.on("ready", function() { redis.on("ready", function() {
console.log("Redis connection established."); console.log("Redis connection established. Flushing all data.");
redis.flushall();
}); });
redis.on("error", function (err) { redis.on("error", function (err) {
console.error(err); console.error(err);
@ -23,17 +24,37 @@ function connect_redis() {
}); });
} }
// sets the date of the face file belonging to +hash+ to now
function update_file_date(hash) {
if (hash) {
var path = config.faces_dir + hash + ".png";
fs.exists(path, function(exists) {
if (exists) {
var date = new Date();
fs.utimes(path, date, date, function(err){
if (err) {
console.error(err);
}
});
} else {
console.error("Tried to update " + path + " date, but it doesn't exist");
}
});
}
}
var exp = {}; var exp = {};
exp.get_redis = function() { exp.get_redis = function() {
return redis; return redis;
}; };
// sets the timestamp for +uuid+ to now // sets the timestamp for +uuid+ and its face file's date to now
exp.update_timestamp = function(uuid) { exp.update_timestamp = function(uuid, hash) {
console.log(uuid + " cache: updating timestamp"); console.log(uuid + " cache: updating timestamp");
var time = new Date().getTime(); var time = new Date().getTime();
redis.hmset(uuid, "t", time); redis.hmset(uuid, "t", time);
update_file_date(hash);
}; };
// create the key +uuid+, store +hash+ and time // create the key +uuid+, store +hash+ and time
@ -52,7 +73,7 @@ exp.get_details = function(uuid, callback) {
if (data) { if (data) {
details = { details = {
hash: (data.h == "null" ? null : data.h), hash: (data.h == "null" ? null : data.h),
time: data.t time: Number(data.t)
}; };
} }
callback(err, details); callback(err, details);

View File

@ -1,8 +1,8 @@
var config = { var config = {
min_size: 0, // < 0 will (obviously) cause crash min_size: 1, // < 1 will (obviously) cause crash
max_size: 512, // too big values might lead to slow response time or DoS max_size: 512, // too big values might lead to slow response time or DoS
default_size: 180, // size to be used when no size given default_size: 160, // size to be used when no size given
local_cache_time: 3600, // seconds until we will check if the image changed local_cache_time: 3600, // seconds until we will check if the image changed. should be > 60 to prevent mojang 429 response
browser_cache_time: 3600, // seconds until browser will request image again browser_cache_time: 3600, // seconds until browser will request image again
http_timeout: 1000, // ms until connection to mojang is dropped http_timeout: 1000, // ms until connection to mojang is dropped
faces_dir: 'skins/faces/', // directory where faces are kept. should have trailing '/' faces_dir: 'skins/faces/', // directory where faces are kept. should have trailing '/'

View File

@ -30,7 +30,7 @@ function store_images(uuid, details, callback) {
if (details && details.hash == hash) { if (details && details.hash == hash) {
// hash hasn't changed // hash hasn't changed
console.log(uuid + " hash has not changed"); console.log(uuid + " hash has not changed");
cache.update_timestamp(uuid); cache.update_timestamp(uuid, hash);
callback(null, hash); callback(null, hash);
} else { } else {
// hash has changed // hash has changed
@ -83,7 +83,7 @@ function get_image_hash(uuid, callback) {
if (err) { if (err) {
callback(err, -1, null); callback(err, -1, null);
} else { } else {
if (details && details.time + config.local_cache_time >= new Date().getTime()) { if (details && details.time + config.local_cache_time * 1000 >= new Date().getTime()) {
// uuid known + recently updated // uuid known + recently updated
console.log(uuid + " uuid known & recently updated"); console.log(uuid + " uuid known & recently updated");
callback(null, (details.hash ? 1 : 0), details.hash); callback(null, (details.hash ? 1 : 0), details.hash);

View File

@ -78,4 +78,13 @@ exp.resize_img = function(inname, size, callback) {
}); });
}; };
// returns "alex" or "steve" calculated by the +uuid+
exp.default_skin = function(uuid) {
if (Number("0x" + uuid[31]) % 2 === 0) {
return "alex";
} else {
return "steve";
}
};
module.exports = exp; module.exports = exp;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 396 B

After

Width:  |  Height:  |  Size: 162 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 484 B

After

Width:  |  Height:  |  Size: 271 B

View File

@ -26,38 +26,39 @@ mark.green {
.code { .code {
font-family: monospace; font-family: monospace;
word-wrap: break-word;
} }
.sideface { .sideface {
width: 180px; width: 160px;
height: 180px; height: 160px;
} }
.sideface.Jake0oo0 { .sideface.Jake0oo0 {
background:url("/avatars/2d5aa9cdaeb049189930461fc9b91cc5?size=180&default=alex"); background:url("/avatars/2d5aa9cdaeb049189930461fc9b91cc5?size=160");
} }
.sideface.Jake0oo0:hover { .sideface.Jake0oo0:hover {
background:url("/avatars/2d5aa9cdaeb049189930461fc9b91cc5?size=180&default=alex&helm=true"); background:url("/avatars/2d5aa9cdaeb049189930461fc9b91cc5?size=160&helm=true");
} }
.sideface.redstone_sheep { .sideface.redstone_sheep {
background:url("/avatars/ae795aa86327408e92ab25c8a59f3ba1?size=180&default=alex"); background:url("/avatars/ae795aa86327408e92ab25c8a59f3ba1?size=160");
} }
.sideface.redstone_sheep:hover { .sideface.redstone_sheep:hover {
background:url("/avatars/ae795aa86327408e92ab25c8a59f3ba1?size=180&default=alex&helm=true"); background:url("/avatars/ae795aa86327408e92ab25c8a59f3ba1?size=160&helm=true");
} }
.sideface.Notch { .sideface.Notch {
background:url("/avatars/069a79f444e94726a5befca90e38aaf5?size=180&default=alex"); background:url("/avatars/069a79f444e94726a5befca90e38aaf5?size=160");
} }
.sideface.Notch:hover { .sideface.Notch:hover {
background:url("/avatars/069a79f444e94726a5befca90e38aaf5?size=180&default=alex&helm=true"); background:url("/avatars/069a79f444e94726a5befca90e38aaf5?size=160&helm=true");
} }
.sideface.sk89q { .sideface.sk89q {
background:url("/avatars/0ea8eca3dbf647cc9d1ac64551ca975c?size=180&default=alex"); background:url("/avatars/0ea8eca3dbf647cc9d1ac64551ca975c?size=160");
} }
.sideface.sk89q:hover { .sideface.sk89q:hover {
background:url("/avatars/0ea8eca3dbf647cc9d1ac64551ca975c?size=180&default=alex&helm=true"); background:url("/avatars/0ea8eca3dbf647cc9d1ac64551ca975c?size=160&helm=true");
} }
.sideface.md_5 { .sideface.md_5 {
background:url("/avatars/af74a02d19cb445bb07f6866a861f783?size=180&default=alex"); background:url("/avatars/af74a02d19cb445bb07f6866a861f783?size=160");
} }
.sideface.md_5:hover { .sideface.md_5:hover {
background:url("/avatars/af74a02d19cb445bb07f6866a861f783?size=180&default=alex&helm=true"); background:url("/avatars/af74a02d19cb445bb07f6866a861f783?size=160&helm=true");
} }

View File

@ -12,7 +12,7 @@ router.get('/:uuid.:ext?', function(req, res) {
var start = new Date(); var start = new Date();
// Prevent app from crashing/freezing // Prevent app from crashing/freezing
if (size <= config.min_size || size > config.max_size) { if (size < config.min_size || size > config.max_size) {
// "Unprocessable Entity", valid request, but semantically erroneous: // "Unprocessable Entity", valid request, but semantically erroneous:
// https://tools.ietf.org/html/rfc4918#page-78 // https://tools.ietf.org/html/rfc4918#page-78
res.status(422).send("422 Invalid size"); res.status(422).send("422 Invalid size");
@ -29,35 +29,34 @@ router.get('/:uuid.:ext?', function(req, res) {
console.error(err); console.error(err);
if (image) { if (image) {
console.warn("error occured, image found anyway"); console.warn("error occured, image found anyway");
sendimage(200, status, image); sendimage(503, true, image);
} else { } else {
handle_404(def); handle_default(404);
} }
} else if (status == 1 || status == 2) { } else if (status == 1 || status == 2) {
sendimage(200, status == 1, image); sendimage(200, status == 1, image);
} else if (status == 0 || status == 3) { } else if (status === 0 || status == 3) {
handle_404(def); handle_default(404);
} else { } else {
console.error("unexpected error/status"); console.error("unexpected error/status");
console.error("error: " + err); console.error("error: " + err);
console.error("status: " + status); console.error("status: " + status);
handle_404(def); handle_default(404);
} }
}); });
} catch(e) { } catch(e) {
console.error("Error!"); console.error("Error!");
console.error(e); console.error(e);
res.status(500).send("500 Internal server error"); handle_default(500);
} }
function handle_404(def) { function handle_default(status) {
if (def == "alex" || def == "steve") { if (def != "steve" && def != "alex") {
skins.resize_img("public/images/" + def + ".png", size, function(err, image) { def = skins.default_skin(uuid);
sendimage(404, true, image);
});
} else {
res.status(404).send('404 Not found');
} }
skins.resize_img("public/images/" + def + ".png", size, function(err, image) {
sendimage(status, true, image);
});
} }
function sendimage(status, local, image) { function sendimage(status, local, image) {

View File

@ -5,9 +5,7 @@ var router = express.Router();
router.get('/', function(req, res) { router.get('/', function(req, res) {
res.render('index', { res.render('index', {
title: 'Crafatar', title: 'Crafatar',
domain: "https://" + req.headers.host, domain: "https://" + req.headers.host
// see http://stackoverflow.com/a/14924922/2517068
commit: process.env.HEAD_HASH || "unknown"
}); });
}); });

View File

@ -1,4 +1,9 @@
#!/bin/bash #!/bin/bash
host="$1"
if [ -z "$host" ]; then
echo "Usage: $0 <host>"
exit 1
fi
dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
rm -f "$dir/../skins/"*.png || exit 1 rm -f "$dir/../skins/"*.png || exit 1
for uuid in `cat "$dir/uuids.txt"`; do for uuid in `cat "$dir/uuids.txt"`; do
@ -8,5 +13,5 @@ for uuid in `cat "$dir/uuids.txt"`; do
if [ "$(( ((RANDOM<<15)|RANDOM) % 2 ))" -eq "1" ]; then if [ "$(( ((RANDOM<<15)|RANDOM) % 2 ))" -eq "1" ]; then
helm="&helm" helm="&helm"
fi fi
curl -sS -o /dev/null -w "%{url_effective} %{http_code} %{time_total}s\\n" "http://crafatar.com/avatars/$uuid?size=$size$helm" || exit 1 curl -sSL -o /dev/null -w "%{url_effective} %{http_code} %{time_total}s\\n" "http://$host/avatars/$uuid?size=$size$helm" || exit 1
done done

View File

@ -27,13 +27,13 @@ describe('Avatar Serving', function(){
}); });
describe('Avatar', function(){ describe('Avatar', function(){
it("should be downloaded", function(done) { it("should be downloaded", function(done) {
helpers.get_avatar(uuid, false, 180, function(err, status, image) { helpers.get_avatar(uuid, false, 160, function(err, status, image) {
assert.equal(status, 2); assert.equal(status, 2);
done(); done();
}); });
}); });
it("should be local", function(done) { it("should be local", function(done) {
helpers.get_avatar(uuid, false, 180, function(err, status, image) { helpers.get_avatar(uuid, false, 160, function(err, status, image) {
assert.equal(status, 1); assert.equal(status, 1);
done(); done();
}); });
@ -44,7 +44,7 @@ describe('Avatar Serving', function(){
cache.get_redis().flushall(); cache.get_redis().flushall();
}); });
it("should be rate limited", function(done) { it("should be rate limited", function(done) {
helpers.get_avatar(uuid, false, 180, function(err, status, image) { helpers.get_avatar(uuid, false, 160, function(err, status, image) {
assert.equal(err, null); assert.equal(err, null);
done(); done();
}); });

View File

@ -23,13 +23,13 @@ block content
h3 Parameters h3 Parameters
h4 size h4 size
p The size of the image in pixels, 1 - 512. <br> Default is 180. p The size of the image in pixels, 1 - 512. <br> Default is 160.
h4 default h4 default
p The image to be returned when the uuid has no skin. <br> Valid options are p The image to be returned when the uuid has no skin. <br> Valid options are
a(href="/avatars/00000000000000000000000000000000?default=steve") steve a(href="/avatars/00000000000000000000000000000000?default=steve") steve
| or | or
a(href="/avatars/00000000000000000000000000000000?default=alex") alex a(href="/avatars/00000000000000000000000000000000?default=alex") alex
| .<br> Otherwise, a 404 with no content is returned. | .<br> The default is calculated based on the UUID (even = alex, odd = steve)
h4 helm h4 helm
p Get an avatar with the second (helmet) layer applied. <br> The content of this parameter is ignored p Get an avatar with the second (helmet) layer applied. <br> The content of this parameter is ignored
@ -41,7 +41,7 @@ block content
p Either 'local' or 'downloaded'. Local means that Crafatar already had the image on disk, while downloaded means that it was retrieved from Mojang's skin servers. p Either 'local' or 'downloaded'. Local means that Crafatar already had the image on disk, while downloaded means that it was retrieved from Mojang's skin servers.
h3 Examples h3 Examples
p Get jeb_'s avatar, 180 × 180 pixels p Get jeb_'s avatar, 160 × 160 pixels
img(src="/avatars/853c80ef3c3749fdaa49938b674adae6") img(src="/avatars/853c80ef3c3749fdaa49938b674adae6")
.well.code &lt;img src="#{domain}/avatars/853c80ef3c3749fdaa49938b674adae6"&gt; .well.code &lt;img src="#{domain}/avatars/853c80ef3c3749fdaa49938b674adae6"&gt;
p Get jeb_'s avatar, 64 × 64 pixels p Get jeb_'s avatar, 64 × 64 pixels
@ -59,6 +59,3 @@ block content
.sideface.Notch(title="Notch") .sideface.Notch(title="Notch")
.sideface.sk89q(title="sk89q") .sideface.sk89q(title="sk89q")
.sideface.md_5(title="md_5") .sideface.md_5(title="md_5")
hr
small Site version
a(href="https://github.com/Jake0oo0/crafatar/commit/#{commit}") #{commit}

View File

@ -5,6 +5,7 @@ html
link(rel='stylesheet', href='/stylesheets/style.css') link(rel='stylesheet', href='/stylesheets/style.css')
link(rel="icon", type="image/x-icon", href="/favicon.ico") link(rel="icon", type="image/x-icon", href="/favicon.ico")
link(href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css", rel="stylesheet") link(href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css", rel="stylesheet")
meta(name="viewport" content="initial-scale=1,maximum-scale=1")
script(src="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js") script(src="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js")
body body
a.forkme(href="https://github.com/Jake0oo0/crafatar", target="_blank") a.forkme(href="https://github.com/Jake0oo0/crafatar", target="_blank")
@ -17,8 +18,4 @@ html
span.icon-bar span.icon-bar
span.icon-bar span.icon-bar
a.navbar-brand(href='/') Crafatar a.navbar-brand(href='/') Crafatar
.navbar-collapse.collapse
ul.nav.navbar-nav
li.active
a(href='/') Home
block content block content