Browse Source

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

Jake 10 years ago
parent
commit
55031236b8

+ 2 - 1
.travis.yml

@@ -7,4 +7,5 @@ notifications:
       - "irc.esper.net#spongy"
     skip_join: true
 services:
-  - redis-server
+  - redis-server
+skip_join: true

+ 6 - 2
README.md

@@ -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)

+ 27 - 6
modules/cache.js

@@ -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);

+ 3 - 3
modules/config.js

@@ -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 '/'

+ 2 - 2
modules/helpers.js

@@ -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);

+ 9 - 0
modules/skins.js

@@ -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;

BIN
public/images/alex.png


BIN
public/images/steve.png


+ 13 - 12
public/stylesheets/style.css

@@ -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");
 }

+ 13 - 14
routes/avatars.js

@@ -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) {

+ 1 - 3
routes/index.js

@@ -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
   });
 });
 

+ 6 - 1
test/bulk.sh

@@ -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

+ 3 - 3
test/test.js

@@ -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();
       });

+ 4 - 7
views/index.jade

@@ -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")

+ 1 - 4
views/layout.jade

@@ -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