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
@ -8,3 +8,4 @@ notifications:
|
|||||||
skip_join: true
|
skip_join: true
|
||||||
services:
|
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).
|
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)
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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 '/'
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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 |
@ -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");
|
||||||
}
|
}
|
||||||
@ -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) {
|
||||||
|
|||||||
@ -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"
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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();
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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 <img src="#{domain}/avatars/853c80ef3c3749fdaa49938b674adae6">
|
.well.code <img src="#{domain}/avatars/853c80ef3c3749fdaa49938b674adae6">
|
||||||
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}
|
|
||||||
@ -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
|
||||||
Loading…
x
Reference in New Issue
Block a user