This commit is contained in:
Lea 2024-06-16 22:25:56 +02:00
parent e730a98b48
commit 4c8ba752cd
Signed by: Lea
GPG key ID: 1BAFFE8347019C42
6 changed files with 79 additions and 6 deletions

View file

@ -15,6 +15,7 @@
"devDependencies": { "devDependencies": {
"@types/color": "^3.0.6", "@types/color": "^3.0.6",
"@types/express": "^4.17.21", "@types/express": "^4.17.21",
"@types/morgan": "^1.9.9",
"@types/node": "^20.14.2", "@types/node": "^20.14.2",
"@types/seedrandom": "^3.0.8", "@types/seedrandom": "^3.0.8",
"typescript": "^5.4.5" "typescript": "^5.4.5"
@ -25,6 +26,7 @@
"color": "^4.2.3", "color": "^4.2.3",
"express": "^4.19.2", "express": "^4.19.2",
"isolated-vm": "^5.0.0", "isolated-vm": "^5.0.0",
"morgan": "^1.10.0",
"seedrandom": "^3.0.5" "seedrandom": "^3.0.5"
} }
} }

View file

@ -20,6 +20,9 @@ dependencies:
isolated-vm: isolated-vm:
specifier: ^5.0.0 specifier: ^5.0.0
version: 5.0.0 version: 5.0.0
morgan:
specifier: ^1.10.0
version: 1.10.0
seedrandom: seedrandom:
specifier: ^3.0.5 specifier: ^3.0.5
version: 3.0.5 version: 3.0.5
@ -31,6 +34,9 @@ devDependencies:
'@types/express': '@types/express':
specifier: ^4.17.21 specifier: ^4.17.21
version: 4.17.21 version: 4.17.21
'@types/morgan':
specifier: ^1.9.9
version: 1.9.9
'@types/node': '@types/node':
specifier: ^20.14.2 specifier: ^20.14.2
version: 20.14.2 version: 20.14.2
@ -116,6 +122,12 @@ packages:
resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==}
dev: true dev: true
/@types/morgan@1.9.9:
resolution: {integrity: sha512-iRYSDKVaC6FkGSpEVVIvrRGw0DfJMiQzIn3qr2G5B3C//AWkulhXgaBd7tS9/J79GWSYMTHGs7PfI5b3Y8m+RQ==}
dependencies:
'@types/node': 20.14.2
dev: true
/@types/node@20.14.2: /@types/node@20.14.2:
resolution: {integrity: sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q==} resolution: {integrity: sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q==}
dependencies: dependencies:
@ -214,6 +226,13 @@ packages:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
dev: false dev: false
/basic-auth@2.0.1:
resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==}
engines: {node: '>= 0.8'}
dependencies:
safe-buffer: 5.1.2
dev: false
/bl@4.1.0: /bl@4.1.0:
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
dependencies: dependencies:
@ -818,6 +837,19 @@ packages:
hasBin: true hasBin: true
dev: false dev: false
/morgan@1.10.0:
resolution: {integrity: sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==}
engines: {node: '>= 0.8.0'}
dependencies:
basic-auth: 2.0.1
debug: 2.6.9
depd: 2.0.0
on-finished: 2.3.0
on-headers: 1.0.2
transitivePeerDependencies:
- supports-color
dev: false
/ms@2.0.0: /ms@2.0.0:
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
dev: false dev: false
@ -889,6 +921,13 @@ packages:
resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==} resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==}
dev: false dev: false
/on-finished@2.3.0:
resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==}
engines: {node: '>= 0.8'}
dependencies:
ee-first: 1.1.1
dev: false
/on-finished@2.4.1: /on-finished@2.4.1:
resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
@ -896,6 +935,11 @@ packages:
ee-first: 1.1.1 ee-first: 1.1.1
dev: false dev: false
/on-headers@1.0.2:
resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==}
engines: {node: '>= 0.8'}
dev: false
/once@1.4.0: /once@1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
dependencies: dependencies:
@ -1003,6 +1047,10 @@ packages:
glob: 7.2.3 glob: 7.2.3
dev: false dev: false
/safe-buffer@5.1.2:
resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
dev: false
/safe-buffer@5.2.1: /safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
dev: false dev: false

View file

@ -5,9 +5,12 @@ import { generatePicrew } from "./picrew.js";
import { dirname, isValidSha256 } from "./util.js"; import { dirname, isValidSha256 } from "./util.js";
import { HttpRespondError } from "./types.js"; import { HttpRespondError } from "./types.js";
import path from "path"; import path from "path";
import morgan from "morgan";
const app = express(); const app = express();
app.use(morgan("tiny"));
app.get("/generate/:makerid/:hash", async (req: express.Request, res: express.Response) => { app.get("/generate/:makerid/:hash", async (req: express.Request, res: express.Response) => {
try { try {
const makerid = req.params.makerid as string; const makerid = req.params.makerid as string;
@ -19,12 +22,16 @@ app.get("/generate/:makerid/:hash", async (req: express.Request, res: express.Re
if (["true", "1"].includes(`${req.query.gravatar || ""}`.toLowerCase())) { if (["true", "1"].includes(`${req.query.gravatar || ""}`.toLowerCase())) {
const gravatarExists = !!(await axios.head(`https://gravatar.com/avatar/${hash}?d=404`).catch(() => false)); const gravatarExists = !!(await axios.head(`https://gravatar.com/avatar/${hash}?d=404`).catch(() => false));
if (gravatarExists) return res.redirect(`https://gravatar.com/avatar/${hash}?s=256`); if (gravatarExists) {
console.log(`Redirecting to Gravatar: ${hash}`);
return res.redirect(`https://gravatar.com/avatar/${hash}?s=256`);
}
} }
const path = await generatePicrew(hash, makerid); const path = await generatePicrew(hash, makerid);
res.sendFile(path); res.sendFile(path);
} catch(e) { } catch(e) {
console.log(e);
if (e instanceof HttpRespondError) { if (e instanceof HttpRespondError) {
res.status(e.statusCode).send(e.message); res.status(e.statusCode).send(e.message);
} else { } else {

View file

@ -1,7 +1,6 @@
import { createCanvas, loadImage } from "canvas"; import { createCanvas, loadImage } from "canvas";
import { fetchScript, extractDataFromScript, getAsset, dirname } from "./util.js"; import { fetchScript, extractDataFromScript, getAsset, dirname } from "./util.js";
import fs from "fs/promises"; import fs from "fs/promises";
import crypto from "crypto";
import path from "path"; import path from "path";
import Color from "color"; import Color from "color";
import seedrandom from "seedrandom"; import seedrandom from "seedrandom";
@ -9,11 +8,14 @@ import seedrandom from "seedrandom";
// Generates a picrew from a given maker ID using the provided hash. // Generates a picrew from a given maker ID using the provided hash.
// Returns the output file path. // Returns the output file path.
export async function generatePicrew(hash: string, maker: string) { export async function generatePicrew(hash: string, maker: string) {
console.log(`Request for ${hash}:${maker}`);
const outDir = path.join(dirname, "..", "cache", "outputs", maker); const outDir = path.join(dirname, "..", "cache", "outputs", maker);
const filePath = path.join(outDir, hash + ".png"); const filePath = path.join(outDir, hash + ".png");
// If the file is already cached, just send that again // If the file is already cached, just send that again
if (!!(await fs.stat(filePath).catch(() => false))) { if (!!(await fs.stat(filePath).catch(() => false))) {
console.log(`${hash}:${maker} already exists, responding with cached file`);
return filePath; return filePath;
} }
@ -85,9 +87,9 @@ export async function generatePicrew(hash: string, maker: string) {
if (canHide) { if (canHide) {
// If there's other things that would be blocked by this, // If there's other things that would be blocked by this,
// it should be more likely for this one to be hidden so // it should possible for this one to be hidden so
// the other item appears more frequently. // the other item can appear too.
if (rng() < (inRuleset ? 0.5 : 0.2)) continue; if (rng() < (inRuleset ? 0.5 : 0)) continue;
} }
// Check if a conflicting object already exists, and if so, skip this one // Check if a conflicting object already exists, and if so, skip this one
@ -134,5 +136,6 @@ export async function generatePicrew(hash: string, maker: string) {
await fs.writeFile(filePath, canvas.toBuffer()); await fs.writeFile(filePath, canvas.toBuffer());
console.log(`Generated ${hash}:${maker}, responding with generated file`);
return filePath; return filePath;
} }

View file

@ -36,6 +36,7 @@ export async function fetchScript(makerId: string) {
try { try {
await downloadToPath(`https://picrew.me/en/image_maker/${encodeURIComponent(makerId)}`, htmlPath); await downloadToPath(`https://picrew.me/en/image_maker/${encodeURIComponent(makerId)}`, htmlPath);
} catch(e) { } catch(e) {
await fs.unlink(htmlPath).catch(() => {});
if (e instanceof AxiosError && e.status == 404) { if (e instanceof AxiosError && e.status == 404) {
throw new HttpRespondError(`There is no Picrew maker with ID ${makerId}.`, 400); throw new HttpRespondError(`There is no Picrew maker with ID ${makerId}.`, 400);
} }

View file

@ -33,7 +33,7 @@
</style> </style>
</head> </head>
<body> <body>
<h1>Picvatar</h1> <h1 id="picvatar">Picvatar</h1>
<p>Generate unique avatars from a Picrew - Like Gravatar, but cooler!</p> <p>Generate unique avatars from a Picrew - Like Gravatar, but cooler!</p>
<div class="input-wrapper"> <div class="input-wrapper">
@ -91,6 +91,18 @@
</p> </p>
</body> </body>
<h3>The boring stuff</h3>
<p>
If you plan on using this, please respect the copyright and license of the Picrew - it's not my job to enforce other people's copyright, so please be reasonable. I also recommend self-hosting this - a Dockerfile and example docker-compose config is provided in the repository.
</p>
<p>
This is a one-day side project of mine that I'll likely never touch again, but the source code is available <a href="https://git.amogus.cloud/Lea/picvatar" target="_blank">here</a>. <br />
Reverse engineering Picrew and trying to figure out how to use the available data to manually render images was a fun challenge, but it is nothing I intend to put long term maintenance into. If you find a bug, feel free to report it, but if Picrew completely changes their backend then I will likely not rewrite everything from scratch to accomodate that.
</p>
<p>
Enjoy &lt;3
</p>
<script> <script>
const outputImg = document.getElementById("output-img"); const outputImg = document.getElementById("output-img");