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

@ -7,4 +7,5 @@ notifications:
- "irc.esper.net#spongy"
skip_join: true
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).
Image manipulation is done by [lwip](https://github.com/EyalAr/lwip)
## Usage
See the [API Usage](https://crafatar.com)

View File

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

View File

@ -1,8 +1,8 @@
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
default_size: 180, // size to be used when no size given
local_cache_time: 3600, // seconds until we will check if the image changed
default_size: 160, // size to be used when no size given
local_cache_time: 3600, // seconds until we will check if the image changed. should be > 60 to prevent mojang 429 response
browser_cache_time: 3600, // seconds until browser will request image again
http_timeout: 1000, // ms until connection to mojang is dropped
faces_dir: 'skins/faces/', // directory where faces are kept. should have trailing '/'

View File

@ -30,7 +30,7 @@ function store_images(uuid, details, callback) {
if (details && details.hash == hash) {
// hash hasn't changed
console.log(uuid + " hash has not changed");
cache.update_timestamp(uuid);
cache.update_timestamp(uuid, hash);
callback(null, hash);
} else {
// hash has changed
@ -83,7 +83,7 @@ function get_image_hash(uuid, callback) {
if (err) {
callback(err, -1, null);
} 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
console.log(uuid + " uuid known & recently updated");
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;

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 {
font-family: monospace;
word-wrap: break-word;
}
.sideface {
width: 180px;
height: 180px;
width: 160px;
height: 160px;
}
.sideface.Jake0oo0 {
background:url("/avatars/2d5aa9cdaeb049189930461fc9b91cc5?size=180&default=alex");
background:url("/avatars/2d5aa9cdaeb049189930461fc9b91cc5?size=160");
}
.sideface.Jake0oo0:hover {
background:url("/avatars/2d5aa9cdaeb049189930461fc9b91cc5?size=180&default=alex&helm=true");
background:url("/avatars/2d5aa9cdaeb049189930461fc9b91cc5?size=160&helm=true");
}
.sideface.redstone_sheep {
background:url("/avatars/ae795aa86327408e92ab25c8a59f3ba1?size=180&default=alex");
background:url("/avatars/ae795aa86327408e92ab25c8a59f3ba1?size=160");
}
.sideface.redstone_sheep:hover {
background:url("/avatars/ae795aa86327408e92ab25c8a59f3ba1?size=180&default=alex&helm=true");
background:url("/avatars/ae795aa86327408e92ab25c8a59f3ba1?size=160&helm=true");
}
.sideface.Notch {
background:url("/avatars/069a79f444e94726a5befca90e38aaf5?size=180&default=alex");
background:url("/avatars/069a79f444e94726a5befca90e38aaf5?size=160");
}
.sideface.Notch:hover {
background:url("/avatars/069a79f444e94726a5befca90e38aaf5?size=180&default=alex&helm=true");
background:url("/avatars/069a79f444e94726a5befca90e38aaf5?size=160&helm=true");
}
.sideface.sk89q {
background:url("/avatars/0ea8eca3dbf647cc9d1ac64551ca975c?size=180&default=alex");
background:url("/avatars/0ea8eca3dbf647cc9d1ac64551ca975c?size=160");
}
.sideface.sk89q:hover {
background:url("/avatars/0ea8eca3dbf647cc9d1ac64551ca975c?size=180&default=alex&helm=true");
background:url("/avatars/0ea8eca3dbf647cc9d1ac64551ca975c?size=160&helm=true");
}
.sideface.md_5 {
background:url("/avatars/af74a02d19cb445bb07f6866a861f783?size=180&default=alex");
background:url("/avatars/af74a02d19cb445bb07f6866a861f783?size=160");
}
.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();
// 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:
// https://tools.ietf.org/html/rfc4918#page-78
res.status(422).send("422 Invalid size");
@ -29,35 +29,34 @@ router.get('/:uuid.:ext?', function(req, res) {
console.error(err);
if (image) {
console.warn("error occured, image found anyway");
sendimage(200, status, image);
sendimage(503, true, image);
} else {
handle_404(def);
handle_default(404);
}
} else if (status == 1 || status == 2) {
sendimage(200, status == 1, image);
} else if (status == 0 || status == 3) {
handle_404(def);
} else if (status === 0 || status == 3) {
handle_default(404);
} else {
console.error("unexpected error/status");
console.error("error: " + err);
console.error("status: " + status);
handle_404(def);
handle_default(404);
}
});
} catch(e) {
console.error("Error!");
console.error(e);
res.status(500).send("500 Internal server error");
handle_default(500);
}
function handle_404(def) {
if (def == "alex" || def == "steve") {
skins.resize_img("public/images/" + def + ".png", size, function(err, image) {
sendimage(404, true, image);
});
} else {
res.status(404).send('404 Not found');
function handle_default(status) {
if (def != "steve" && def != "alex") {
def = skins.default_skin(uuid);
}
skins.resize_img("public/images/" + def + ".png", size, function(err, image) {
sendimage(status, true, image);
});
}
function sendimage(status, local, image) {

View File

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

View File

@ -1,4 +1,9 @@
#!/bin/bash
host="$1"
if [ -z "$host" ]; then
echo "Usage: $0 <host>"
exit 1
fi
dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
rm -f "$dir/../skins/"*.png || exit 1
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
helm="&helm"
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

View File

@ -27,13 +27,13 @@ describe('Avatar Serving', function(){
});
describe('Avatar', function(){
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);
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);
done();
});
@ -44,7 +44,7 @@ describe('Avatar Serving', function(){
cache.get_redis().flushall();
});
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);
done();
});

View File

@ -23,13 +23,13 @@ block content
h3 Parameters
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
p The image to be returned when the uuid has no skin. <br> Valid options are
a(href="/avatars/00000000000000000000000000000000?default=steve") steve
| or
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
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.
h3 Examples
p Get jeb_'s avatar, 180 × 180 pixels
p Get jeb_'s avatar, 160 × 160 pixels
img(src="/avatars/853c80ef3c3749fdaa49938b674adae6")
.well.code &lt;img src="#{domain}/avatars/853c80ef3c3749fdaa49938b674adae6"&gt;
p Get jeb_'s avatar, 64 × 64 pixels
@ -58,7 +58,4 @@ block content
.sideface.Jake0oo0(title="Jake0oo0")
.sideface.Notch(title="Notch")
.sideface.sk89q(title="sk89q")
.sideface.md_5(title="md_5")
hr
small Site version
a(href="https://github.com/Jake0oo0/crafatar/commit/#{commit}") #{commit}
.sideface.md_5(title="md_5")

View File

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