├── .gitignore ├── LICENSE ├── README.md ├── dist ├── bundleV1.js └── bundleV2.js ├── package-lock.json ├── package.json ├── src ├── api.coffee ├── http.coffee ├── image.coffee ├── index.coffee ├── mojang.coffee └── util.coffee ├── webpack.config.js └── wrangler.toml /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | # Mac OS temp files 64 | .DS_Store 65 | 66 | # Serverless folder 67 | .serverless 68 | 69 | # wrangler files 70 | worker 71 | dist/worker.js 72 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Ashcon Partovi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mojang-api 2 | Javascript microservice that bundles multiple Mojang APIs into a single GET request. 3 | 4 | ### Purpose 5 | 6 | Mojang, the developers of [Minecraft](https://en.wikipedia.org/wiki/Minecraft), provides [multiple APIs](http://wiki.vg/Mojang_API) for websites and servers to fetch identity information about users. Requests do not accept authentication tokens, however they are heavily rate limited and fragmented among several endpoints. The purpose of this project is to package several of the most commonly used APIs into a single GET request with no rate limiting and no need for client-side caching. 7 | 8 | I have deployed this on my personal domain `ashcon.app` and am opening it up for the internet to use for free. It runs using [Cloudflare Workers](https://developers.cloudflare.com/workers/about/), which are Javascript functions that live in the closest datacenter to your request. The API is currently handling 1M+ requests per day with an average response time of 200ms and a < 0.0001% error rate. 9 | 10 | ### Single Request *(now)* 11 | 12 | Username or UUID -> Everything
13 | [https://api.ashcon.app/mojang/v2/user/[username|uuid]](https://api.ashcon.app/mojang/v2/user/Notch) `(click for example)` 14 | ``` 15 | { 16 | "uuid": , 17 | "username": , 18 | "username_history": [ 19 | { 20 | "username": , 21 | "changed_at": 22 | } 23 | ], 24 | "textures": { 25 | "slim": , 26 | "custom": , 27 | "skin": { 28 | "url": , 29 | "data": 30 | }, 31 | "cape": { 32 | "url": , 33 | "data": 34 | }, 35 | "raw": { 36 | "value": , 37 | "signature": 38 | } 39 | }, 40 | "legacy": , 41 | "demo": , 42 | "created_at": 43 | } 44 | ``` 45 | 46 | ### Multiple Requests *(before)* 47 | 48 | Username -> UUID
49 | [https://api.mojang.com/users/profiles/minecraft/[username]](https://api.mojang.com/users/profiles/minecraft/ElectroidFilms) 50 | ``` 51 | { 52 | "id": , 53 | "name": 54 | } 55 | ``` 56 | UUID -> Username History
57 | [https://api.mojang.com/user/profiles/[uuid]/names](https://api.mojang.com/user/profiles/dad8b95ccf6a44df982e8c8dd70201e0/names) 58 | ``` 59 | [ 60 | { 61 | "name": 62 | }, 63 | { 64 | "name": , 65 | "changedToAt": 66 | } 67 | ] 68 | ``` 69 | UUID -> Profile + Textures
70 | [https://sessionserver.mojang.com/session/minecraft/profile/[uuid]](https://sessionserver.mojang.com/session/minecraft/profile/dad8b95ccf6a44df982e8c8dd70201e0) 71 | ``` 72 | { 73 | "id": , 74 | "name": , 75 | "properties": [ 76 | { 77 | "name": "textures", 78 | "value": // Then decode the base64 string and make http requests to fetch the textures... 79 | } 80 | ] 81 | } 82 | ``` 83 | 84 | ### Build 85 | 86 | ``` 87 | wrangler build 88 | wrangler preview 89 | ``` 90 | -------------------------------------------------------------------------------- /dist/bundleV1.js: -------------------------------------------------------------------------------- 1 | !function(e){var t={};function n(r){if(t[r])return t[r].exports;var A=t[r]={i:r,l:!1,exports:{}};return e[r].call(A.exports,A,A.exports,n),A.l=!0,A.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var A in e)n.d(r,A,function(t){return e[t]}.bind(null,A));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=1)}([function(e,t){var n,r;String.prototype.toInt=function(e=0){var t;return(t=this.charCodeAt(e))>=97?t-87:t-48},Object.prototype.merge=function(e){return Object.assign({},this,e)},String.prototype.insert=function(e,t){return this.slice(0,e)+t+this.slice(e)},String.prototype.asUuid=function({dashed:e}={}){var t,n;if(t=r.exec(this))return n=t.slice(1).join(""),e?n.insert(8,"-").insert(13,"-").insert(18,"-").insert(23,"-"):n},r=/^([0-9a-f]{8})(?:-|)([0-9a-f]{4})(?:-|)(4[0-9a-f]{3})(?:-|)([0-9a-f]{4})(?:-|)([0-9a-f]{12})$/i,String.prototype.asUsername=function(){return!!n.test(this)&&this},n=/^[0-9A-Za-z_]{1,16}$/i,Number.prototype.asDate=function(){return new Date(Math.floor(this))},Object.prototype.isEmpty=function(){return 0===Object.keys(this).length},ArrayBuffer.prototype.asBase64=function(){var e,t,n,r,A,a,o,u;for(e="",a="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",u=(t=(r=new Uint8Array(this)).byteLength)-(n=t%3),o=0;o>18]+a[(258048&A)>>12]+a[(4032&A)>>6]+a[63&A],o+=3;return 1===n?e+=a[(252&(A=r[u]))>>2]+a[(3&A)<<4]+"==":2===n&&(e+=a[(64512&(A=r[u]<<8|r[u+1]))>>10]+a[(1008&A)>>4]+a[(15&A)<<2]+"="),e}},function(e,t,n){"use strict";n.r(t);n(0);var r,A=async function(e,{method:t,body:n,ttl:r,key:A,json:a,base64:o}={}){var u,i;return e?(null==t&&(t="GET"),null==r&&(r=60),null==A&&(A=e),i=fetch(e,{method:t,body:n,cf:{cacheTtl:r,polish:o?"lossless":void 0,cacheKey:A,cacheTtlByStatus:{"200-299":r,"300-399":-1,"400-404":1,"405-599":-1}},headers:{"User-Agent":"mojang-api (https://api.ashcon.app/mojang)","Content-Type":"application/json","Accept-Encoding":"gzip"}}),(a||o)&&(i=await i),a?(u=l(i.status))?[u,null]:[null,await i.json()]:o?i.ok?(await i.arrayBuffer()).asBase64():void 0:i):Promise.resolve(void 0)},a=function(e,t={}){return A(e,t.merge({method:"GET"}))},o=function(e,{code:t,type:n,json:r,text:A}={}){return null==t&&(t=200),r?(n="application/json",e=JSON.stringify(e,void 0,2)):A?(n="text/plain",e=String(e)):null==n&&(n="application/octet-stream"),new Response(e,{status:t,headers:{"Content-Type":n}})},u=function(e=null,{code:t,type:n}={}){return null==t&&(t=500),null==n&&(n="Internal Error"),o(`${t} - ${n}`+(e?` (${e})`:""),{code:t,text:!0})},i=function(e=null){return u(e,{code:400,type:"Bad Request"})},s=function(e=null){return u(e,{code:404,type:"Not Found"})},l=function(e){switch(e){case 200:return null;case 204:return s();case 400:return invalidRequest();case 429:return function(e=null){return u(e,{code:429,type:"Too Many Requests"})}();default:return u("Unknown Response",{code:e})}},c=async function(e){var t,n,r,A;return e.asUsername()?([t,A]=await function(e,t=-1){return a(`https://api.mojang.com/users/profiles/minecraft/${e}${t>=0?`?at=${t}`:""}`,{json:!0})}(e),(n=null!=A&&null!=(r=A.id)?r.asUuid({dashed:!0}):void 0)?[null,o(n,{text:!0})]:[t||s(),null]):[i(`malformed username '${e}'`),null]},f=async function(e){var t,n,A,u,s,l,p,d;return e.asUsername()?([t,p]=await c(e),t?[t,null]:await f(e=await p.text())):e.asUuid()?([[n,l],[A,s]]=await Promise.all([function(e){return a(`https://sessionserver.mojang.com/session/minecraft/profile/${e}`,{json:!0})}(e=e.asUuid()),function(e){return a(`https://api.mojang.com/user/profiles/${e}/names`,{json:!0})}(e)]),[u,d]=await r(l),(t=n||A||u)?[t,null]:[null,o({uuid:l.id.asUuid({dashed:!0}),username:l.name,username_history:s.map(function(e){var t;return{username:e.name,changed_at:null!=(t=e.changedToAt)?t.asDate():void 0}}),textures:d,cached_at:new Date},{json:!0})]):[i(`malformed uuid '${e}'`),null]};r=async function(e){var t,n,r,A,o,i,s,l,c,f,p;return e?(!(f=1===(r=e.properties).length?r[0]:r.filter(function(e){return"textures"===e.name&&null!=e.value})[0])||(f=JSON.parse(atob(f.value)).textures).isEmpty()?([p,l]=function(e){return(e.toInt(7)^e.toInt(15)^e.toInt(23)^e.toInt(31))%2!=0}(e.id)?["alex","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"]:["steve","iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAFDUlEQVR42u2a20sUURzH97G0LKMotPuWbVpslj1olJXdjCgyisowsSjzgrB0gSKyC5UF1ZNQWEEQSBQ9dHsIe+zJ/+nXfM/sb/rN4ZwZ96LOrnPgyxzP/M7Z+X7OZc96JpEISfWrFhK0YcU8knlozeJKunE4HahEqSc2nF6zSEkCgGCyb+82enyqybtCZQWAzdfVVFgBJJNJn1BWFgC49/VpwGVlD0CaxQiA5HSYEwBM5sMAdKTqygcAG9+8coHKY/XXAZhUNgDYuBSPjJL/GkzVVhAEU5tqK5XZ7cnFtHWtq/TahdSw2l0HUisr1UKIWJQBAMehDuqiDdzndsP2EZECAG1ZXaWMwOCODdXqysLf++uXUGv9MhUHIByDOijjdiSAoH3ErANQD73C7TXXuGOsFj1d4YH4OTJAEy8y9Hd0mCaeZ5z8dfp88zw1bVyiYhCLOg1ZeAqC0ybaDttHRGME1DhDeVWV26u17lRAPr2+mj7dvULfHw2q65fhQRrLXKDfIxkau3ZMCTGIRR3URR5toU38HbaPiMwUcKfBAkoun09PzrbQ2KWD1JJaqswjdeweoR93rirzyCMBCmIQizqoizZkm2H7iOgAcHrMHbbV9KijkUYv7qOn55sdc4fo250e+vUg4329/Xk6QB/6DtOws+dHDGJRB3XRBve+XARt+4hIrAF4UAzbnrY0ve07QW8uHfB+0LzqanMM7qVb+3f69LJrD90/1axiEIs6qIs21BTIToewfcSsA+Bfb2x67OoR1aPPzu2i60fSNHRwCw221Suz0O3jO+jh6V1KyCMGse9721XdN5ePutdsewxS30cwuMjtC860T5JUKpXyKbSByUn7psi5l+juDlZYGh9324GcPKbkycaN3jUSAGxb46IAYPNZzW0AzgiQ5tVnzLUpUDCAbakMQXXrOtX1UMtHn+Q9/X5L4wgl7t37r85OSrx+TYl379SCia9KXjxRpiTjIZTBFOvrV1f8ty2eY/T7XJ81FQAwmA8ASH1ob68r5PnBsxA88/xAMh6SpqW4HRnLBrkOA9Xv5wPAZjAUgOkB+SHxgBgR0qSMh0zmZRsmwDJm1gFg2PMDIC8/nAHIMls8x8GgzOsG5WiaqREgYzDvpTwjLDy8NM15LpexDEA3LepjU8Z64my+8PtDCmUyRr+fFwA2J0eAFYA0AxgSgMmYBMZTwFQnO9RNAEaHOj2DXF5UADmvAToA2ftyxZYA5BqgmZZApDkdAK4mAKo8GzPlr8G8AehzMAyA/i1girUA0HtYB2CaIkUBEHQ/cBHSvwF0AKZFS5M0ZwMQtEaEAmhtbSUoDADH9ff3++QZ4o0I957e+zYAMt6wHkhzpjkuAcgpwNcpA7AZDLsvpwiuOkBvxygA6Bsvb0HlaeKIF2EbADZpGiGzBsA0gnwQHGOhW2snRpbpPexbAB2Z1oicAMQpTnGKU5ziFKc4xSlOcYpTnOIUpzgVmgo+XC324WfJAdDO/+ceADkCpuMFiFKbApEHkOv7BfzfXt+5gpT8V7rpfYJcDz+jAsB233r6yyBsJ0mlBCDofuBJkel4vOwBFPv8fyYAFPJ+wbSf/88UANNRVy4Awo6+Ig2gkCmgA5DHWjoA+X7AlM//owLANkX0w0359od++pvX8fdMAcj3/QJ9iJsAFPQCxHSnQt8vMJ3v2wCYpkhkAOR7vG7q4aCXoMoSgG8hFAuc/grMdAD4B/kHl9da7Ne9AAAAAElFTkSuQmCC"],c=`http://assets.mojang.com/SkinTemplates/${p}.png`):[l,t]=await Promise.all([a(c=null!=(A=f.SKIN)?A.url:void 0,{base64:!0,ttl:86400}),a(n=null!=(o=f.CAPE)?o.url:void 0,{base64:!0,ttl:86400})]),l?[null,f={custom:null==p,slim:"slim"===(null!=(i=f.SKIN)&&null!=(s=i.metadata)?s.model:void 0)||"alex"===p,skin:{url:c,data:l},cape:n?{url:n,data:t}:void 0}]:[u(`unable to fetch skin '${c}'`),null]):[u("no user profile found"),null]};var p,d;addEventListener("fetch",function(e){return e.respondWith(p(e.request))}),p=function(e){var t,n,r,A,a;return[t,a,A,r,n]=e.url.split("/").slice(3,8),"mojang"===t&&null!=r?"v1"===a?d(A,r,n):s(`unknown api version '${a}'`):s("unknown route")},d=async function(e,t,n){var r,A;return"uuid"===e?[r,A]=await c(t):"user"===e?[r,A]=await f(t):"avatar"===e&&(A=function(e="Steve",t=8){return a(`https://us-central1-ashcon-app.cloudfunctions.net/avatar/${e}/${t}`)}(t,n)),r||A||s(`unknown v1 route '${e}'`)}}]); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mojang-api", 3 | "version": "2.2.1", 4 | "description": "Fast caching layer for the Mojang API.", 5 | "author": "Ashcon Partovi", 6 | "email": "ashcon@partovi.net", 7 | "keywords": [ 8 | "minecraft", 9 | "mojang", 10 | "api" 11 | ], 12 | "homepage": "https://github.com/Electroid/mojang-api", 13 | "repository": { 14 | "type": "git", 15 | "url": "github:Electroid/mojang-api" 16 | }, 17 | "license": "MIT", 18 | "main": "src/index.coffee", 19 | "scripts": { 20 | "build": "webpack", 21 | "build:watch": "webpack --watch", 22 | "dev": "wrangler dev", 23 | "deploy": "wrangler publish" 24 | }, 25 | "devDependencies": { 26 | "@cloudflare/workers-types": "^4.20221111.1", 27 | "coffee-loader": "^0.9.0", 28 | "coffeescript": "^2.5.1", 29 | "webpack": "^4.44.2", 30 | "webpack-cli": "^3.3.12", 31 | "wrangler": "^2.4.4" 32 | }, 33 | "dependencies": { 34 | "browserify-zlib": "^0.2.0", 35 | "pngjs": "^3.4.0", 36 | "xmlbuilder": "^13.0.2" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/api.coffee: -------------------------------------------------------------------------------- 1 | import { pngToSvg } from "./image" 2 | import { request, json, buffer, respond, error, badRequest, notFound } from "./http" 3 | import { usernameToUuid, uuidToProfile, uuidIsSlim, textureAlex, textureSteve, uuidSteve } from "./mojang" 4 | 5 | # Get the Uuid of a user given their name. 6 | # 7 | # @param {string} name - Minecraft username, must be alphanumeric 16 characters. 8 | # @returns {promise} - An error or a Uuid response as text. 9 | export uuid = (name) -> 10 | unless name.asUsername() 11 | return badRequest("Invalid format for the name '#{name}'") 12 | unless id = await NAMES.get(name.toLowerCase(), "text") 13 | [status, response] = await usernameToUuid(name) 14 | if status == 204 || status == 404 15 | return notFound("No user with the name '#{name}' was found") 16 | unless status == 200 17 | return error("Failed to find user with the name '#{name}'", {status: status}) 18 | id = response.id?.asUuid(dashed: true) 19 | await NAMES.put(name.toLowerCase(), id, {expirationTtl: 3600}) 20 | respond(id, text: true) 21 | 22 | # Get the profile of a user given their Uuid or name. 23 | # 24 | # @param {string} id - Uuid or Minecraft username. 25 | # @returns {promise} - An error or a profile response as Json. 26 | export user = (id) -> 27 | if id.asUsername() 28 | if (response = await uuid(id)).ok 29 | response = user(await response.text()) 30 | return response 31 | unless id.asUuid() 32 | return badRequest("Invalid format for the UUID '#{id}'") 33 | if response = await USERS.get(id.asUuid(dashed: true), "json") 34 | return respond(response, json: true) 35 | [status, profile] = await uuidToProfile(id = id.asUuid()) 36 | if status == 204 || status == 404 37 | return notFound("No user with the UUID '#{id}' was found") 38 | unless status == 200 39 | return error("Failed to find user with the UUID '#{id}'", {status: status}) 40 | history = [name: profile.name] 41 | texturesRaw = profile.properties?.filter((item) -> item.name == "textures")[0] || {} 42 | textures = JSON.parse(atob(texturesRaw?.value || btoa("{}"))).textures || {} 43 | unless textures.isEmpty() 44 | [skin, cape] = await Promise.all([ 45 | buffer(skinUrl) if skinUrl = textures.SKIN?.url, 46 | buffer(capeUrl) if capeUrl = textures.CAPE?.url]) 47 | unless skin 48 | [type, skin] = if uuidIsSlim(id) then ["alex", textureAlex] else ["steve", textureSteve] 49 | skinUrl = "http://assets.mojang.com/SkinTemplates/#{type}.png" 50 | if profile.legacy || profile.demo 51 | date = null 52 | else 53 | date = await created(id, profile.name) 54 | response = 55 | uuid: id = profile.id.asUuid(dashed: true) 56 | username: profile.name 57 | username_history: history.map((item) -> 58 | username: item.name 59 | changed_at: item.changedToAt?.asDate()) 60 | textures: 61 | custom: !type? 62 | slim: textures.SKIN?.metadata?.model == "slim" || type == "alex" 63 | skin: {url: skinUrl, data: skin} 64 | cape: {url: capeUrl, data: cape} if capeUrl, 65 | raw: {value: texturesRaw.value, signature: texturesRaw.signature} unless texturesRaw.isEmpty() 66 | legacy: true if profile.legacy 67 | demo: true if profile.demo 68 | created_at: date 69 | await USERS.put(id, JSON.stringify(response), {expirationTtl: 3600}) 70 | respond(response, json: true) 71 | 72 | # Approximate the date a user was created to within a day. 73 | # 74 | # This no longer works, since Mojang disabled the API. 75 | # 76 | # @param {string} id - Uuid of the user. 77 | export created = (id) -> 78 | unless date = await BIRTHDAYS.get(id, "text") 79 | await BIRTHDAYS.put(id, date = "null") 80 | return if date == "null" then null else date 81 | 82 | # Redirect to the avatar service to render the face of a user. 83 | # 84 | # @param {string} id - Uuid of the user. 85 | # @returns {promise} - Avatar response as a Svg. 86 | export avatar = (id) -> 87 | if !id.asUsername() && !id.asUuid() 88 | return avatar(uuidSteve) 89 | unless svg = await AVATARS.get(id.toLowerCase(), "text") 90 | try 91 | [status, profile] = await json(user(id)) 92 | svg = pngToSvg(profile.textures.skin.data, 93 | snap: true, 94 | view: {width: 8, height: 8}, 95 | regions: [ 96 | {x: 8, y: 8, width: 8, height: 8}, 97 | {x: 40, y: 8, width: 8, height: 8}]) 98 | if id != uuidSteve 99 | options = {expirationTtl: 86400} 100 | await AVATARS.put(id.toLowerCase(), svg, options) 101 | catch err 102 | if id == uuidSteve 103 | return error(err) 104 | else 105 | return avatar(uuidSteve) 106 | respond(svg, svg: true) 107 | -------------------------------------------------------------------------------- /src/http.coffee: -------------------------------------------------------------------------------- 1 | # Send a Http request and get a response. 2 | # 3 | # @param {string} url - Url of the request. 4 | # @param {string} method - Http method. 5 | # @param {string} type - Return type of the request. 6 | # @param {object} body - Body to be sent with the request. 7 | # @param {integer} ttl - Number of seconds to cache results. 8 | # @param {function} parser - Function to parse the raw response. 9 | # @returns {promise<[status, response]>} - Promise of the parsed response. 10 | export request = (url, {method, type, body, ttl, parser} = {}) -> 11 | method ?= "POST" if body 12 | method ?= "GET" 13 | if url instanceof Promise 14 | response = url 15 | else 16 | response = fetch(url, 17 | method: method 18 | body: body 19 | cf: 20 | mirage: true 21 | polish: "lossy" 22 | cacheEverything: true 23 | cacheTtl: ttl ?= 3600 24 | cacheTtlByStatus: 25 | "200-399": ttl 26 | "400-599": 60 27 | headers: 28 | "Accept": type 29 | "User-Agent": "mojang-api/2.2 (+https://api.ashcon.app/mojang/v2)") 30 | response = await response 31 | status = response.status 32 | if parser 33 | if response.ok && response.status < 204 34 | response = await parser(response) 35 | else 36 | response = null 37 | [status, response] 38 | 39 | # Send a Http request and get a Json response. 40 | # 41 | # @param {string} url - Url of the request. 42 | # @param {object} body - Json body to be sent with the request. 43 | # @param {integer} ttl - Number of seconds to cache results. 44 | # @returns {promise} - Promise of a Json response. 45 | export json = (url, {body, ttl} = {}) -> 46 | request(url, 47 | ttl: ttl, 48 | type: "application/json" 49 | body: JSON.stringify(body) if body 50 | parser: ((response) -> await response.json())) 51 | 52 | # Send a Http request and get a Buffer response. 53 | # 54 | # @param {string} url - Url of the request. 55 | # @param {object} body - Body to be sent with the request. 56 | # @param {integer} ttl - Number of seconds to cache results. 57 | # @param {boolean} base64 - Whether to encode the response as base64. 58 | # @returns {promise} - Promise of a Buffer response. 59 | export buffer = (url, {body, ttl, base64} = {}) -> 60 | base64 ?= true 61 | [status, data] = await request(url, 62 | ttl: ttl, 63 | body: body 64 | parser: ((response) -> 65 | response = await response.arrayBuffer() 66 | if base64 67 | response = response.asBase64() 68 | response)) 69 | return data 70 | 71 | # Respond to a client with a Http response. 72 | # 73 | # @param {object} data - Data to send back in the response. 74 | # @param {integer} status - Http status code. 75 | # @param {string} type - Http content type. 76 | # @param {object} headers - Http headers. 77 | # @param {boolean} json - Whether to respond in json. 78 | # @param {boolean} text - Whether to respond in plain text. 79 | # @param {boolean} svg - Whether to respond in image svg. 80 | # @returns {response} - Raw response object. 81 | export respond = (data, {status, type, headers, json, text, svg} = {}) -> 82 | status ?= 200 83 | if json 84 | type = "application/json" 85 | data = JSON.stringify(data, undefined, 2) 86 | else if text 87 | type = "text/plain" 88 | data = String(data) 89 | else if svg 90 | type = "image/svg+xml" 91 | data = String(data) 92 | else 93 | type ?= "application/octet-stream" 94 | headers ?= 95 | "Access-Control-Allow-Origin": "*" 96 | "Content-Type": type if status != 204 97 | new Response(data, status: status, headers: headers) 98 | 99 | # Respond with a Cors preflight. 100 | # 101 | # @see #respond(data) 102 | export cors = -> 103 | headers = 104 | "Access-Control-Allow-Origin": "*" 105 | "Access-Control-Allow-Methods": "GET, OPTIONS" 106 | "Access-Control-Max-Age": "86400" 107 | respond(null, status: 204, headers: headers) 108 | 109 | # Respond with a generic Http error. 110 | # 111 | # @see #respond(data) 112 | export error = (reason = null, {status, type} = {}) -> 113 | status ?= 500 114 | type ?= new Response(null, {status: status}).statusText; 115 | respond({code: status, error: type, reason: reason}, status: status, json: true) 116 | 117 | # Respond with a 400 - Bad Request error. 118 | # 119 | # @see #error(status, message, reason) 120 | export badRequest = (reason = null) -> 121 | error(reason, status: 400, type: "Bad Request") 122 | 123 | # Respond with a 404 - Not Found error. 124 | # 125 | # @see #error(status, message, reason) 126 | export notFound = (reason = null) -> 127 | error(reason, status: 404, type: "Not Found") 128 | 129 | # Respond with a 429 - Too Many Requests error. 130 | # 131 | # @see #error(status, message, reason) 132 | export tooManyRequests = (reason = null) -> 133 | error(reason, status: 429, type: "Too Many Requests") 134 | -------------------------------------------------------------------------------- /src/image.coffee: -------------------------------------------------------------------------------- 1 | import "browserify-zlib" 2 | import { PNG } from "pngjs" 3 | import XML from "xmlbuilder" 4 | 5 | # Convert a Png to a Svg representation. 6 | # 7 | # @param {string|buffer} data - Png buffer or base64 representation. 8 | # @param {array} regions - Select which regions from the Png to export. 9 | # @param {object} view - Width and height of the viewbox. 10 | # @param {boolean} snap - Whether to snap all regions to the origin (0, 0). 11 | # @returns {string} - A Xml representation of the Svg. 12 | export pngToSvg = (data, {regions, view, snap} = {}) -> 13 | if typeof data == "string" 14 | data = Buffer.from(data, "base64") 15 | if !view 16 | view = {width: img.width, height: img.height} 17 | if !regions || regions.length == 0 18 | regions = [{x: 0, y: 0, width: img.width, height: img.height}] 19 | img = PNG.sync.read(data) 20 | svg = XML.create("svg") 21 | .att("xmlns", "http://www.w3.org/2000/svg") 22 | .att("viewBox", "0 0 #{view.width} #{view.height}") 23 | .att("shape-rendering", "crispEdges") 24 | r = 0 25 | while r < regions.length 26 | region = regions[r] 27 | x = region.x 28 | while x < region.x + region.width 29 | y = region.y 30 | while y < region.y + region.height 31 | i = (img.width * y + x) << 2 32 | xAdj = x 33 | yAdj = y 34 | if snap 35 | xAdj -= region.x 36 | yAdj -= region.y 37 | svg.ele('rect', 38 | x: xAdj 39 | y: yAdj, 40 | width: 1, 41 | height: 1, 42 | fill: "rgba(#{img.data[i..i+3].join(",")})") 43 | y++ 44 | x++ 45 | r++ 46 | svg.end(pretty: true) 47 | -------------------------------------------------------------------------------- /src/index.coffee: -------------------------------------------------------------------------------- 1 | import "./util" 2 | import { error, notFound, cors } from "./http" 3 | import { uuid, user, avatar } from "./api" 4 | 5 | addEventListener("fetch", (event) -> 6 | event.respondWith(routeDebug(event.request))) 7 | 8 | routeDebug = (request) -> 9 | try 10 | await route(request) 11 | catch err 12 | error(err.stack || err) 13 | 14 | route = (request) -> 15 | [base, version, method, arg] = request.url.split("/")[3..6] 16 | if base == "mojang" && arg? 17 | if version == "v2" 18 | if request.method == "OPTIONS" 19 | cors() 20 | else 21 | v2(method, arg) 22 | else 23 | notFound("Unknown API version '#{version}'") 24 | else 25 | notFound("Unknown route") 26 | 27 | v2 = (method, arg) -> 28 | if method == "uuid" 29 | uuid(arg) 30 | else if method == "user" 31 | user(arg) 32 | else if method == "avatar" 33 | avatar(arg) 34 | else 35 | notFound("Unknown v2 route '#{method}'") 36 | -------------------------------------------------------------------------------- /src/mojang.coffee: -------------------------------------------------------------------------------- 1 | import { json } from "./http" 2 | 3 | # Check the health of various Mojang services. 4 | # 5 | # @example 6 | # [ 7 | # {"minecraft.net": "green"}, 8 | # {"api.mojang.com": "yellow"}, 9 | # {"textures.minecraft.net": "red"} 10 | # ] 11 | # 12 | # @throws {non-200} - When status servers are down, which should not happen. 13 | # @returns {promise} - An array of service statuses. 14 | export health = -> 15 | json("https://status.mojang.com/check") 16 | 17 | # Get the Uuid of a username at a given time. 18 | # 19 | # @example 20 | # { 21 | # "id": "dad8b95ccf6a44df982e8c8dd70201e0", 22 | # "name": "ElectroidFilms" 23 | # } 24 | # 25 | # @param {string} username - Minecraft username. 26 | # @throws {204} - When no user exists with that name. 27 | # @throws {400} - When timestamp is invalid. 28 | # @returns {promise} - Uuid response. 29 | export usernameToUuid = (username) -> 30 | json("https://api.mojang.com/users/profiles/minecraft/#{username}") 31 | 32 | # Get the Uuids of multiple usernames at the current time. 33 | # 34 | # @example 35 | # [ 36 | # { 37 | # "id": "dad8b95ccf6a44df982e8c8dd70201e0", 38 | # "name": "ElectroidFilms", 39 | # "legacy": false, 40 | # "demo": false 41 | # } 42 | # ] 43 | # 44 | # @param {array} usernames - Minecraft usernames, maximum of 100. 45 | # @throws {400} - When given an empty or null username. 46 | # @returns {promise} - Bulk Uuid response. 47 | export usernameToUuidBulk = (usernames...) -> 48 | json("https://api.mojang.com/profiles/minecraft", body: usernames) 49 | 50 | # Get the history of usernames for the Uuid. 51 | # 52 | # @example 53 | # [ 54 | # { 55 | # "name": "ElectroidFilms" 56 | # }, 57 | # { 58 | # "name": "Electric", 59 | # "changedToAt": 1423059891000 60 | # } 61 | # ] 62 | # 63 | # @param {string} id - Uuid to check the username history. 64 | # @returns {promise} - Username history response. 65 | export uuidToUsernameHistory = (id) -> 66 | json("https://api.mojang.com/user/profiles/#{id}/names") 67 | 68 | # Get the session profile of the Uuid. 69 | # 70 | # @example 71 | # { 72 | # "id": "dad8b95ccf6a44df982e8c8dd70201e0", 73 | # "name": "ElectroidFilms", 74 | # "properties": [ 75 | # {"name": "textures", "value": "...base64"} 76 | # ] 77 | # } 78 | # 79 | # @param {string} id - Uuid to get the session profile. 80 | # @returns {promise} - Uuid session profile. 81 | export uuidToProfile = (id) -> 82 | json("https://sessionserver.mojang.com/session/minecraft/profile/#{id}?unsigned=false") 83 | 84 | # Determine if a Uuid inherits a slim skin. 85 | # 86 | # @param {string} id - Uuid of the user. 87 | # @returns {boolean} - Whether the Uuid, by default, comes with a slim model. 88 | export uuidIsSlim = (id) -> 89 | # Take every fourth byte of the Uuid and determine 90 | # whether the sum is even (original) or odd (slim). 91 | sum = id.toInt(7) ^ id.toInt(15) ^ id.toInt(23) ^ id.toInt(31) 92 | sum % 2 != 0 93 | 94 | export uuidSteve = "8667ba71-b85a-4004-af54-457a9734eed7" 95 | export textureSteve = "iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAFDUlEQVR42u2a20sUURzH97G0LKMotPuWbVpslj1olJXdjCgyisowsSjzgrB0gSKyC5UF1ZNQWEEQSBQ9dHsIe+zJ/+nXfM/sb/rN4ZwZ96LOrnPgyxzP/M7Z+X7OZc96JpEISfWrFhK0YcU8knlozeJKunE4HahEqSc2nF6zSEkCgGCyb+82enyqybtCZQWAzdfVVFgBJJNJn1BWFgC49/VpwGVlD0CaxQiA5HSYEwBM5sMAdKTqygcAG9+8coHKY/XXAZhUNgDYuBSPjJL/GkzVVhAEU5tqK5XZ7cnFtHWtq/TahdSw2l0HUisr1UKIWJQBAMehDuqiDdzndsP2EZECAG1ZXaWMwOCODdXqysLf++uXUGv9MhUHIByDOijjdiSAoH3ErANQD73C7TXXuGOsFj1d4YH4OTJAEy8y9Hd0mCaeZ5z8dfp88zw1bVyiYhCLOg1ZeAqC0ybaDttHRGME1DhDeVWV26u17lRAPr2+mj7dvULfHw2q65fhQRrLXKDfIxkau3ZMCTGIRR3URR5toU38HbaPiMwUcKfBAkoun09PzrbQ2KWD1JJaqswjdeweoR93rirzyCMBCmIQizqoizZkm2H7iOgAcHrMHbbV9KijkUYv7qOn55sdc4fo250e+vUg4329/Xk6QB/6DtOws+dHDGJRB3XRBve+XARt+4hIrAF4UAzbnrY0ve07QW8uHfB+0LzqanMM7qVb+3f69LJrD90/1axiEIs6qIs21BTIToewfcSsA+Bfb2x67OoR1aPPzu2i60fSNHRwCw221Suz0O3jO+jh6V1KyCMGse9721XdN5ePutdsewxS30cwuMjtC860T5JUKpXyKbSByUn7psi5l+juDlZYGh9324GcPKbkycaN3jUSAGxb46IAYPNZzW0AzgiQ5tVnzLUpUDCAbakMQXXrOtX1UMtHn+Q9/X5L4wgl7t37r85OSrx+TYl379SCia9KXjxRpiTjIZTBFOvrV1f8ty2eY/T7XJ81FQAwmA8ASH1ob68r5PnBsxA88/xAMh6SpqW4HRnLBrkOA9Xv5wPAZjAUgOkB+SHxgBgR0qSMh0zmZRsmwDJm1gFg2PMDIC8/nAHIMls8x8GgzOsG5WiaqREgYzDvpTwjLDy8NM15LpexDEA3LepjU8Z64my+8PtDCmUyRr+fFwA2J0eAFYA0AxgSgMmYBMZTwFQnO9RNAEaHOj2DXF5UADmvAToA2ftyxZYA5BqgmZZApDkdAK4mAKo8GzPlr8G8AehzMAyA/i1girUA0HtYB2CaIkUBEHQ/cBHSvwF0AKZFS5M0ZwMQtEaEAmhtbSUoDADH9ff3++QZ4o0I957e+zYAMt6wHkhzpjkuAcgpwNcpA7AZDLsvpwiuOkBvxygA6Bsvb0HlaeKIF2EbADZpGiGzBsA0gnwQHGOhW2snRpbpPexbAB2Z1oicAMQpTnGKU5ziFKc4xSlOcYpTnOIUpzgVmgo+XC324WfJAdDO/+ceADkCpuMFiFKbApEHkOv7BfzfXt+5gpT8V7rpfYJcDz+jAsB233r6yyBsJ0mlBCDofuBJkel4vOwBFPv8fyYAFPJ+wbSf/88UANNRVy4Awo6+Ig2gkCmgA5DHWjoA+X7AlM//owLANkX0w0359od++pvX8fdMAcj3/QJ9iJsAFPQCxHSnQt8vMJ3v2wCYpkhkAOR7vG7q4aCXoMoSgG8hFAuc/grMdAD4B/kHl9da7Ne9AAAAAElFTkSuQmCC" 96 | 97 | export uuidAlex = "ec561538-f3fd-461d-aff5-086b22154bce" 98 | export textureAlex = "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" 99 | -------------------------------------------------------------------------------- /src/util.coffee: -------------------------------------------------------------------------------- 1 | # Convert base 16 representations into base 10, 2 | # much faster than #parseInt(hex, 16). 3 | # 4 | # @param {integer} i - Index of the string. 5 | # @returns {integer} - Base 10 representation of the string. 6 | String::toInt = (i = 0) -> 7 | c = this.charCodeAt(i) 8 | if c >= 97 # a-f 9 | c - 87 10 | else # 0-9 11 | c - 48 12 | 13 | # Merge two objects together into one. 14 | # 15 | # If a key exists in both objects, only the value 16 | # from the second object will exist since it is applied last. 17 | # 18 | # @param {object} other - Object to merge into the original. 19 | Object::merge = (other) -> 20 | Object.assign({}, this, other) 21 | 22 | # Insert a string at a given index. 23 | # 24 | # @param {integer} i - Index to insert the string at. 25 | # @param {string} str - String to insert. 26 | String::insert = (i, str) -> 27 | this.slice(0, i) + str + this.slice(i) 28 | 29 | # Ensure that the string is a valid Uuid. 30 | # 31 | # If dashed is enabled, it is possible the input 32 | # string is not the same as the output string. 33 | # 34 | # @param {boolean} dashed - Whether to return a dashed uuid. 35 | # @returns {string|null} - A uuid or null. 36 | String::asUuid = ({dashed} = {}) -> 37 | if match = uuidPattern.exec(this) 38 | uuid = match[1..].join("") 39 | if dashed 40 | uuid.insert(8, "-") 41 | .insert(12+1, "-") 42 | .insert(16+2, "-") 43 | .insert(20+3, "-") 44 | else 45 | uuid 46 | uuidPattern = /^([0-9a-f]{8})(?:-|)([0-9a-f]{4})(?:-|)(4[0-9a-f]{3})(?:-|)([0-9a-f]{4})(?:-|)([0-9a-f]{12})$/i 47 | 48 | # Ensure that the string is a valid Minecraft username. 49 | # 50 | # @returns {string|null} - Minecraft username or null. 51 | String::asUsername = -> 52 | if usernamePattern.test(this) then this else false 53 | usernamePattern = /^[0-9A-Za-z_]{1,16}$/i 54 | 55 | # Ensure that the unix number is a Date. 56 | # 57 | # @returns {date} - The number as a floored date. 58 | Number::asDate = -> 59 | new Date(Math.floor(this)) 60 | 61 | # Ensure that the unix number is a ISO date string. 62 | # 63 | # @returns {string} - The number as an ISO date string. 64 | Number::asDay = -> 65 | this.asDate().toISOString().split('T')[0] 66 | 67 | # Determine if the object is empty. 68 | # 69 | # @returns {boolean} - Whether the object is empty. 70 | Object::isEmpty = -> 71 | Object.keys(this).length == 0 72 | 73 | # Fast method of encoding an array buffer as a base64 string. 74 | # 75 | # @copyright https://gist.github.com/jonleighton/958841 76 | # @returns {string} - Array buffer as base64 string. 77 | ArrayBuffer::asBase64 = -> 78 | base64 = "" 79 | encodings = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" 80 | bytes = new Uint8Array(this) 81 | byteLength = bytes.byteLength 82 | byteRemainder = byteLength % 3 83 | mainLength = byteLength - byteRemainder 84 | i = 0 85 | # Main loop deals with bytes in chunks of 3 86 | while i < mainLength 87 | # Combine the three bytes into a single integer 88 | chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2] 89 | # Use bitmasks to extract 6-bit segments from the triplet 90 | a = (chunk & 16515072) >> 18 # 16515072 = (2^6 - 1) << 18 91 | b = (chunk & 258048) >> 12 # 258048 = (2^6 - 1) << 12 92 | c = (chunk & 4032) >> 6 # 4032 = (2^6 - 1) << 6 93 | d = chunk & 63 # 63 = 2^6 - 1 94 | # Convert the raw binary segments to the appropriate ASCII encoding 95 | base64 += encodings[a] + encodings[b] + encodings[c] + encodings[d] 96 | i = i + 3 97 | # Deal with the remaining bytes and padding 98 | if (byteRemainder == 1) 99 | chunk = bytes[mainLength] 100 | a = (chunk & 252) >> 2 # 252 = (2^6 - 1) << 2 101 | # Set the 4 least significant bits to zero 102 | b = (chunk & 3) << 4 # 3 = 2^2 - 1 103 | base64 += encodings[a] + encodings[b] + "==" 104 | else if (byteRemainder == 2) 105 | chunk = (bytes[mainLength] << 8) | bytes[mainLength + 1] 106 | a = (chunk & 64512) >> 10 # 64512 = (2^6 - 1) << 10 107 | b = (chunk & 1008) >> 4 # 1008 = (2^6 - 1) << 4 108 | # Set the 2 least significant bits to zero 109 | c = (chunk & 15) << 2 # 15 = 2^4 - 1 110 | base64 += encodings[a] + encodings[b] + encodings[c] + "=" 111 | base64 112 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | entry: { 5 | bundle: path.join(__dirname, './src/index.coffee'), 6 | }, 7 | output: { 8 | filename: 'worker.js', 9 | path: path.join(__dirname, 'dist'), 10 | }, 11 | mode: 'production', 12 | // devtool: 'cheap-module-source-map', 13 | watchOptions: { 14 | ignored: /node_modules|dist|\.js/g, 15 | }, 16 | resolve: { 17 | extensions: ['.coffee', '.js', '.json'], 18 | plugins: [], 19 | }, 20 | module: { 21 | rules: [ 22 | { 23 | test: /\.coffee?$/, 24 | loader: 'coffee-loader', 25 | } 26 | ] 27 | }, 28 | plugins: [ 29 | function() { // Certain build errors give no stack trace by default 30 | this.plugin("done", function(stats) { 31 | if (stats.compilation.errors && stats.compilation.errors.length) { 32 | console.error(stats.compilation.errors) 33 | process.exit(1) 34 | } 35 | }) 36 | } 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "mojang_api_v2" 2 | compatibility_date = "2017-01-01" 3 | account_id = "301982b3799377b8f6c0fbeeed3ee65f" 4 | zone_id = "1233d7f70968e8751cfd90318ae4c54c" 5 | routes = [ 6 | "api.ashcon.app/mojang/v2*" 7 | ] 8 | workers_dev = false 9 | kv_namespaces = [ 10 | { binding = "USERS", id = "ba64684b6f3c44778748d3e3010a81dd", preview_id = "ba64684b6f3c44778748d3e3010a81dd" }, 11 | { binding = "NAMES", id = "f4e19fdf2b2c43d4869071de5e1c59f8", preview_id = "f4e19fdf2b2c43d4869071de5e1c59f8" }, 12 | { binding = "BIRTHDAYS", id = "297276ad8bd3474b8344558da7f8e5f0", preview_id = "297276ad8bd3474b8344558da7f8e5f0" }, 13 | { binding = "AVATARS", id = "360dd7a808454c228457118f18ee4a88", preview_id = "360dd7a808454c228457118f18ee4a88" } 14 | ] 15 | main = "dist/worker.js" 16 | [build] 17 | command = "npm run build" --------------------------------------------------------------------------------