Compare commits
2 commits
d3aba5c519
...
2de60a98a3
Author | SHA1 | Date | |
---|---|---|---|
2de60a98a3 | |||
1d4a2673b0 |
6 changed files with 110 additions and 104 deletions
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -62,6 +62,6 @@ export interface DiscordConfig {
|
|||
}
|
||||
|
||||
export interface ImagesConfig {
|
||||
maxSize: { width: number, height: number };
|
||||
maxSize: { width: number; height: number };
|
||||
expirySeconds: number;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue