Compare commits

...

2 commits

Author SHA1 Message Date
2de60a98a3 reformat with prettier 2024-07-14 20:33:47 -04:00
1d4a2673b0 add image upload from clipboard 2024-07-14 20:33:41 -04:00
6 changed files with 110 additions and 104 deletions

View file

@ -181,8 +181,8 @@ export function wordballoonDrawText(ctx: CanvasRenderingContext2D, at: Point, te
export function wordballoonDrawImage(ctx: CanvasRenderingContext2D, at: Point, img: HTMLImageElement, hasTip: boolean = true): Rect {
// Round the image size up to the nearest 12x12, plus 12px padding.
let size = {
w: (Math.ceil(img.width / 12) * 12) + 6,
h: (Math.ceil(img.height / 12) * 12) + 6
w: Math.ceil(img.width / 12) * 12 + 6,
h: Math.ceil(img.height / 12) * 12 + 6
};
// Draw the word balloon and get the inner rect
@ -195,6 +195,6 @@ export function wordballoonDrawImage(ctx: CanvasRenderingContext2D, at: Point, i
x: at.x,
y: at.y,
w: rectInner.w + 12 * 3 + 12,
h: rectInner.h + 13 * 3 + 18,
h: rectInner.h + 13 * 3 + 18
};
}
}

View file

@ -62,6 +62,6 @@ export interface DiscordConfig {
}
export interface ImagesConfig {
maxSize: { width: number, height: number };
maxSize: { width: number; height: number };
expirySeconds: number;
}
}

View file

@ -1,116 +1,111 @@
import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
import { fileTypeFromBuffer } from "file-type";
import * as crypto from "crypto";
import { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
import { fileTypeFromBuffer } from 'file-type';
import * as crypto from 'crypto';
import Sharp from 'sharp';
import { ImagesConfig } from "./config";
import { ImagesConfig } from './config';
export interface image {
img: Buffer;
mime: string;
timeout: NodeJS.Timeout;
img: Buffer;
mime: string;
timeout: NodeJS.Timeout;
}
const allowedTypes = [
"image/png",
"image/jpeg",
"image/gif",
"image/webp",
];
const allowedTypes = ['image/png', 'image/jpeg', 'image/gif', 'image/webp'];
function randomString(length: number): Promise<string> {
return new Promise((res, rej) => {
let _len = length;
if (_len % 2 !== 0) _len++;
crypto.randomBytes(_len / 2, (err, buf) => {
if (err) {
rej(err);
return;
}
let out = buf.toString("hex");
if (out.length !== length) out = out.substring(0, length);
res(out);
});
});
return new Promise((res, rej) => {
let _len = length;
if (_len % 2 !== 0) _len++;
crypto.randomBytes(_len / 2, (err, buf) => {
if (err) {
rej(err);
return;
}
let out = buf.toString('hex');
if (out.length !== length) out = out.substring(0, length);
res(out);
});
});
}
export class ImageUploader {
private images: Map<string, image> = new Map();
private config: ImagesConfig;
private images: Map<string, image> = new Map();
private config: ImagesConfig;
constructor(app: FastifyInstance, config: ImagesConfig) {
this.config = config;
for (let type of allowedTypes) {
// i kinda hate this
app.addContentTypeParser(type, {parseAs: "buffer"}, (req, body, done) => done(null, body));
}
app.put("/api/upload", async (req, res) => await this.handleRequest(req, res));
app.get("/api/image/:id", (req, res) => this.handleGet(req, res));
}
constructor(app: FastifyInstance, config: ImagesConfig) {
this.config = config;
for (let type of allowedTypes) {
// i kinda hate this
app.addContentTypeParser(type, { parseAs: 'buffer' }, (req, body, done) => done(null, body));
}
app.put('/api/upload', async (req, res) => await this.handleRequest(req, res));
app.get('/api/image/:id', (req, res) => this.handleGet(req, res));
}
private handleGet(req: FastifyRequest, res: FastifyReply) {
let {id} = req.params as {id: string};
let img = this.images.get(id);
if (!img) {
res.status(404);
return { success: false, error: "Image not found" };
}
res.header("Content-Type", img.mime);
return img.img;
}
private handleGet(req: FastifyRequest, res: FastifyReply) {
let { id } = req.params as { id: string };
let img = this.images.get(id);
if (!img) {
res.status(404);
return { success: false, error: 'Image not found' };
}
res.header('Content-Type', img.mime);
return img.img;
}
private async handleRequest(req: FastifyRequest, res: FastifyReply) {
let contentType;
if ((contentType = req.headers["content-type"]) === undefined || !allowedTypes.includes(contentType)) {
res.status(400);
return { success: false, error: "Invalid Content-Type" };
}
private async handleRequest(req: FastifyRequest, res: FastifyReply) {
let contentType;
if ((contentType = req.headers['content-type']) === undefined || !allowedTypes.includes(contentType)) {
res.status(400);
return { success: false, error: 'Invalid Content-Type' };
}
let data = req.body as Buffer;
let data = req.body as Buffer;
// Check MIME
let mime = await fileTypeFromBuffer(data);
// Check MIME
let mime = await fileTypeFromBuffer(data);
if (mime?.mime !== contentType) {
res.status(400);
return { success: false, error: "Image is corrupt" };
}
if (mime?.mime !== contentType) {
res.status(400);
return { success: false, error: 'Image is corrupt' };
}
// Parse and resize if necessary
// Parse and resize if necessary
let sharp, meta;
try {
sharp = Sharp(data);
meta = await sharp.metadata();
} catch {
res.status(400);
return { success: false, error: "Image is corrupt" };
}
let sharp, meta;
try {
sharp = Sharp(data);
meta = await sharp.metadata();
} catch {
res.status(400);
return { success: false, error: 'Image is corrupt' };
}
if (!meta.width || !meta.height) {
res.status(400);
return { success: false, error: "Image is corrupt" };
}
if (!meta.width || !meta.height) {
res.status(400);
return { success: false, error: 'Image is corrupt' };
}
if (meta.width > this.config.maxSize.width || meta.height > this.config.maxSize.height) {
sharp.resize(this.config.maxSize.width, this.config.maxSize.height, { fit: "inside" });
}
if (meta.width > this.config.maxSize.width || meta.height > this.config.maxSize.height) {
sharp.resize(this.config.maxSize.width, this.config.maxSize.height, { fit: 'inside' });
}
let outputImg = await sharp.toBuffer();
let outputImg = await sharp.toBuffer();
// Add to the map
let id;
do {
id = await randomString(16);
} while (this.images.has(id));
// Add to the map
let id;
do {
id = await randomString(16);
} while (this.images.has(id));
let timeout = setTimeout(() => {
this.images.delete(id);
}, this.config.expirySeconds * 1000);
this.images.set(id, { img: outputImg, mime: mime.mime, timeout });
return { success: true, id };
}
let timeout = setTimeout(() => {
this.images.delete(id);
}, this.config.expirySeconds * 1000);
this.images.set(id, { img: outputImg, mime: mime.mime, timeout });
return { success: true, id };
}
has(id: string) {
return this.images.has(id);
}
}
has(id: string) {
return this.images.has(id);
}
}

View file

@ -43,7 +43,7 @@ const app = Fastify({
app.register(FastifyCors, {
origin: config.http.origins,
methods: ['GET', 'POST', 'PUT'],
methods: ['GET', 'POST', 'PUT']
});
app.register(FastifyWS);

View file

@ -114,7 +114,7 @@ export class MSAgentChatRoom {
for (const _client of this.getActiveClients()) {
_client.send(msg);
}
})
});
client.on('admin', () => {
let msg: MSAgentPromoteMessage = {
op: MSAgentProtocolMessageType.Promote,

View file

@ -129,19 +129,30 @@ elements.imageUploadBtn.addEventListener('click', () => {
imgUploadInput.click();
});
document.addEventListener('paste', (e) => {
if (e.clipboardData?.files.length === 0) return;
let file = e.clipboardData!.files[0];
if (!file.type.startsWith('image/')) return;
if (!window.confirm(`Upload ${file.name} (${Math.round(file.size / 1000)}KB)?`)) return;
uploadFile(file);
});
imgUploadInput.addEventListener('change', async () => {
if (!imgUploadInput.files || imgUploadInput.files.length === 0) return;
let file = imgUploadInput.files[0];
uploadFile(imgUploadInput.files[0]);
});
function uploadFile(file: File) {
let reader = new FileReader();
reader.onload = async () => {
let buffer = reader.result as ArrayBuffer;
await Room?.sendImage(buffer, file.type);
};
reader.readAsArrayBuffer(file);
});
}
let w = window as any;
w.agentchat = {
getRoom: () => Room,
}
getRoom: () => Room
};