├── .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","iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAAZiS0dEAIwAuACKS3UjegAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB94IFA0kCPwApjEAAAAWaVRYdENvbW1lbnQAAAAAAG1vZGVsPXNsaW1TpLy5AAAMuklEQVR42uVbe5AUxR3+Zue1M/u+A08UjjrRAoPBRwoKjQ+IIhW1TEEpFSsVUEvE9wO1LkooH1SBSSGVkHCGQDRSlqYwFf4IVxUfVRiIRpPyBRFIYgROgTvw2Nftzs2jd/NHT/fM7O49dy8C6aqtnpme6e7v+z26+9e9AoZIhzfOLwMA+vqBaBiB675+5PP5Qb+f9tSHAupIBzbeQC/yJhBTg9d5E7kh2r/w6fcGLZeG7IEfuJsMhUADICZjgNuB3doFgXdmGP9AQ5IfOG/fgQYVYsprf69+fuCdbxT3Dav60JBvRMMwrAIFywhxc+LYHHwulwv8KgkZdYqpMMwCBcsIcXPiWBx8LpsN/CoJGT0BDLCPEE2JUElYBYx5YoB9hGiq275Zf/uh4bykWWKV9JlGqNc+POYcaJZUJX2mEdFr7xtjAlz7Z+puWAX+LKonx14DXPtn6m6YBf4sGknVXf2QTtCwCtAgcmeouc9JJu/5BQDxeHxM8BtmARok7gw1uISk855fABBPJMaGgKieBFFsqvaSHADfV8xABDDzinkDfD0PeOqHdREQjaRAVIuqvaQEwPcV0lABzLr86gG+vhoYYhgUDm+cX2Y2TRybg2RA2XjPc2YWA8wLWM7qq1WGaBiiJINk8rCiMYHZNHEsDpIBZeM9z5lZDDAvYDmrr1YZYipESQFJ5yGxDrHOkihqA3VzahLBMm4KSZmTxnJWxurl94xct0OssySG2kDdnJqEGgTDTCGlcNJYzspYvfzeLZeqOuSXkGNXgU/MaQcAWO/+EgC4JFniknfL/MNpLS2r7FBAQo5VBb55zqMAgOJfn3fbcEH76mMO069NA2mZRBybSjUa4QD6ihnAAjQlQr2+AsAFr6oqctk0cMntED58gZNkWAVoSsRzjkmZa5XfWVaSTaIylWqMtt/26JuBaXDbsu38vmd3J1RVRXLqNcF3/N+snUdJTSlcq/zOspLsEOs4F1Qxg8ScdiTmtMOwCgjPvh+T79qB+R91Q1VVQAzjqudXoOWihShfcjslqK/fq8NVf7/9c7Ng0nd9h5iMUfBqJNDBnt2d6NndibZl2/HIdRNh2zZmrpiFlhnXIzn1GsxcMQu2beOR6yaibdl2HFg7z1szuOrvt39uFkz6ru8QUzEIn/3i8rJmiVzyiTntUPUEQKgfcBwHgihDFMqAGKbPBRGO40ASBZimiezbP6EE+Byj3zQCDrHCTLKmJWiWxL1685xHoepxgFA/4Jd25rNdADEDz3p2d6L37bWURJ9j9JtGwCFWmIlw+Lmryn6bVy67j0ua6ks/B04OGbSCyXQ2YJomVD0Bs5ilJFhiTU/vn1Rx3+L6BausCH6b1y+9223f9fDEpNfEBPnSbX+iv/04zGKOkmBJNT29f1LFfYvrFyTPFgHlMndaySTNjYaSIE6GVw5AVVENfgBbZ1LnPsfVOjjg3l+/9G63fpVrgJ8EcSK8ct5+BfgBbJ1JnTgWNTtX66rW6nfMvaDsv//LUSdQvn///kHX9090tpeju49hoflJ4Hly0lkgjo1Vv+8ZdGLyqzc/Hrz+be28f4sjk9A8YTJ6jx7C4+0/FwBg20f/buxMcLTpD+qF/Hqh+UnV8FdXUgBYjalqzAgAgFuu+g4A4NU/A0twpOH1M+k3T5iMCYnIyUXA0hsWUmcGYEnTEc8nNCJZjeun8MS29jIUr+ITHa8jlYxBlUX0ZvKwbUKHPEmC4ziYtnIxHb76MhSUSAIda0o0w3p/X4CA4q71VSvIqnWHf90AYPPE2dBlDelCOqDyTYlmlJ0Scv05EELg73sqkoIghVB2SnisMJX6kgULBteAY+s7ccYD1wOg4HWN1mjaJACepS/WvYZUPAIRQK6viPCyuVVSkSpsXb/iAeR3PBcY/zeccREA4N5jHwfWHpsnzqbfyFq1xBVw8IloEieM3qq2y04JghQamQmc6HidAyWlMlRZRPfxDHRNQS7v8AmRnwjTJl7jSkUUa+o4vPRGJ5ZcS4m13ljLJcuAM0AMMEsRLYKyU6p2er6UiCbpO6xtV0OY9EfsAxzHG+pM00b38QxSyRhM0xyxTR3PHsf4xHjg4iReeqOTOkN3ArRh/AxAJAGpMcCs8wxArj+HeDgOZc8XUNWw27d+2PIxaJoOwyhCsc1AmeUrw7TzRu4EHcdBJKKhpYmGmoqGNx32593HM/ybcKWUfOqYPa8JiWgS4oEvsaFlBuJKnNr0UI5JCiERpeG2O+bfGChTdc/bm8XCgGXnbv0jAGDRUATss7xQ0vlKFqZpI1cscm0AAP87ETk43DT57VQBFFHmtshU9eW2KxGvpZoWUJY9m/anSltm4JhjpdPgCCdC1SNQVZU+V9Xhm4A/lrcvB0yeMBkFkUaBiUJw6NChQLxPkiTouk41xCWq0j6ZCleC4oAqJjL+9wQphGxfJvDML1mWSsRBSJQgCDIQCtGcESSGRxcVjsfjUFUV8Xg8cD3krKyGo7KIzYkIgPd/pwSfs3f9KRyhRDCAhkH9kqZHIClUEAJEiIondaMvO/KJUC6XGxDsYGUvrHoRIVVEySQIqSJmzv0WrrjyyioiAGDXzp34+44PqARrvG+VrMA3u3buxK03zeDgaW5zCROjCFFRoQIglglRo4Ro0eFrgMSGtpaWFvT09CCVSiEUCnE7y+VyaGlpgW3TThFC+OhACMHtK2/zhiMLeOEnL+L9P/2NqpcqcrDs/taHFgc05rc/3VLzfXaNm9wFoCthRdRd3VUAVJhgSAFKFvqNIr7KFUY+CrS1tUGWZYTDlMFSqYS2traatj5QeuSZ5XxIY/YcD8e5GZzI9gac4MMrHuRmwGyfDYWCFOLAiWXS65DLXMniZaKigljekB12NWFYBORynt3puo5QKARZpirHNKG31+u0pml8OCyVSjUnQp+teQWxiKeG/QDyhX5Meez7g67kmONjhJSdEgUuV6wiXVNhoP32P+K1AL6mtHTpUmzatGnkH65dC7S2Al1duGf7yziaDar6SRMPGCqNCjxAwZ9zDr+dkIhUkXBKEFB36uriwEcbCzg1CejqCtzWA/5r9QHDTosXA9God3/WWYA7SuHzz4PvTptG8+5u79nq1ae4BvT2Au7UGy0twPTpgGkCxSI/H4SeHlo2adKIqw+d9AQ0NwfvTZPGw1OphlQfOuV8AJuUpdMNqe6kN4HlRz7AuWefQWeKe/dgSmk6cICW/Wf/p3QCpSvAwX9iXOkoAODL4x45y087DQD4PD+hK0joCrJF6/Q1gbxhD0rC/5UPSOh00TEuXj32j1YLThkCBgPIiDktnWBMq72fOC4ewVeZ2iSkC8ZpPAxWaEU9DvCUWgswCe85eASpiIZ0wUBfrohoWEbRoo6yX0uPSPpjshao93xB929uKK8/czpgBff/16/eLABAx9sfDy9eAOCejtVDxgtOOhMgjo0Huj8dfQWtrcCiRXzVONRq8aQ0AZLJA03B/f8RBz1aW4cVLzj5nGBfPzZMm11fvGDr1mFrQN0+4K55F5X95wc0TUVzMgbTJki7ByL85ZGIhvid83gA9ES2lwdK2Vb5K9/8rjucBRc8bP//lo86cfY5D9G+b9tWV7ygbhPwg5MkCbZN+NY53V4vBrbVAUD83bvog3u+4La5PEosSjI9U+xPFVHkslPCqxdfD2R98YItW+j1k0/SeMGCBdQZjh9Pnz/9NC2rES9ouA/QNQXpTB5njk+iaJSriOLLepsEQQJYf+Z0pOQU30+oJKHm/n+teAEANDV5AZOxdoL+8wUAkErGAlvog6WB9v81owhrNPv/LF6QSgGHDv1vJ0KO44CUZMQ0DTFNQ8+JNBzHGfR8wfI69//fO/IB1rnP1ux9C1NK0+mZgHQaa/a+BQB4HEDHwZ014wV1E1Dv+YJKcI3a/9964D18NYxFUt0ENOJ8Qa39fwZWEGSoemTA/f+B4gVfy1qAnSnwAxzqfEE4kkS5bEMQZIiyDMMoQtOCErb6LagDrApXbn4WzybOBgAUsocRkmRkH7wX+XQXSo6NkCTjxh8v59exVCvy6S5sfW5LYwkY7fkCwDv8QNwteCZhQaAeX9XcP0oaJrRo8N9hq+74ETa98y8AQMeSuRjXeh4Wrfo1Oh78AZChBzTveWkH1tz8bUyZNr3xPqDe8wV8buBucTOARl8WonveqLKsUgOedM8lMikPRwM2PrOx8SYwmvMFlXv7dIKuQIsmQAgBsUzvbIBb5k8/u38l1r31aZUGbF15J48aP/7aO7yMLakbpgF1ny8AUC7bIJYH3r//zwjyH4GpTB1L5vLrPQePYN1dNwMAjrnxgpXfm4WjmULNeMF/ARkM8/cV3rqtAAAAAElFTkSuQmCC"]:["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