From 8394920d4da4cd0f529077c4179d3c838c582fc5 Mon Sep 17 00:00:00 2001 From: Elijah R Date: Sun, 14 Jul 2024 19:35:00 -0400 Subject: [PATCH] add image upload support --- msagent.js/src/agent.ts | 113 ++++++++---- msagent.js/src/wordballoon.ts | 21 +++ protocol/src/protocol.ts | 16 ++ server/config.example.toml | 9 + server/package.json | 3 + server/src/client.ts | 8 + server/src/config.ts | 7 + server/src/imageuploader.ts | 116 ++++++++++++ server/src/index.ts | 15 +- server/src/room.ts | 21 ++- webapp/src/html/index.html | 1 + webapp/src/ts/client.ts | 50 ++++- webapp/src/ts/main.ts | 19 ++ yarn.lock | 338 +++++++++++++++++++++++++++++++++- 14 files changed, 697 insertions(+), 40 deletions(-) create mode 100644 server/src/imageuploader.ts diff --git a/msagent.js/src/agent.ts b/msagent.js/src/agent.ts index c71399f..78ea25e 100644 --- a/msagent.js/src/agent.ts +++ b/msagent.js/src/agent.ts @@ -4,7 +4,7 @@ import { ContextMenu, ContextMenuItem } from './contextmenu.js'; import { AcsAnimation, AcsAnimationFrameInfo } from './structs/animation.js'; import { AcsImageEntry } from './structs/image.js'; import { Point, Size } from './types.js'; -import { wordballoonDrawText } from './wordballoon.js'; +import { wordballoonDrawImage, wordballoonDrawText } from './wordballoon.js'; // probably should be in a utility module function dwAlign(off: number): number { @@ -61,44 +61,19 @@ enum AgentWordBalloonPosition { BelowCentered } -class AgentWordBalloonState { - char: Agent; - text: string; - hasTip: boolean; - position: AgentWordBalloonPosition; +abstract class AgentWordBalloonState { + abstract char: Agent; + abstract hasTip: boolean; + abstract position: AgentWordBalloonPosition; balloonCanvas: HTMLCanvasElement; balloonCanvasCtx: CanvasRenderingContext2D; - constructor(char: Agent, text: string, hasTip: boolean, position: AgentWordBalloonPosition, textColor: string) { - this.char = char; - this.text = text; - this.hasTip = hasTip; - this.position = position; + constructor() { this.balloonCanvas = document.createElement('canvas'); this.balloonCanvasCtx = this.balloonCanvas.getContext('2d')!; - this.balloonCanvas.style.position = 'absolute'; - - this.balloonCanvasCtx.font = '14px arial'; - - this.balloonCanvas.width = 300; - this.balloonCanvas.height = 300; - - // hack fix for above this.balloonCanvas.style.pointerEvents = 'none'; - - let rect = wordballoonDrawText(this.balloonCanvasCtx, { x: 0, y: 0 }, this.text, 20, hasTip, textColor); - - // Second pass, actually set the element to the right width and stuffs - this.balloonCanvas.width = rect.w; - this.balloonCanvas.height = rect.h; - - wordballoonDrawText(this.balloonCanvasCtx, { x: 0, y: 0 }, this.text, 20, hasTip, textColor); - - this.char.getElement().appendChild(this.balloonCanvas); - - this.show(); } show() { @@ -129,6 +104,66 @@ class AgentWordBalloonState { } } +class AgentTextWordBalloonState extends AgentWordBalloonState { + char: Agent; + text: string; + hasTip: boolean; + position: AgentWordBalloonPosition; + + constructor(char: Agent, text: string, hasTip: boolean, position: AgentWordBalloonPosition, textColor: string) { + super(); + this.char = char; + this.text = text; + this.hasTip = hasTip; + this.position = position; + this.balloonCanvasCtx.font = '14px arial'; + + this.balloonCanvas.width = 300; + this.balloonCanvas.height = 300; + + let rect = wordballoonDrawText(this.balloonCanvasCtx, { x: 0, y: 0 }, this.text, 20, hasTip, textColor); + + // Second pass, actually set the element to the right width and stuffs + this.balloonCanvas.width = rect.w; + this.balloonCanvas.height = rect.h; + + wordballoonDrawText(this.balloonCanvasCtx, { x: 0, y: 0 }, this.text, 20, hasTip, textColor); + + this.char.getElement().appendChild(this.balloonCanvas); + + this.show(); + } +} + +class AgentImageWordBalloonState extends AgentWordBalloonState { + char: Agent; + img: HTMLImageElement; + hasTip: boolean; + position: AgentWordBalloonPosition; + + constructor(char: Agent, img: HTMLImageElement, hasTip: boolean, position: AgentWordBalloonPosition) { + super(); + this.char = char; + this.img = img; + this.hasTip = hasTip; + this.position = position; + + this.balloonCanvas.width = 300; + this.balloonCanvas.height = 300; + + let rect = wordballoonDrawImage(this.balloonCanvasCtx, { x: 0, y: 0 }, this.img, hasTip); + + // Second pass, actually set the element to the right width and stuffs + this.balloonCanvas.width = rect.w; + this.balloonCanvas.height = rect.h; + + wordballoonDrawImage(this.balloonCanvasCtx, { x: 0, y: 0 }, this.img, hasTip); + + this.char.getElement().appendChild(this.balloonCanvas); + this.show(); + } +} + export class Agent { private data: AcsData; private charDiv: HTMLDivElement; @@ -141,7 +176,7 @@ export class Agent { private animState: AgentAnimationState | null = null; private wordballoonState: AgentWordBalloonState | null = null; - private usernameBalloonState: AgentWordBalloonState | null = null; + private usernameBalloonState: AgentTextWordBalloonState | null = null; private contextMenu: ContextMenu; @@ -299,7 +334,7 @@ export class Agent { this.usernameBalloonState = null; } - this.usernameBalloonState = new AgentWordBalloonState(this, username, false, AgentWordBalloonPosition.BelowCentered, color); + this.usernameBalloonState = new AgentTextWordBalloonState(this, username, false, AgentWordBalloonPosition.BelowCentered, color); this.usernameBalloonState.show(); } @@ -308,7 +343,17 @@ export class Agent { this.stopSpeaking(); } - this.wordballoonState = new AgentWordBalloonState(this, text, true, AgentWordBalloonPosition.AboveCentered, '#000000'); + this.wordballoonState = new AgentTextWordBalloonState(this, text, true, AgentWordBalloonPosition.AboveCentered, '#000000'); + this.wordballoonState.positionUpdated(); + this.wordballoonState.show(); + } + + speakImage(img: HTMLImageElement) { + if (this.wordballoonState != null) { + this.stopSpeaking(); + } + + this.wordballoonState = new AgentImageWordBalloonState(this, img, true, AgentWordBalloonPosition.AboveCentered); this.wordballoonState.positionUpdated(); this.wordballoonState.show(); } diff --git a/msagent.js/src/wordballoon.ts b/msagent.js/src/wordballoon.ts index 6c12f88..2f41683 100644 --- a/msagent.js/src/wordballoon.ts +++ b/msagent.js/src/wordballoon.ts @@ -177,3 +177,24 @@ export function wordballoonDrawText(ctx: CanvasRenderingContext2D, at: Point, te h: rectInner.h + 13 * 3 + 18 }; } + +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 + }; + + // Draw the word balloon and get the inner rect + let rectInner = wordballoonDraw(ctx, at, size, hasTip); + + // Draw the image + ctx.drawImage(img, 6, 6); + + return { + x: at.x, + y: at.y, + w: rectInner.w + 12 * 3 + 12, + h: rectInner.h + 13 * 3 + 18, + }; +} \ No newline at end of file diff --git a/protocol/src/protocol.ts b/protocol/src/protocol.ts index bacbf56..780a26a 100644 --- a/protocol/src/protocol.ts +++ b/protocol/src/protocol.ts @@ -5,6 +5,7 @@ export enum MSAgentProtocolMessageType { KeepAlive = 'nop', Join = 'join', Talk = 'talk', + SendImage = 'img', Admin = 'admin', // Server-to-client Init = 'init', @@ -36,6 +37,13 @@ export interface MSAgentTalkMessage extends MSAgentProtocolMessage { }; } +export interface MSAgentSendImageMessage extends MSAgentProtocolMessage { + op: MSAgentProtocolMessageType.SendImage; + data: { + id: string; + }; +} + // Server-to-client export interface MSAgentInitMessage extends MSAgentProtocolMessage { @@ -76,6 +84,14 @@ export interface MSAgentChatMessage extends MSAgentProtocolMessage { }; } +export interface MSAgentImageMessage extends MSAgentProtocolMessage { + op: MSAgentProtocolMessageType.SendImage; + data: { + username: string; + id: string; + }; +} + export interface MSAgentPromoteMessage extends MSAgentProtocolMessage { op: MSAgentProtocolMessageType.Promote; data: { diff --git a/server/config.example.toml b/server/config.example.toml index 6d41b59..e426a33 100644 --- a/server/config.example.toml +++ b/server/config.example.toml @@ -2,6 +2,11 @@ host = "127.0.0.1" port = 3000 proxied = true +# Allowed CORS origins. +# true = All origins allowed (not recommended in production) +# false = Cross-origin requests are not allowed +# String or array of strings = Only the specified origin(s) are allowed +origins = true [mysql] host = "127.0.0.1" @@ -46,6 +51,10 @@ tempDir = "/tmp/msac-tts" transcodeOpus = true wavExpirySeconds = 60 +[images] +maxSize = { width = 300, height = 300 } +expirySeconds = 60 + [[agents]] friendlyName = "Clippy" filename = "CLIPPIT.ACS" diff --git a/server/package.json b/server/package.json index 2aea996..aca74e8 100644 --- a/server/package.json +++ b/server/package.json @@ -12,13 +12,16 @@ "typescript": "5.4.5" }, "dependencies": { + "@fastify/cors": "^9.0.1", "@fastify/static": "^7.0.4", "@fastify/websocket": "^10.0.1", "discord.js": "^14.15.3", "fastify": "^4.28.1", + "file-type": "^19.1.1", "fluent-ffmpeg": "^2.1.3", "html-entities": "^2.5.2", "mysql2": "^3.10.2", + "sharp": "^0.33.4", "toml": "^3.0.0", "ws": "^8.17.1" }, diff --git a/server/src/client.ts b/server/src/client.ts index b3e3579..bc10017 100644 --- a/server/src/client.ts +++ b/server/src/client.ts @@ -10,6 +10,7 @@ import { MSAgentAdminMessage, MSAgentAdminOperation, MSAgentErrorMessage, + MSAgentSendImageMessage, MSAgentJoinMessage, MSAgentProtocolMessage, MSAgentProtocolMessageType, @@ -26,6 +27,7 @@ export interface Client { on(event: 'join', listener: () => void): this; on(event: 'close', listener: () => void): this; on(event: 'talk', listener: (msg: string) => void): this; + on(event: 'image', listener: (id: string) => void): this; on(event: string, listener: Function): this; } @@ -160,6 +162,12 @@ export class Client extends EventEmitter { this.emit('talk', talkMsg.data.msg); break; } + case MSAgentProtocolMessageType.SendImage: { + let imgMsg = msg as MSAgentSendImageMessage; + if (!imgMsg.data || !imgMsg.data.id || !this.chatRateLimit.request()) return; + this.emit('image', imgMsg.data.id); + break; + } case MSAgentProtocolMessageType.Admin: { let adminMsg = msg as MSAgentAdminMessage; if (!adminMsg.data) return; diff --git a/server/src/config.ts b/server/src/config.ts index c407344..1d7c7ca 100644 --- a/server/src/config.ts +++ b/server/src/config.ts @@ -3,12 +3,14 @@ export interface IConfig { host: string; port: number; proxied: boolean; + origins: string | string[] | boolean; }; mysql: MySQLConfig; chat: ChatConfig; motd: motdConfig; discord: DiscordConfig; tts: TTSConfig; + images: ImagesConfig; agents: AgentConfig[]; } @@ -58,3 +60,8 @@ export interface DiscordConfig { enabled: boolean; webhookURL: string; } + +export interface ImagesConfig { + maxSize: { width: number, height: number }; + expirySeconds: number; +} \ No newline at end of file diff --git a/server/src/imageuploader.ts b/server/src/imageuploader.ts new file mode 100644 index 0000000..1175dff --- /dev/null +++ b/server/src/imageuploader.ts @@ -0,0 +1,116 @@ +import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; +import { fileTypeFromBuffer } from "file-type"; +import * as crypto from "crypto"; +import Sharp from 'sharp'; +import { ImagesConfig } from "./config"; + +export interface image { + img: Buffer; + mime: string; + timeout: NodeJS.Timeout; +} + +const allowedTypes = [ + "image/png", + "image/jpeg", + "image/gif", + "image/webp", +]; + +function randomString(length: number): Promise { + 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 = 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)); + } + + 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" }; + } + + let data = req.body as Buffer; + + // Check MIME + let mime = await fileTypeFromBuffer(data); + + if (mime?.mime !== contentType) { + res.status(400); + return { success: false, error: "Image is corrupt" }; + } + + // 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" }; + } + + 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" }); + } + + let outputImg = await sharp.toBuffer(); + + // 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 }; + } + + has(id: string) { + return this.images.has(id); + } +} \ No newline at end of file diff --git a/server/src/index.ts b/server/src/index.ts index baed029..6d6f6ab 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -1,6 +1,7 @@ import Fastify from 'fastify'; import FastifyWS from '@fastify/websocket'; import FastifyStatic from '@fastify/static'; +import FastifyCors from '@fastify/cors'; import { Client } from './client.js'; import { MSAgentChatRoom } from './room.js'; import * as toml from 'toml'; @@ -12,6 +13,7 @@ import { isIP } from 'net'; import { Database } from './database.js'; import { MSAgentErrorMessage, MSAgentProtocolMessageType } from '@msagent-chat/protocol'; import { DiscordLogger } from './discord.js'; +import { ImageUploader } from './imageuploader.js'; let config: IConfig; let configPath: string; @@ -35,7 +37,13 @@ let db = new Database(config.mysql); await db.init(); const app = Fastify({ - logger: true + logger: true, + bodyLimit: 20971520 +}); + +app.register(FastifyCors, { + origin: config.http.origins, + methods: ['GET', 'POST', 'PUT'], }); app.register(FastifyWS); @@ -92,7 +100,10 @@ if (config.discord.enabled) { discord = new DiscordLogger(config.discord); } -let room = new MSAgentChatRoom(config.chat, config.agents, db, tts, discord); +// Image upload +let img = new ImageUploader(app, config.images); + +let room = new MSAgentChatRoom(config.chat, config.agents, db, img, tts, discord); app.register(async (app) => { app.get('/api/socket', { websocket: true }, async (socket, req) => { diff --git a/server/src/room.ts b/server/src/room.ts index 08721e4..6bcfb2f 100644 --- a/server/src/room.ts +++ b/server/src/room.ts @@ -1,6 +1,7 @@ import { MSAgentAddUserMessage, MSAgentChatMessage, + MSAgentImageMessage, MSAgentInitMessage, MSAgentPromoteMessage, MSAgentProtocolMessage, @@ -13,6 +14,7 @@ import { AgentConfig, ChatConfig } from './config.js'; import * as htmlentities from 'html-entities'; import { Database } from './database.js'; import { DiscordLogger } from './discord.js'; +import { ImageUploader } from './imageuploader.js'; export class MSAgentChatRoom { agents: AgentConfig[]; @@ -21,14 +23,16 @@ export class MSAgentChatRoom { msgId: number = 0; config: ChatConfig; db: Database; + img: ImageUploader; discord: DiscordLogger | null; - constructor(config: ChatConfig, agents: AgentConfig[], db: Database, tts: TTSClient | null, discord: DiscordLogger | null) { + constructor(config: ChatConfig, agents: AgentConfig[], db: Database, img: ImageUploader, tts: TTSClient | null, discord: DiscordLogger | null) { this.agents = agents; this.clients = []; this.config = config; this.tts = tts; this.db = db; + this.img = img; this.discord = discord; } @@ -96,6 +100,21 @@ export class MSAgentChatRoom { } this.discord?.logMsg(client.username!, message); }); + client.on('image', async (id) => { + if (!this.img.has(id)) return; + + let msg: MSAgentImageMessage = { + op: MSAgentProtocolMessageType.SendImage, + data: { + username: client.username!, + id: id + } + }; + + for (const _client of this.getActiveClients()) { + _client.send(msg); + } + }) client.on('admin', () => { let msg: MSAgentPromoteMessage = { op: MSAgentProtocolMessageType.Promote, diff --git a/webapp/src/html/index.html b/webapp/src/html/index.html index 96159a3..c2d3094 100644 --- a/webapp/src/html/index.html +++ b/webapp/src/html/index.html @@ -122,6 +122,7 @@
+
diff --git a/webapp/src/ts/client.ts b/webapp/src/ts/client.ts index 949e310..4790e0d 100644 --- a/webapp/src/ts/client.ts +++ b/webapp/src/ts/client.ts @@ -11,12 +11,14 @@ import { MSAgentAdminOperation, MSAgentChatMessage, MSAgentErrorMessage, + MSAgentImageMessage, MSAgentInitMessage, MSAgentJoinMessage, MSAgentPromoteMessage, MSAgentProtocolMessage, MSAgentProtocolMessageType, MSAgentRemoveUserMessage, + MSAgentSendImageMessage, MSAgentTalkMessage } from '@msagent-chat/protocol'; import { User } from './user'; @@ -49,6 +51,7 @@ export class MSAgentClient { private charlimit: number = 0; private admin: boolean; private loginCb: (e: KeyboardEvent) => void; + private currentMsgId: number = 0; private username: string | null = null; private agentContainer: HTMLElement; @@ -172,6 +175,31 @@ export class MSAgentClient { this.send(talkMsg); } + async sendImage(img: ArrayBuffer, type: string) { + // Upload image + let res = await fetch(this.url + '/api/upload', { + method: 'PUT', + body: img, + headers: { + 'Content-Type': type + } + }); + let json = await res.json(); + if (!json.success) { + throw new Error('Failed to upload image: ' + json.error); + } + let id = json.id as string; + + // Send image + let msg: MSAgentSendImageMessage = { + op: MSAgentProtocolMessageType.SendImage, + data: { + id + } + }; + this.send(msg); + } + getCharlimit() { return this.charlimit; } @@ -317,10 +345,12 @@ export class MSAgentClient { this.playingAudio.set(user!.username, audio); + let msgId = ++this.currentMsgId; + audio.addEventListener('ended', () => { // give a bit of time before the wordballoon disappears setTimeout(() => { - if (this.playingAudio.get(user!.username) === audio) { + if (this.currentMsgId === msgId) { user!.agent.stopSpeaking(); this.playingAudio.delete(user!.username); } @@ -332,6 +362,24 @@ export class MSAgentClient { } break; } + case MSAgentProtocolMessageType.SendImage: { + let imgMsg = msg as MSAgentImageMessage; + let user = this.users.find((u) => u.username === imgMsg.data.username); + if (!user || user.muted) return; + let img = new Image(); + let msgId = ++this.currentMsgId; + img.addEventListener('load', () => { + this.playingAudio.get(user.username)?.pause(); + user.agent.speakImage(img); + setTimeout(() => { + if (this.currentMsgId === msgId) { + user.agent.stopSpeaking(); + } + }, 5000); + }); + img.src = `${this.url}/api/image/${imgMsg.data.id}`; + break; + } case MSAgentProtocolMessageType.Admin: { let adminMsg = msg as MSAgentAdminMessage; switch (adminMsg.data.action) { diff --git a/webapp/src/ts/main.ts b/webapp/src/ts/main.ts index 0f5fe53..f3822f0 100644 --- a/webapp/src/ts/main.ts +++ b/webapp/src/ts/main.ts @@ -17,6 +17,7 @@ const elements = { chatView: document.getElementById('chatView') as HTMLDivElement, chatInput: document.getElementById('chatInput') as HTMLInputElement, + imageUploadBtn: document.getElementById('imageUploadBtn') as HTMLButtonElement, chatSendBtn: document.getElementById('chatSendBtn') as HTMLButtonElement, roomSettingsWindow: document.getElementById('roomSettingsWindow') as HTMLDivElement @@ -120,6 +121,24 @@ function talk() { roomInit(); +let imgUploadInput = document.createElement('input'); +imgUploadInput.type = 'file'; +imgUploadInput.accept = 'image/jpeg,image/png,image/gif,image/webp'; + +elements.imageUploadBtn.addEventListener('click', () => { + imgUploadInput.click(); +}); + +imgUploadInput.addEventListener('change', async () => { + if (!imgUploadInput.files || imgUploadInput.files.length === 0) return; + let file = imgUploadInput.files[0]; + 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; diff --git a/yarn.lock b/yarn.lock index 05b9ab2..84f9c3d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -113,6 +113,15 @@ __metadata: languageName: node linkType: hard +"@emnapi/runtime@npm:^1.1.1": + version: 1.2.0 + resolution: "@emnapi/runtime@npm:1.2.0" + dependencies: + tslib: "npm:^2.4.0" + checksum: 10c0/7005ff8b67724c9e61b6cd79a3decbdb2ce25d24abd4d3d187472f200ee6e573329c30264335125fb136bd813aa9cf9f4f7c9391d04b07dd1e63ce0a3427be57 + languageName: node + linkType: hard + "@fastify/accept-negotiator@npm:^1.0.0": version: 1.1.0 resolution: "@fastify/accept-negotiator@npm:1.1.0" @@ -131,6 +140,16 @@ __metadata: languageName: node linkType: hard +"@fastify/cors@npm:^9.0.1": + version: 9.0.1 + resolution: "@fastify/cors@npm:9.0.1" + dependencies: + fastify-plugin: "npm:^4.0.0" + mnemonist: "npm:0.39.6" + checksum: 10c0/4db9d3d02edbca741c8ed053819bf3b235ecd70e07c640ed91ba0fc1ee2dc8abedbbffeb79ae1a38ccbf59832e414cad90a554ee44227d0811d5a2d062940611 + languageName: node + linkType: hard + "@fastify/error@npm:^3.3.0, @fastify/error@npm:^3.4.0": version: 3.4.1 resolution: "@fastify/error@npm:3.4.1" @@ -194,6 +213,181 @@ __metadata: languageName: node linkType: hard +"@img/sharp-darwin-arm64@npm:0.33.4": + version: 0.33.4 + resolution: "@img/sharp-darwin-arm64@npm:0.33.4" + dependencies: + "@img/sharp-libvips-darwin-arm64": "npm:1.0.2" + dependenciesMeta: + "@img/sharp-libvips-darwin-arm64": + optional: true + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@img/sharp-darwin-x64@npm:0.33.4": + version: 0.33.4 + resolution: "@img/sharp-darwin-x64@npm:0.33.4" + dependencies: + "@img/sharp-libvips-darwin-x64": "npm:1.0.2" + dependenciesMeta: + "@img/sharp-libvips-darwin-x64": + optional: true + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@img/sharp-libvips-darwin-arm64@npm:1.0.2": + version: 1.0.2 + resolution: "@img/sharp-libvips-darwin-arm64@npm:1.0.2" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@img/sharp-libvips-darwin-x64@npm:1.0.2": + version: 1.0.2 + resolution: "@img/sharp-libvips-darwin-x64@npm:1.0.2" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@img/sharp-libvips-linux-arm64@npm:1.0.2": + version: 1.0.2 + resolution: "@img/sharp-libvips-linux-arm64@npm:1.0.2" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-libvips-linux-arm@npm:1.0.2": + version: 1.0.2 + resolution: "@img/sharp-libvips-linux-arm@npm:1.0.2" + conditions: os=linux & cpu=arm & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-libvips-linux-s390x@npm:1.0.2": + version: 1.0.2 + resolution: "@img/sharp-libvips-linux-s390x@npm:1.0.2" + conditions: os=linux & cpu=s390x & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-libvips-linux-x64@npm:1.0.2": + version: 1.0.2 + resolution: "@img/sharp-libvips-linux-x64@npm:1.0.2" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-libvips-linuxmusl-arm64@npm:1.0.2": + version: 1.0.2 + resolution: "@img/sharp-libvips-linuxmusl-arm64@npm:1.0.2" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@img/sharp-libvips-linuxmusl-x64@npm:1.0.2": + version: 1.0.2 + resolution: "@img/sharp-libvips-linuxmusl-x64@npm:1.0.2" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@img/sharp-linux-arm64@npm:0.33.4": + version: 0.33.4 + resolution: "@img/sharp-linux-arm64@npm:0.33.4" + dependencies: + "@img/sharp-libvips-linux-arm64": "npm:1.0.2" + dependenciesMeta: + "@img/sharp-libvips-linux-arm64": + optional: true + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-linux-arm@npm:0.33.4": + version: 0.33.4 + resolution: "@img/sharp-linux-arm@npm:0.33.4" + dependencies: + "@img/sharp-libvips-linux-arm": "npm:1.0.2" + dependenciesMeta: + "@img/sharp-libvips-linux-arm": + optional: true + conditions: os=linux & cpu=arm & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-linux-s390x@npm:0.33.4": + version: 0.33.4 + resolution: "@img/sharp-linux-s390x@npm:0.33.4" + dependencies: + "@img/sharp-libvips-linux-s390x": "npm:1.0.2" + dependenciesMeta: + "@img/sharp-libvips-linux-s390x": + optional: true + conditions: os=linux & cpu=s390x & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-linux-x64@npm:0.33.4": + version: 0.33.4 + resolution: "@img/sharp-linux-x64@npm:0.33.4" + dependencies: + "@img/sharp-libvips-linux-x64": "npm:1.0.2" + dependenciesMeta: + "@img/sharp-libvips-linux-x64": + optional: true + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-linuxmusl-arm64@npm:0.33.4": + version: 0.33.4 + resolution: "@img/sharp-linuxmusl-arm64@npm:0.33.4" + dependencies: + "@img/sharp-libvips-linuxmusl-arm64": "npm:1.0.2" + dependenciesMeta: + "@img/sharp-libvips-linuxmusl-arm64": + optional: true + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@img/sharp-linuxmusl-x64@npm:0.33.4": + version: 0.33.4 + resolution: "@img/sharp-linuxmusl-x64@npm:0.33.4" + dependencies: + "@img/sharp-libvips-linuxmusl-x64": "npm:1.0.2" + dependenciesMeta: + "@img/sharp-libvips-linuxmusl-x64": + optional: true + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@img/sharp-wasm32@npm:0.33.4": + version: 0.33.4 + resolution: "@img/sharp-wasm32@npm:0.33.4" + dependencies: + "@emnapi/runtime": "npm:^1.1.1" + conditions: cpu=wasm32 + languageName: node + linkType: hard + +"@img/sharp-win32-ia32@npm:0.33.4": + version: 0.33.4 + resolution: "@img/sharp-win32-ia32@npm:0.33.4" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@img/sharp-win32-x64@npm:0.33.4": + version: 0.33.4 + resolution: "@img/sharp-win32-x64@npm:0.33.4" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@isaacs/cliui@npm:^8.0.2": version: 8.0.2 resolution: "@isaacs/cliui@npm:8.0.2" @@ -306,6 +500,7 @@ __metadata: version: 0.0.0-use.local resolution: "@msagent-chat/server@workspace:server" dependencies: + "@fastify/cors": "npm:^9.0.1" "@fastify/static": "npm:^7.0.4" "@fastify/websocket": "npm:^10.0.1" "@types/fluent-ffmpeg": "npm:^2.1.24" @@ -313,9 +508,11 @@ __metadata: "@types/ws": "npm:^8.5.10" discord.js: "npm:^14.15.3" fastify: "npm:^4.28.1" + file-type: "npm:^19.1.1" fluent-ffmpeg: "npm:^2.1.3" html-entities: "npm:^2.5.2" mysql2: "npm:^3.10.2" + sharp: "npm:^0.33.4" toml: "npm:^3.0.0" typescript: "npm:5.4.5" ws: "npm:^8.17.1" @@ -1453,6 +1650,13 @@ __metadata: languageName: node linkType: hard +"@tokenizer/token@npm:^0.3.0": + version: 0.3.0 + resolution: "@tokenizer/token@npm:0.3.0" + checksum: 10c0/7ab9a822d4b5ff3f5bca7f7d14d46bdd8432528e028db4a52be7fbf90c7f495cc1af1324691dda2813c6af8dc4b8eb29de3107d4508165f9aa5b53e7d501f155 + languageName: node + linkType: hard + "@trysound/sax@npm:0.2.0": version: 0.2.0 resolution: "@trysound/sax@npm:0.2.0" @@ -2065,7 +2269,7 @@ __metadata: languageName: node linkType: hard -"detect-libc@npm:^2.0.0, detect-libc@npm:^2.0.1": +"detect-libc@npm:^2.0.0, detect-libc@npm:^2.0.1, detect-libc@npm:^2.0.3": version: 2.0.3 resolution: "detect-libc@npm:2.0.3" checksum: 10c0/88095bda8f90220c95f162bf92cad70bd0e424913e655c20578600e35b91edc261af27531cf160a331e185c0ced93944bc7e09939143225f56312d7fd800fdb7 @@ -2401,6 +2605,17 @@ __metadata: languageName: node linkType: hard +"file-type@npm:^19.1.1": + version: 19.1.1 + resolution: "file-type@npm:19.1.1" + dependencies: + strtok3: "npm:^7.1.0" + token-types: "npm:^6.0.0" + uint8array-extras: "npm:^1.3.0" + checksum: 10c0/621f6f5ac9f2c0fad1c5ac0a77d715953fbe7e5441e595443857b7aede0ae8dfb80e29b4f1d7000c6bca49a6e1d5c711c5265fd3d6f81c40361fc61a0ba415c4 + languageName: node + linkType: hard + "fill-range@npm:^7.1.1": version: 7.1.1 resolution: "fill-range@npm:7.1.1" @@ -3273,6 +3488,15 @@ __metadata: languageName: node linkType: hard +"mnemonist@npm:0.39.6": + version: 0.39.6 + resolution: "mnemonist@npm:0.39.6" + dependencies: + obliterator: "npm:^2.0.1" + checksum: 10c0/a538945ea547976136ee6e16f224c0a50983143619941f6c4d2c82159e36eb6f8ee93d69d3a1267038fc5b16f88e2d43390023de10dfb145fa15c5e2befa1cdf + languageName: node + linkType: hard + "ms@npm:2.1.2": version: 2.1.2 resolution: "ms@npm:2.1.2" @@ -3504,6 +3728,13 @@ __metadata: languageName: node linkType: hard +"obliterator@npm:^2.0.1": + version: 2.0.4 + resolution: "obliterator@npm:2.0.4" + checksum: 10c0/ff2c10d4de7d62cd1d588b4d18dfc42f246c9e3a259f60d5716f7f88e5b3a3f79856b3207db96ec9a836a01d0958a21c15afa62a3f4e73a1e0b75f2c2f6bab40 + languageName: node + linkType: hard + "on-exit-leak-free@npm:^2.1.0": version: 2.1.2 resolution: "on-exit-leak-free@npm:2.1.2" @@ -3605,6 +3836,13 @@ __metadata: languageName: node linkType: hard +"peek-readable@npm:^5.1.1": + version: 5.1.2 + resolution: "peek-readable@npm:5.1.2" + checksum: 10c0/015d7f50c17f07efde09a1507d4be67ff01d5819533bbfdbb79232f280aca81ea34053988f49ce9f0e77876487182bbd23ce9178aabe58515f06a91df0b028f0 + languageName: node + linkType: hard + "picocolors@npm:^1.0.0, picocolors@npm:^1.0.1": version: 1.0.1 resolution: "picocolors@npm:1.0.1" @@ -3967,7 +4205,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:^7.3.5, semver@npm:^7.3.8, semver@npm:^7.5.2, semver@npm:^7.5.4": +"semver@npm:^7.3.5, semver@npm:^7.3.8, semver@npm:^7.5.2, semver@npm:^7.5.4, semver@npm:^7.6.0": version: 7.6.2 resolution: "semver@npm:7.6.2" bin: @@ -4014,6 +4252,75 @@ __metadata: languageName: node linkType: hard +"sharp@npm:^0.33.4": + version: 0.33.4 + resolution: "sharp@npm:0.33.4" + dependencies: + "@img/sharp-darwin-arm64": "npm:0.33.4" + "@img/sharp-darwin-x64": "npm:0.33.4" + "@img/sharp-libvips-darwin-arm64": "npm:1.0.2" + "@img/sharp-libvips-darwin-x64": "npm:1.0.2" + "@img/sharp-libvips-linux-arm": "npm:1.0.2" + "@img/sharp-libvips-linux-arm64": "npm:1.0.2" + "@img/sharp-libvips-linux-s390x": "npm:1.0.2" + "@img/sharp-libvips-linux-x64": "npm:1.0.2" + "@img/sharp-libvips-linuxmusl-arm64": "npm:1.0.2" + "@img/sharp-libvips-linuxmusl-x64": "npm:1.0.2" + "@img/sharp-linux-arm": "npm:0.33.4" + "@img/sharp-linux-arm64": "npm:0.33.4" + "@img/sharp-linux-s390x": "npm:0.33.4" + "@img/sharp-linux-x64": "npm:0.33.4" + "@img/sharp-linuxmusl-arm64": "npm:0.33.4" + "@img/sharp-linuxmusl-x64": "npm:0.33.4" + "@img/sharp-wasm32": "npm:0.33.4" + "@img/sharp-win32-ia32": "npm:0.33.4" + "@img/sharp-win32-x64": "npm:0.33.4" + color: "npm:^4.2.3" + detect-libc: "npm:^2.0.3" + semver: "npm:^7.6.0" + dependenciesMeta: + "@img/sharp-darwin-arm64": + optional: true + "@img/sharp-darwin-x64": + optional: true + "@img/sharp-libvips-darwin-arm64": + optional: true + "@img/sharp-libvips-darwin-x64": + optional: true + "@img/sharp-libvips-linux-arm": + optional: true + "@img/sharp-libvips-linux-arm64": + optional: true + "@img/sharp-libvips-linux-s390x": + optional: true + "@img/sharp-libvips-linux-x64": + optional: true + "@img/sharp-libvips-linuxmusl-arm64": + optional: true + "@img/sharp-libvips-linuxmusl-x64": + optional: true + "@img/sharp-linux-arm": + optional: true + "@img/sharp-linux-arm64": + optional: true + "@img/sharp-linux-s390x": + optional: true + "@img/sharp-linux-x64": + optional: true + "@img/sharp-linuxmusl-arm64": + optional: true + "@img/sharp-linuxmusl-x64": + optional: true + "@img/sharp-wasm32": + optional: true + "@img/sharp-win32-ia32": + optional: true + "@img/sharp-win32-x64": + optional: true + checksum: 10c0/428c5c6a84ff8968effe50c2de931002f5f30b9f263e1c026d0384e581673c13088a49322f7748114d3d9be4ae9476a74bf003a3af34743e97ef2f880d1cfe45 + languageName: node + linkType: hard + "shebang-command@npm:^2.0.0": version: 2.0.0 resolution: "shebang-command@npm:2.0.0" @@ -4229,6 +4536,16 @@ __metadata: languageName: node linkType: hard +"strtok3@npm:^7.1.0": + version: 7.1.0 + resolution: "strtok3@npm:7.1.0" + dependencies: + "@tokenizer/token": "npm:^0.3.0" + peek-readable: "npm:^5.1.1" + checksum: 10c0/7a67ba59d348c2e8afe5d9b6de03e8c7527bb348e2d6b7abd3b7277839d9a36c9f1c0e49b89f3b5b32e11865f2c078bf3d14c567e962c1bf458df4ac203216ca + languageName: node + linkType: hard + "supports-color@npm:^5.3.0": version: 5.5.0 resolution: "supports-color@npm:5.5.0" @@ -4349,6 +4666,16 @@ __metadata: languageName: node linkType: hard +"token-types@npm:^6.0.0": + version: 6.0.0 + resolution: "token-types@npm:6.0.0" + dependencies: + "@tokenizer/token": "npm:^0.3.0" + ieee754: "npm:^1.2.1" + checksum: 10c0/5bf5eba51d63f71f301659ff70ce10ca43e7038364883437d8b4541cc98377e3e56109b11720e25fe51047014efaccdff90eaf6de9a78270483578814b838ab9 + languageName: node + linkType: hard + "toml@npm:^3.0.0": version: 3.0.0 resolution: "toml@npm:3.0.0" @@ -4433,6 +4760,13 @@ __metadata: languageName: node linkType: hard +"uint8array-extras@npm:^1.3.0": + version: 1.3.0 + resolution: "uint8array-extras@npm:1.3.0" + checksum: 10c0/facc6eedc38f9db4879c3d60ab5c8d89c7be7b9487f1c812cdfa46194517b104de844a2128875cbdc9af3d4f52ac94c816ed6bb825f5184f270cfef9269fdd82 + languageName: node + linkType: hard + "undici-types@npm:~5.26.4": version: 5.26.5 resolution: "undici-types@npm:5.26.5"