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 {
|
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.
|
// Round the image size up to the nearest 12x12, plus 12px padding.
|
||||||
let size = {
|
let size = {
|
||||||
w: (Math.ceil(img.width / 12) * 12) + 6,
|
w: Math.ceil(img.width / 12) * 12 + 6,
|
||||||
h: (Math.ceil(img.height / 12) * 12) + 6
|
h: Math.ceil(img.height / 12) * 12 + 6
|
||||||
};
|
};
|
||||||
|
|
||||||
// Draw the word balloon and get the inner rect
|
// Draw the word balloon and get the inner rect
|
||||||
|
@ -195,6 +195,6 @@ export function wordballoonDrawImage(ctx: CanvasRenderingContext2D, at: Point, i
|
||||||
x: at.x,
|
x: at.x,
|
||||||
y: at.y,
|
y: at.y,
|
||||||
w: rectInner.w + 12 * 3 + 12,
|
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 {
|
export interface ImagesConfig {
|
||||||
maxSize: { width: number, height: number };
|
maxSize: { width: number; height: number };
|
||||||
expirySeconds: number;
|
expirySeconds: number;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,116 +1,111 @@
|
||||||
import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
|
import { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
|
||||||
import { fileTypeFromBuffer } from "file-type";
|
import { fileTypeFromBuffer } from 'file-type';
|
||||||
import * as crypto from "crypto";
|
import * as crypto from 'crypto';
|
||||||
import Sharp from 'sharp';
|
import Sharp from 'sharp';
|
||||||
import { ImagesConfig } from "./config";
|
import { ImagesConfig } from './config';
|
||||||
|
|
||||||
export interface image {
|
export interface image {
|
||||||
img: Buffer;
|
img: Buffer;
|
||||||
mime: string;
|
mime: string;
|
||||||
timeout: NodeJS.Timeout;
|
timeout: NodeJS.Timeout;
|
||||||
}
|
}
|
||||||
|
|
||||||
const allowedTypes = [
|
const allowedTypes = ['image/png', 'image/jpeg', 'image/gif', 'image/webp'];
|
||||||
"image/png",
|
|
||||||
"image/jpeg",
|
|
||||||
"image/gif",
|
|
||||||
"image/webp",
|
|
||||||
];
|
|
||||||
|
|
||||||
function randomString(length: number): Promise<string> {
|
function randomString(length: number): Promise<string> {
|
||||||
return new Promise((res, rej) => {
|
return new Promise((res, rej) => {
|
||||||
let _len = length;
|
let _len = length;
|
||||||
if (_len % 2 !== 0) _len++;
|
if (_len % 2 !== 0) _len++;
|
||||||
crypto.randomBytes(_len / 2, (err, buf) => {
|
crypto.randomBytes(_len / 2, (err, buf) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
rej(err);
|
rej(err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let out = buf.toString("hex");
|
let out = buf.toString('hex');
|
||||||
if (out.length !== length) out = out.substring(0, length);
|
if (out.length !== length) out = out.substring(0, length);
|
||||||
res(out);
|
res(out);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ImageUploader {
|
export class ImageUploader {
|
||||||
private images: Map<string, image> = new Map();
|
private images: Map<string, image> = new Map();
|
||||||
private config: ImagesConfig;
|
private config: ImagesConfig;
|
||||||
|
|
||||||
constructor(app: FastifyInstance, config: ImagesConfig) {
|
constructor(app: FastifyInstance, config: ImagesConfig) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
for (let type of allowedTypes) {
|
for (let type of allowedTypes) {
|
||||||
// i kinda hate this
|
// i kinda hate this
|
||||||
app.addContentTypeParser(type, {parseAs: "buffer"}, (req, body, done) => done(null, body));
|
app.addContentTypeParser(type, { parseAs: 'buffer' }, (req, body, done) => done(null, body));
|
||||||
}
|
}
|
||||||
app.put("/api/upload", async (req, res) => await this.handleRequest(req, res));
|
app.put('/api/upload', async (req, res) => await this.handleRequest(req, res));
|
||||||
app.get("/api/image/:id", (req, res) => this.handleGet(req, res));
|
app.get('/api/image/:id', (req, res) => this.handleGet(req, res));
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleGet(req: FastifyRequest, res: FastifyReply) {
|
private handleGet(req: FastifyRequest, res: FastifyReply) {
|
||||||
let {id} = req.params as {id: string};
|
let { id } = req.params as { id: string };
|
||||||
let img = this.images.get(id);
|
let img = this.images.get(id);
|
||||||
if (!img) {
|
if (!img) {
|
||||||
res.status(404);
|
res.status(404);
|
||||||
return { success: false, error: "Image not found" };
|
return { success: false, error: 'Image not found' };
|
||||||
}
|
}
|
||||||
res.header("Content-Type", img.mime);
|
res.header('Content-Type', img.mime);
|
||||||
return img.img;
|
return img.img;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleRequest(req: FastifyRequest, res: FastifyReply) {
|
private async handleRequest(req: FastifyRequest, res: FastifyReply) {
|
||||||
let contentType;
|
let contentType;
|
||||||
if ((contentType = req.headers["content-type"]) === undefined || !allowedTypes.includes(contentType)) {
|
if ((contentType = req.headers['content-type']) === undefined || !allowedTypes.includes(contentType)) {
|
||||||
res.status(400);
|
res.status(400);
|
||||||
return { success: false, error: "Invalid Content-Type" };
|
return { success: false, error: 'Invalid Content-Type' };
|
||||||
}
|
}
|
||||||
|
|
||||||
let data = req.body as Buffer;
|
let data = req.body as Buffer;
|
||||||
|
|
||||||
// Check MIME
|
// Check MIME
|
||||||
let mime = await fileTypeFromBuffer(data);
|
let mime = await fileTypeFromBuffer(data);
|
||||||
|
|
||||||
if (mime?.mime !== contentType) {
|
if (mime?.mime !== contentType) {
|
||||||
res.status(400);
|
res.status(400);
|
||||||
return { success: false, error: "Image is corrupt" };
|
return { success: false, error: 'Image is corrupt' };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse and resize if necessary
|
// Parse and resize if necessary
|
||||||
|
|
||||||
let sharp, meta;
|
let sharp, meta;
|
||||||
try {
|
try {
|
||||||
sharp = Sharp(data);
|
sharp = Sharp(data);
|
||||||
meta = await sharp.metadata();
|
meta = await sharp.metadata();
|
||||||
} catch {
|
} catch {
|
||||||
res.status(400);
|
res.status(400);
|
||||||
return { success: false, error: "Image is corrupt" };
|
return { success: false, error: 'Image is corrupt' };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!meta.width || !meta.height) {
|
if (!meta.width || !meta.height) {
|
||||||
res.status(400);
|
res.status(400);
|
||||||
return { success: false, error: "Image is corrupt" };
|
return { success: false, error: 'Image is corrupt' };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (meta.width > this.config.maxSize.width || meta.height > this.config.maxSize.height) {
|
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" });
|
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
|
// Add to the map
|
||||||
let id;
|
let id;
|
||||||
do {
|
do {
|
||||||
id = await randomString(16);
|
id = await randomString(16);
|
||||||
} while (this.images.has(id));
|
} while (this.images.has(id));
|
||||||
|
|
||||||
let timeout = setTimeout(() => {
|
let timeout = setTimeout(() => {
|
||||||
this.images.delete(id);
|
this.images.delete(id);
|
||||||
}, this.config.expirySeconds * 1000);
|
}, this.config.expirySeconds * 1000);
|
||||||
this.images.set(id, { img: outputImg, mime: mime.mime, timeout });
|
this.images.set(id, { img: outputImg, mime: mime.mime, timeout });
|
||||||
return { success: true, id };
|
return { success: true, id };
|
||||||
}
|
}
|
||||||
|
|
||||||
has(id: string) {
|
has(id: string) {
|
||||||
return this.images.has(id);
|
return this.images.has(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,7 +43,7 @@ const app = Fastify({
|
||||||
|
|
||||||
app.register(FastifyCors, {
|
app.register(FastifyCors, {
|
||||||
origin: config.http.origins,
|
origin: config.http.origins,
|
||||||
methods: ['GET', 'POST', 'PUT'],
|
methods: ['GET', 'POST', 'PUT']
|
||||||
});
|
});
|
||||||
|
|
||||||
app.register(FastifyWS);
|
app.register(FastifyWS);
|
||||||
|
|
|
@ -114,7 +114,7 @@ export class MSAgentChatRoom {
|
||||||
for (const _client of this.getActiveClients()) {
|
for (const _client of this.getActiveClients()) {
|
||||||
_client.send(msg);
|
_client.send(msg);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
client.on('admin', () => {
|
client.on('admin', () => {
|
||||||
let msg: MSAgentPromoteMessage = {
|
let msg: MSAgentPromoteMessage = {
|
||||||
op: MSAgentProtocolMessageType.Promote,
|
op: MSAgentProtocolMessageType.Promote,
|
||||||
|
|
|
@ -129,19 +129,30 @@ elements.imageUploadBtn.addEventListener('click', () => {
|
||||||
imgUploadInput.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 () => {
|
imgUploadInput.addEventListener('change', async () => {
|
||||||
if (!imgUploadInput.files || imgUploadInput.files.length === 0) return;
|
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();
|
let reader = new FileReader();
|
||||||
reader.onload = async () => {
|
reader.onload = async () => {
|
||||||
let buffer = reader.result as ArrayBuffer;
|
let buffer = reader.result as ArrayBuffer;
|
||||||
await Room?.sendImage(buffer, file.type);
|
await Room?.sendImage(buffer, file.type);
|
||||||
};
|
};
|
||||||
reader.readAsArrayBuffer(file);
|
reader.readAsArrayBuffer(file);
|
||||||
});
|
}
|
||||||
|
|
||||||
let w = window as any;
|
let w = window as any;
|
||||||
|
|
||||||
w.agentchat = {
|
w.agentchat = {
|
||||||
getRoom: () => Room,
|
getRoom: () => Room
|
||||||
}
|
};
|
||||||
|
|
Loading…
Reference in a new issue