From 393538951a1d8560a537b37f27e3a54d8e144388 Mon Sep 17 00:00:00 2001 From: Lea Date: Sun, 16 Jun 2024 21:55:01 +0200 Subject: [PATCH] YEAH! --- package.json | 6 +- pnpm-lock.yaml | 521 +++++++++++++++++++++++++++++++++++++++++++++++++ src/index.ts | 138 ++++--------- src/picrew.ts | 138 +++++++++++++ src/types.ts | 9 + src/util.ts | 21 +- web/index.html | 111 +++++++++++ web/kitty.png | Bin 0 -> 25736 bytes 8 files changed, 835 insertions(+), 109 deletions(-) create mode 100644 src/picrew.ts create mode 100644 web/index.html create mode 100644 web/kitty.png diff --git a/package.json b/package.json index 43fea56..9f240fe 100644 --- a/package.json +++ b/package.json @@ -14,13 +14,17 @@ "license": "ISC", "devDependencies": { "@types/color": "^3.0.6", + "@types/express": "^4.17.21", "@types/node": "^20.14.2", + "@types/seedrandom": "^3.0.8", "typescript": "^5.4.5" }, "dependencies": { "axios": "^1.7.2", "canvas": "^2.11.2", "color": "^4.2.3", - "isolated-vm": "^5.0.0" + "express": "^4.19.2", + "isolated-vm": "^5.0.0", + "seedrandom": "^3.0.5" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dcfc1d2..dd398c2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,17 +14,29 @@ dependencies: color: specifier: ^4.2.3 version: 4.2.3 + express: + specifier: ^4.19.2 + version: 4.19.2 isolated-vm: specifier: ^5.0.0 version: 5.0.0 + seedrandom: + specifier: ^3.0.5 + version: 3.0.5 devDependencies: '@types/color': specifier: ^3.0.6 version: 3.0.6 + '@types/express': + specifier: ^4.17.21 + version: 4.17.21 '@types/node': specifier: ^20.14.2 version: 20.14.2 + '@types/seedrandom': + specifier: ^3.0.8 + version: 3.0.8 typescript: specifier: ^5.4.5 version: 5.4.5 @@ -49,6 +61,13 @@ packages: - supports-color dev: false + /@types/body-parser@1.19.5: + resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} + dependencies: + '@types/connect': 3.4.38 + '@types/node': 20.14.2 + dev: true + /@types/color-convert@2.0.3: resolution: {integrity: sha512-2Q6wzrNiuEvYxVQqhh7sXM2mhIhvZR/Paq4FdsQkOMgWsCIkKvSGj8Le1/XalulrmgOzPMqNa0ix+ePY4hTrfg==} dependencies: @@ -65,16 +84,83 @@ packages: '@types/color-convert': 2.0.3 dev: true + /@types/connect@3.4.38: + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + dependencies: + '@types/node': 20.14.2 + dev: true + + /@types/express-serve-static-core@4.19.3: + resolution: {integrity: sha512-KOzM7MhcBFlmnlr/fzISFF5vGWVSvN6fTd4T+ExOt08bA/dA5kpSzY52nMsI1KDFmUREpJelPYyuslLRSjjgCg==} + dependencies: + '@types/node': 20.14.2 + '@types/qs': 6.9.15 + '@types/range-parser': 1.2.7 + '@types/send': 0.17.4 + dev: true + + /@types/express@4.17.21: + resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==} + dependencies: + '@types/body-parser': 1.19.5 + '@types/express-serve-static-core': 4.19.3 + '@types/qs': 6.9.15 + '@types/serve-static': 1.15.7 + dev: true + + /@types/http-errors@2.0.4: + resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} + dev: true + + /@types/mime@1.3.5: + resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + dev: true + /@types/node@20.14.2: resolution: {integrity: sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q==} dependencies: undici-types: 5.26.5 dev: true + /@types/qs@6.9.15: + resolution: {integrity: sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==} + dev: true + + /@types/range-parser@1.2.7: + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + dev: true + + /@types/seedrandom@3.0.8: + resolution: {integrity: sha512-TY1eezMU2zH2ozQoAFAQFOPpvP15g+ZgSfTZt31AUUH/Rxtnz3H+A/Sv1Snw2/amp//omibc+AEkTaA8KUeOLQ==} + dev: true + + /@types/send@0.17.4: + resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==} + dependencies: + '@types/mime': 1.3.5 + '@types/node': 20.14.2 + dev: true + + /@types/serve-static@1.15.7: + resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==} + dependencies: + '@types/http-errors': 2.0.4 + '@types/node': 20.14.2 + '@types/send': 0.17.4 + dev: true + /abbrev@1.1.1: resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} dev: false + /accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + dev: false + /agent-base@6.0.2: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} @@ -102,6 +188,10 @@ packages: readable-stream: 3.6.2 dev: false + /array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + dev: false + /asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} dev: false @@ -132,6 +222,26 @@ packages: readable-stream: 3.6.2 dev: false + /body-parser@1.20.2: + resolution: {integrity: sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.11.0 + raw-body: 2.5.2 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + dev: false + /brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} dependencies: @@ -146,6 +256,22 @@ packages: ieee754: 1.2.1 dev: false + /bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + dev: false + + /call-bind@1.0.7: + resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} + engines: {node: '>= 0.4'} + dependencies: + es-define-property: 1.0.0 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + set-function-length: 1.2.2 + dev: false + /canvas@2.11.2: resolution: {integrity: sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw==} engines: {node: '>=6'} @@ -214,6 +340,38 @@ packages: resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} dev: false + /content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + dependencies: + safe-buffer: 5.2.1 + dev: false + + /content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + dev: false + + /cookie-signature@1.0.6: + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + dev: false + + /cookie@0.6.0: + resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} + engines: {node: '>= 0.6'} + dev: false + + /debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.0.0 + dev: false + /debug@4.3.5: resolution: {integrity: sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==} engines: {node: '>=6.0'} @@ -245,6 +403,15 @@ packages: engines: {node: '>=4.0.0'} dev: false + /define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + dependencies: + es-define-property: 1.0.0 + es-errors: 1.3.0 + gopd: 1.0.1 + dev: false + /delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} @@ -254,26 +421,120 @@ packages: resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} dev: false + /depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + dev: false + + /destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + dev: false + /detect-libc@2.0.3: resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} engines: {node: '>=8'} dev: false + /ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + dev: false + /emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} dev: false + /encodeurl@1.0.2: + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} + dev: false + /end-of-stream@1.4.4: resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} dependencies: once: 1.4.0 dev: false + /es-define-property@1.0.0: + resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.4 + dev: false + + /es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + dev: false + + /escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + dev: false + + /etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + dev: false + /expand-template@2.0.3: resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} engines: {node: '>=6'} dev: false + /express@4.19.2: + resolution: {integrity: sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==} + engines: {node: '>= 0.10.0'} + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.2 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.6.0 + cookie-signature: 1.0.6 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.2.0 + fresh: 0.5.2 + http-errors: 2.0.0 + merge-descriptors: 1.0.1 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.7 + proxy-addr: 2.0.7 + qs: 6.11.0 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.18.0 + serve-static: 1.15.0 + setprototypeof: 1.2.0 + statuses: 2.0.1 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + dev: false + + /finalhandler@1.2.0: + resolution: {integrity: sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==} + engines: {node: '>= 0.8'} + dependencies: + debug: 2.6.9 + encodeurl: 1.0.2 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + dev: false + /follow-redirects@1.15.6: resolution: {integrity: sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==} engines: {node: '>=4.0'} @@ -293,6 +554,16 @@ packages: mime-types: 2.1.35 dev: false + /forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + dev: false + + /fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + dev: false + /fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} dev: false @@ -308,6 +579,10 @@ packages: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} dev: false + /function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + dev: false + /gauge@3.0.2: resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==} engines: {node: '>=10'} @@ -324,6 +599,17 @@ packages: wide-align: 1.1.5 dev: false + /get-intrinsic@1.2.4: + resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + has-proto: 1.0.3 + has-symbols: 1.0.3 + hasown: 2.0.2 + dev: false + /github-from-package@0.0.0: resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} dev: false @@ -340,10 +626,50 @@ packages: path-is-absolute: 1.0.1 dev: false + /gopd@1.0.1: + resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + dependencies: + get-intrinsic: 1.2.4 + dev: false + + /has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + dependencies: + es-define-property: 1.0.0 + dev: false + + /has-proto@1.0.3: + resolution: {integrity: sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==} + engines: {node: '>= 0.4'} + dev: false + + /has-symbols@1.0.3: + resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} + engines: {node: '>= 0.4'} + dev: false + /has-unicode@2.0.1: resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} dev: false + /hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + dependencies: + function-bind: 1.1.2 + dev: false + + /http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + dev: false + /https-proxy-agent@5.0.1: resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} engines: {node: '>= 6'} @@ -354,6 +680,13 @@ packages: - supports-color dev: false + /iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + dependencies: + safer-buffer: 2.1.2 + dev: false + /ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} dev: false @@ -374,6 +707,11 @@ packages: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} dev: false + /ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + dev: false + /is-arrayish@0.3.2: resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} dev: false @@ -398,6 +736,20 @@ packages: semver: 6.3.1 dev: false + /media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + dev: false + + /merge-descriptors@1.0.1: + resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==} + dev: false + + /methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + dev: false + /mime-db@1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} @@ -410,6 +762,12 @@ packages: mime-db: 1.52.0 dev: false + /mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + dev: false + /mimic-response@2.1.0: resolution: {integrity: sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==} engines: {node: '>=8'} @@ -460,10 +818,18 @@ packages: hasBin: true dev: false + /ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + dev: false + /ms@2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} dev: false + /ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + dev: false + /nan@2.20.0: resolution: {integrity: sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw==} dev: false @@ -472,6 +838,11 @@ packages: resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==} dev: false + /negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + dev: false + /node-abi@3.65.0: resolution: {integrity: sha512-ThjYBfoDNr08AWx6hGaRbfPwxKV9kVzAzOzlLKbk2CuqXE2xnCh+cbAGnwM3t8Lq4v9rUB7VfondlkBckcJrVA==} engines: {node: '>=10'} @@ -514,17 +885,37 @@ packages: engines: {node: '>=0.10.0'} dev: false + /object-inspect@1.13.1: + resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==} + dev: false + + /on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + dependencies: + ee-first: 1.1.1 + dev: false + /once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} dependencies: wrappy: 1.0.2 dev: false + /parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + dev: false + /path-is-absolute@1.0.1: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} engines: {node: '>=0.10.0'} dev: false + /path-to-regexp@0.1.7: + resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==} + dev: false + /prebuild-install@7.1.2: resolution: {integrity: sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==} engines: {node: '>=10'} @@ -544,6 +935,14 @@ packages: tunnel-agent: 0.6.0 dev: false + /proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + dev: false + /proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} dev: false @@ -555,6 +954,28 @@ packages: once: 1.4.0 dev: false + /qs@6.11.0: + resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==} + engines: {node: '>=0.6'} + dependencies: + side-channel: 1.0.6 + dev: false + + /range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + dev: false + + /raw-body@2.5.2: + resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} + engines: {node: '>= 0.8'} + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + dev: false + /rc@1.2.8: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true @@ -586,6 +1007,14 @@ packages: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} dev: false + /safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + dev: false + + /seedrandom@3.0.5: + resolution: {integrity: sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==} + dev: false + /semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -597,10 +1026,69 @@ packages: hasBin: true dev: false + /send@0.18.0: + resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==} + engines: {node: '>= 0.8.0'} + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.0 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + dev: false + + /serve-static@1.15.0: + resolution: {integrity: sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==} + engines: {node: '>= 0.8.0'} + dependencies: + encodeurl: 1.0.2 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.18.0 + transitivePeerDependencies: + - supports-color + dev: false + /set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} dev: false + /set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + gopd: 1.0.1 + has-property-descriptors: 1.0.2 + dev: false + + /setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + dev: false + + /side-channel@1.0.6: + resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + object-inspect: 1.13.1 + dev: false + /signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} dev: false @@ -631,6 +1119,11 @@ packages: is-arrayish: 0.3.2 dev: false + /statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + dev: false + /string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -690,6 +1183,11 @@ packages: yallist: 4.0.0 dev: false + /toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + dev: false + /tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} dev: false @@ -700,6 +1198,14 @@ packages: safe-buffer: 5.2.1 dev: false + /type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + dev: false + /typescript@5.4.5: resolution: {integrity: sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==} engines: {node: '>=14.17'} @@ -710,10 +1216,25 @@ packages: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} dev: true + /unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + dev: false + /util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} dev: false + /utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + dev: false + + /vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + dev: false + /webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} dev: false diff --git a/src/index.ts b/src/index.ts index 8ac9702..62c03e5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,120 +1,52 @@ -import { createCanvas, loadImage } from "canvas"; -import { fetchScript, extractDataFromScript, getAsset, dirname } from "./util.js"; -import { writeFile } from "fs/promises"; +import axios from "axios"; +import express from "express"; +import crypto from "crypto"; +import { generatePicrew } from "./picrew.js"; +import { dirname, isValidSha256 } from "./util.js"; +import { HttpRespondError } from "./types.js"; import path from "path"; -import Color from "color"; -const MAKER_ID = "1904634"; +const app = express(); -await fetchScript(MAKER_ID); -const { config, commonImages, cdnRoot, partsId2Index } = await extractDataFromScript(MAKER_ID); +app.get("/generate/:makerid/:hash", async (req: express.Request, res: express.Response) => { + try { + const makerid = req.params.makerid as string; + const hash = req.params.hash as string; -type UrlsList = { colorId: string, url: string }; -type LayerList = { layerId: string, urls: UrlsList[] }; -type ItemList = { itemId: string, layers: LayerList[] }; -type LayerColorList = { layerIndex: number, cpId: string, colors: string[] }; -type CategoryList = { cpId: string, pId: string, items: ItemList[], index: number, isRmv: boolean }; + if (!isValidSha256(hash)) return res.status(400).send("The hash you provided is not a valid hex-encoded sha256 hash."); + if (isNaN(Number(makerid))) return res.status(400).send("You did not provide a valid Picrew maker ID."); -const exclusions: number[][] = Object.values(config.ruleList) - .filter((rule) => rule.pId == 0) // Not sure what pId is but just to be sure that this won't break something else - .map((rule) => rule.list); + if (["true", "1"].includes(`${req.query.gravatar || ""}`.toLowerCase())) { + const gravatarExists = !!(await axios.head(`https://gravatar.com/avatar/${hash}?d=404`).catch(() => false)); -const items: ItemList[] = []; -const categories: CategoryList[] = []; -const layerColors: LayerColorList[] = []; - -for (const c in config.pList) { - const category = config.pList[c]; - - layerColors.push({ - layerIndex: Number(c), - cpId: category.cpId, - colors: config.cpList[category.cpId].map((c) => c.cId.toString()), - }); - - const localItems: ItemList[] = []; - for (const item of category.items) { - const id = item.itmId; - - const obj = commonImages[id]; - const layers: LayerList[] = []; - - for (const layer of category.lyrs) { - if (!obj[layer]) continue; // Not every entry uses all layers - const variants = Object.keys(obj[layer]); - - const urls: UrlsList[] = variants.map((v) => ({ - colorId: v, - url: cdnRoot + obj[layer][v].url, - })); - - layers.push({ layerId: layer.toString(), urls }); + if (gravatarExists) return res.redirect(`https://gravatar.com/avatar/${hash}?s=256`); } - - localItems.push({ itemId: id.toString(), layers: layers }); - } - items.push(...localItems); - categories.push({ - cpId: category.cpId, - pId: category.pId.toString(), - items: localItems, - index: partsId2Index[category.pId.toString()], - isRmv: !!category.isRmv, - }); -} - -const images: { layer: number, url: string, categoryId: number }[] = []; - -categoryLoop: for (const category of categories.sort((a, b) => b.index - a.index)) { - const canHide = category.isRmv; - const inRuleset = exclusions.find((arr) => arr.includes(Number(category.pId))); - - if (canHide) { - // If there's other things that would be blocked by this, - // it should be more likely for this one to be hidden so - // the other item appears more frequently. - if (Math.random() < (inRuleset ? 0.5 : 0.2)) continue; - } - - // Check if a conflicting object already exists, and if so, skip this one - for (const rule of exclusions) { - if (rule.includes(Number(category.pId))) { - if (images.find((i) => rule.includes(i.categoryId))) continue categoryLoop; + const path = await generatePicrew(hash, makerid); + res.sendFile(path); + } catch(e) { + if (e instanceof HttpRespondError) { + res.status(e.statusCode).send(e.message); + } else { + res.status(500).send(`${e}`); } } +}); - const colors = layerColors.find((c) => c.cpId == category.cpId)!.colors; - const colorId = colors[Math.floor(Math.random() * colors.length)]; - const item = category.items[Math.floor(Math.random() * category.items.length)]; +app.get("/generate-by-email/:makerid/:email", async (req: express.Request, res: express.Response) => { + const hash = crypto.createHash("sha256").update(req.params.email).digest("hex"); + const url = path.join("/", "generate", req.params.makerid, hash); - for (const layer of item.layers) { - const url = (layer.urls.find((i) => i.colorId == colorId) ?? layer.urls?.[0])?.url; - - if (url) { - images.push({ layer: Number(layer.layerId), url, categoryId: Number(category.pId) }); - } + const params = new URLSearchParams(); + for (const [k, v] of Object.entries(req.query)) { + if (typeof v == "string") params.append(k, v); } -} -const canvas = createCanvas(config.w, config.h); -const ctx = canvas.getContext('2d'); + res.redirect(url + (params.size ? "?" + params.toString() : "")); +}); -// So we don't accidentally end up with a transparent background -const bgCol = Color.hsv(Math.floor(Math.random() * 360), 20, 100).hex(); -ctx.fillStyle = bgCol; -ctx.fillRect(0, 0, config.w, config.h); +app.use(express.static(path.join(dirname, "..", "web"))); -// Retains the correct order -const imgPaths = await Promise.all( - images - .sort((a, b) => config.lyrList[a.layer] - config.lyrList[b.layer]) - .map((img) => getAsset(img.url)) -); +// 1904634 -for (const path of imgPaths) { - const img = await loadImage(path); - ctx.drawImage(img, 0, 0, config.w, config.h); -} - -await writeFile(path.join(dirname, "..", "cache", MAKER_ID + ".png"), canvas.toBuffer()); +app.listen(3000, () => console.log("Listening on http://127.0.0.1:3000")); diff --git a/src/picrew.ts b/src/picrew.ts new file mode 100644 index 0000000..a4f2f53 --- /dev/null +++ b/src/picrew.ts @@ -0,0 +1,138 @@ +import { createCanvas, loadImage } from "canvas"; +import { fetchScript, extractDataFromScript, getAsset, dirname } from "./util.js"; +import fs from "fs/promises"; +import crypto from "crypto"; +import path from "path"; +import Color from "color"; +import seedrandom from "seedrandom"; + +// Generates a picrew from a given maker ID using the provided hash. +// Returns the output file path. +export async function generatePicrew(hash: string, maker: string) { + const outDir = path.join(dirname, "..", "cache", "outputs", maker); + const filePath = path.join(outDir, hash + ".png"); + + // If the file is already cached, just send that again + if (!!(await fs.stat(filePath).catch(() => false))) { + return filePath; + } + + const rng = seedrandom(hash); + + await fetchScript(maker); + const { config, commonImages, cdnRoot, partsId2Index } = await extractDataFromScript(maker); + + type UrlsList = { colorId: string, url: string }; + type LayerList = { layerId: string, urls: UrlsList[] }; + type ItemList = { itemId: string, layers: LayerList[] }; + type LayerColorList = { layerIndex: number, cpId: string, colors: string[] }; + type CategoryList = { cpId: string, pId: string, items: ItemList[], index: number, isRmv: boolean }; + + const exclusions: number[][] = Object.values(config.ruleList) + .filter((rule) => rule.pId == 0) // Not sure what pId is but just to be sure that this won't break something else + .map((rule) => rule.list); + + const items: ItemList[] = []; + const categories: CategoryList[] = []; + const layerColors: LayerColorList[] = []; + + for (const c in config.pList) { + const category = config.pList[c]; + + layerColors.push({ + layerIndex: Number(c), + cpId: category.cpId, + colors: config.cpList[category.cpId].map((c) => c.cId.toString()), + }); + + const localItems: ItemList[] = []; + for (const item of category.items) { + const id = item.itmId; + + const obj = commonImages[id]; + const layers: LayerList[] = []; + + for (const layer of category.lyrs) { + if (!obj[layer]) continue; // Not every entry uses all layers + const variants = Object.keys(obj[layer]); + + const urls: UrlsList[] = variants.map((v) => ({ + colorId: v, + url: cdnRoot + obj[layer][v].url, + })); + + layers.push({ layerId: layer.toString(), urls }); + } + + localItems.push({ itemId: id.toString(), layers: layers }); + } + + items.push(...localItems); + categories.push({ + cpId: category.cpId, + pId: category.pId.toString(), + items: localItems, + index: partsId2Index[category.pId.toString()], + isRmv: !!category.isRmv, + }); + } + + const images: { layer: number, url: string, categoryId: number }[] = []; + + categoryLoop: for (const category of categories.sort((a, b) => b.index - a.index)) { + const canHide = category.isRmv; + const inRuleset = exclusions.find((arr) => arr.includes(Number(category.pId))); + + if (canHide) { + // If there's other things that would be blocked by this, + // it should be more likely for this one to be hidden so + // the other item appears more frequently. + if (rng() < (inRuleset ? 0.5 : 0.2)) continue; + } + + // Check if a conflicting object already exists, and if so, skip this one + for (const rule of exclusions) { + if (rule.includes(Number(category.pId))) { + if (images.find((i) => rule.includes(i.categoryId))) continue categoryLoop; + } + } + + const colors = layerColors.find((c) => c.cpId == category.cpId)!.colors; + const colorId = colors[Math.floor(rng() * colors.length)]; + const item = category.items[Math.floor(rng() * category.items.length)]; + + for (const layer of item.layers) { + const url = (layer.urls.find((i) => i.colorId == colorId) ?? layer.urls?.[0])?.url; + + if (url) { + images.push({ layer: Number(layer.layerId), url, categoryId: Number(category.pId) }); + } + } + } + + const canvas = createCanvas(config.w, config.h); + const ctx = canvas.getContext('2d'); + + // So we don't accidentally end up with a transparent background + const bgCol = Color.hsv(Math.floor(rng() * 360), 20, 100).hex(); + ctx.fillStyle = bgCol; + ctx.fillRect(0, 0, config.w, config.h); + + // Retains the correct order + const imgPaths = await Promise.all( + images + .sort((a, b) => config.lyrList[a.layer] - config.lyrList[b.layer]) + .map((img) => getAsset(img.url)) + ); + + for (const path of imgPaths) { + const img = await loadImage(path); + ctx.drawImage(img, 0, 0, config.w, config.h); + } + await fs.mkdir(outDir, { recursive: true }) + .catch((e) => { if (e?.code != "EEXIST") throw e; }); // Ignore "already exists" error + + await fs.writeFile(filePath, canvas.toBuffer()); + + return filePath; +} diff --git a/src/types.ts b/src/types.ts index b412298..83bf5cc 100644 --- a/src/types.ts +++ b/src/types.ts @@ -54,3 +54,12 @@ export type CommonImages = { export type PartsId2Index = { [key: string]: number; } + +export class HttpRespondError extends Error { + statusCode: number; + + constructor(message: string, statusCode: number) { + super(message); + this.statusCode = statusCode; + } +} diff --git a/src/util.ts b/src/util.ts index 20461d6..4960625 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,10 +1,10 @@ import path from "path"; import { fileURLToPath, URL } from "url"; import fs from "fs/promises"; -import axios from "axios"; +import axios, { AxiosError } from "axios"; import { createWriteStream } from "fs"; import ivm from "isolated-vm"; -import { CommonImages, Config, PartsId2Index } from "./types.js"; +import { CommonImages, Config, HttpRespondError, PartsId2Index } from "./types.js"; export const dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -33,7 +33,14 @@ export async function fetchScript(makerId: string) { const scriptExists = await fsExists(scriptPath); if (!htmlExists) { - await downloadToPath(`https://picrew.me/en/image_maker/${encodeURIComponent(makerId)}`, htmlPath); + try { + await downloadToPath(`https://picrew.me/en/image_maker/${encodeURIComponent(makerId)}`, htmlPath); + } catch(e) { + if (e instanceof AxiosError && e.status == 404) { + throw new HttpRespondError(`There is no Picrew maker with ID ${makerId}.`, 400); + } + else throw e; + } } if (!scriptExists) { @@ -76,7 +83,7 @@ export async function getAsset(url: string) { // https://cdn.picrew.me/app/image_maker/{makerId}/{pId}/{random_string}.png const [ makerId, cId, filename ] = new URL(url).pathname.split("/").slice(-3); - console.log(`Starting download: ${makerId}/${cId}/${filename}`); + //console.log(`Starting download: ${makerId}/${cId}/${filename}`); const dir = path.join(dirname, "..", "cache", "assets", makerId, cId); const filePath = path.join(dir, filename); @@ -88,6 +95,10 @@ export async function getAsset(url: string) { await downloadToPath(url, filePath); } - console.log(`Finished download: ${makerId}/${cId}/${filename}`); + //console.log(`Finished download: ${makerId}/${cId}/${filename}`); return filePath; } + +export async function isValidSha256(hash: string) { + return /^[0-9A-f]{64}$/.test(hash); +} diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..d484cbb --- /dev/null +++ b/web/index.html @@ -0,0 +1,111 @@ + + + + Picvatar + + + +

Picvatar

+

Generate unique avatars from a Picrew - Like Gravatar, but cooler!

+ +
+
+ + +
+
+ + +
+ +
+ +
+ +
+ +

API

+ +

Generate an image from a sha256 hash

+ /generate/:makerid/:hash?gravatar=bool +

+ Returns: The generated image in png format. +

+

+ :makerid - The ID of the Picrew maker you want to use. You can get this from the Picrew URL: https://picrew.me/en/image_maker/000000 +

+

+ :hash - Hex-encoded sha256 hash of the email address (or whatever other identifier you want to use). +

+

+ ?gravatar=bool - If this is set to true or 1, and the provided hash belongs to an email on Gravatar that has a custom avatar set, a redirect to that avatar will be returned and no Picrew will be generated. Any other value will disable this behaviour. +
+ Note that this will slow down load times. +

+ +

Generate an image from an email address

+ /generate/:makerid/:email +

+ Returns: a redirect to /generate/:makerid/:hash, where :hash is a sha256 hash created from the provided email address. This is useful when you don't have access to relevant APIs to generate a sha256 hash, such as on a vanilla JS website without external dependencies. +
+ Query parameters will be forwarded, so any parameters for /generate/:makerid/:hash are available. +

+

+ :makerid - See above. +

+

+ :email - The email address you want to generate an avatar for. Make sure this is URL-encoded! +

+ + + + diff --git a/web/kitty.png b/web/kitty.png new file mode 100644 index 0000000000000000000000000000000000000000..817cabf012b1b43f573de4fe7b73ca7547639e17 GIT binary patch literal 25736 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