mirror of
https://github.com/azures04/crafatar.git
synced 2026-03-21 23:41:18 +01:00
Merge branch 'master' of github.com:Jake0oo0/crafatar
This commit is contained in:
commit
55031236b8
@ -7,4 +7,5 @@ notifications:
|
||||
- "irc.esper.net#spongy"
|
||||
skip_join: true
|
||||
services:
|
||||
- redis-server
|
||||
- redis-server
|
||||
skip_join: true
|
||||
|
||||
@ -1,8 +1,12 @@
|
||||
# Crafatar
|
||||
# Crafatar [](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)
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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 '/'
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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 |
@ -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");
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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();
|
||||
});
|
||||
|
||||
@ -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 <img src="#{domain}/avatars/853c80ef3c3749fdaa49938b674adae6">
|
||||
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")
|
||||
@ -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
|
||||
Loading…
x
Reference in New Issue
Block a user