add image upload support

This commit is contained in:
Elijah R 2024-07-14 19:35:00 -04:00
parent 653ce2e413
commit 8394920d4d
14 changed files with 697 additions and 40 deletions

View file

@ -4,7 +4,7 @@ import { ContextMenu, ContextMenuItem } from './contextmenu.js';
import { AcsAnimation, AcsAnimationFrameInfo } from './structs/animation.js'; import { AcsAnimation, AcsAnimationFrameInfo } from './structs/animation.js';
import { AcsImageEntry } from './structs/image.js'; import { AcsImageEntry } from './structs/image.js';
import { Point, Size } from './types.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 // probably should be in a utility module
function dwAlign(off: number): number { function dwAlign(off: number): number {
@ -61,44 +61,19 @@ enum AgentWordBalloonPosition {
BelowCentered BelowCentered
} }
class AgentWordBalloonState { abstract class AgentWordBalloonState {
char: Agent; abstract char: Agent;
text: string; abstract hasTip: boolean;
hasTip: boolean; abstract position: AgentWordBalloonPosition;
position: AgentWordBalloonPosition;
balloonCanvas: HTMLCanvasElement; balloonCanvas: HTMLCanvasElement;
balloonCanvasCtx: CanvasRenderingContext2D; balloonCanvasCtx: CanvasRenderingContext2D;
constructor(char: Agent, text: string, hasTip: boolean, position: AgentWordBalloonPosition, textColor: string) { constructor() {
this.char = char;
this.text = text;
this.hasTip = hasTip;
this.position = position;
this.balloonCanvas = document.createElement('canvas'); this.balloonCanvas = document.createElement('canvas');
this.balloonCanvasCtx = this.balloonCanvas.getContext('2d')!; this.balloonCanvasCtx = this.balloonCanvas.getContext('2d')!;
this.balloonCanvas.style.position = 'absolute'; 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'; 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() { 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 { export class Agent {
private data: AcsData; private data: AcsData;
private charDiv: HTMLDivElement; private charDiv: HTMLDivElement;
@ -141,7 +176,7 @@ export class Agent {
private animState: AgentAnimationState | null = null; private animState: AgentAnimationState | null = null;
private wordballoonState: AgentWordBalloonState | null = null; private wordballoonState: AgentWordBalloonState | null = null;
private usernameBalloonState: AgentWordBalloonState | null = null; private usernameBalloonState: AgentTextWordBalloonState | null = null;
private contextMenu: ContextMenu; private contextMenu: ContextMenu;
@ -299,7 +334,7 @@ export class Agent {
this.usernameBalloonState = null; 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(); this.usernameBalloonState.show();
} }
@ -308,7 +343,17 @@ export class Agent {
this.stopSpeaking(); 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.positionUpdated();
this.wordballoonState.show(); this.wordballoonState.show();
} }

View file

@ -177,3 +177,24 @@ export function wordballoonDrawText(ctx: CanvasRenderingContext2D, at: Point, te
h: rectInner.h + 13 * 3 + 18 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,
};
}

View file

@ -5,6 +5,7 @@ export enum MSAgentProtocolMessageType {
KeepAlive = 'nop', KeepAlive = 'nop',
Join = 'join', Join = 'join',
Talk = 'talk', Talk = 'talk',
SendImage = 'img',
Admin = 'admin', Admin = 'admin',
// Server-to-client // Server-to-client
Init = 'init', 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 // Server-to-client
export interface MSAgentInitMessage extends MSAgentProtocolMessage { 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 { export interface MSAgentPromoteMessage extends MSAgentProtocolMessage {
op: MSAgentProtocolMessageType.Promote; op: MSAgentProtocolMessageType.Promote;
data: { data: {

View file

@ -2,6 +2,11 @@
host = "127.0.0.1" host = "127.0.0.1"
port = 3000 port = 3000
proxied = true 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] [mysql]
host = "127.0.0.1" host = "127.0.0.1"
@ -46,6 +51,10 @@ tempDir = "/tmp/msac-tts"
transcodeOpus = true transcodeOpus = true
wavExpirySeconds = 60 wavExpirySeconds = 60
[images]
maxSize = { width = 300, height = 300 }
expirySeconds = 60
[[agents]] [[agents]]
friendlyName = "Clippy" friendlyName = "Clippy"
filename = "CLIPPIT.ACS" filename = "CLIPPIT.ACS"

View file

@ -12,13 +12,16 @@
"typescript": "5.4.5" "typescript": "5.4.5"
}, },
"dependencies": { "dependencies": {
"@fastify/cors": "^9.0.1",
"@fastify/static": "^7.0.4", "@fastify/static": "^7.0.4",
"@fastify/websocket": "^10.0.1", "@fastify/websocket": "^10.0.1",
"discord.js": "^14.15.3", "discord.js": "^14.15.3",
"fastify": "^4.28.1", "fastify": "^4.28.1",
"file-type": "^19.1.1",
"fluent-ffmpeg": "^2.1.3", "fluent-ffmpeg": "^2.1.3",
"html-entities": "^2.5.2", "html-entities": "^2.5.2",
"mysql2": "^3.10.2", "mysql2": "^3.10.2",
"sharp": "^0.33.4",
"toml": "^3.0.0", "toml": "^3.0.0",
"ws": "^8.17.1" "ws": "^8.17.1"
}, },

View file

@ -10,6 +10,7 @@ import {
MSAgentAdminMessage, MSAgentAdminMessage,
MSAgentAdminOperation, MSAgentAdminOperation,
MSAgentErrorMessage, MSAgentErrorMessage,
MSAgentSendImageMessage,
MSAgentJoinMessage, MSAgentJoinMessage,
MSAgentProtocolMessage, MSAgentProtocolMessage,
MSAgentProtocolMessageType, MSAgentProtocolMessageType,
@ -26,6 +27,7 @@ export interface Client {
on(event: 'join', listener: () => void): this; on(event: 'join', listener: () => void): this;
on(event: 'close', listener: () => void): this; on(event: 'close', listener: () => void): this;
on(event: 'talk', listener: (msg: string) => void): this; on(event: 'talk', listener: (msg: string) => void): this;
on(event: 'image', listener: (id: string) => void): this;
on(event: string, listener: Function): this; on(event: string, listener: Function): this;
} }
@ -160,6 +162,12 @@ export class Client extends EventEmitter {
this.emit('talk', talkMsg.data.msg); this.emit('talk', talkMsg.data.msg);
break; 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: { case MSAgentProtocolMessageType.Admin: {
let adminMsg = msg as MSAgentAdminMessage; let adminMsg = msg as MSAgentAdminMessage;
if (!adminMsg.data) return; if (!adminMsg.data) return;

View file

@ -3,12 +3,14 @@ export interface IConfig {
host: string; host: string;
port: number; port: number;
proxied: boolean; proxied: boolean;
origins: string | string[] | boolean;
}; };
mysql: MySQLConfig; mysql: MySQLConfig;
chat: ChatConfig; chat: ChatConfig;
motd: motdConfig; motd: motdConfig;
discord: DiscordConfig; discord: DiscordConfig;
tts: TTSConfig; tts: TTSConfig;
images: ImagesConfig;
agents: AgentConfig[]; agents: AgentConfig[];
} }
@ -58,3 +60,8 @@ export interface DiscordConfig {
enabled: boolean; enabled: boolean;
webhookURL: string; webhookURL: string;
} }
export interface ImagesConfig {
maxSize: { width: number, height: number };
expirySeconds: number;
}

116
server/src/imageuploader.ts Normal file
View file

@ -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<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);
});
});
}
export class ImageUploader {
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));
}
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);
}
}

View file

@ -1,6 +1,7 @@
import Fastify from 'fastify'; import Fastify from 'fastify';
import FastifyWS from '@fastify/websocket'; import FastifyWS from '@fastify/websocket';
import FastifyStatic from '@fastify/static'; import FastifyStatic from '@fastify/static';
import FastifyCors from '@fastify/cors';
import { Client } from './client.js'; import { Client } from './client.js';
import { MSAgentChatRoom } from './room.js'; import { MSAgentChatRoom } from './room.js';
import * as toml from 'toml'; import * as toml from 'toml';
@ -12,6 +13,7 @@ import { isIP } from 'net';
import { Database } from './database.js'; import { Database } from './database.js';
import { MSAgentErrorMessage, MSAgentProtocolMessageType } from '@msagent-chat/protocol'; import { MSAgentErrorMessage, MSAgentProtocolMessageType } from '@msagent-chat/protocol';
import { DiscordLogger } from './discord.js'; import { DiscordLogger } from './discord.js';
import { ImageUploader } from './imageuploader.js';
let config: IConfig; let config: IConfig;
let configPath: string; let configPath: string;
@ -35,7 +37,13 @@ let db = new Database(config.mysql);
await db.init(); await db.init();
const app = Fastify({ const app = Fastify({
logger: true logger: true,
bodyLimit: 20971520
});
app.register(FastifyCors, {
origin: config.http.origins,
methods: ['GET', 'POST', 'PUT'],
}); });
app.register(FastifyWS); app.register(FastifyWS);
@ -92,7 +100,10 @@ if (config.discord.enabled) {
discord = new DiscordLogger(config.discord); 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.register(async (app) => {
app.get('/api/socket', { websocket: true }, async (socket, req) => { app.get('/api/socket', { websocket: true }, async (socket, req) => {

View file

@ -1,6 +1,7 @@
import { import {
MSAgentAddUserMessage, MSAgentAddUserMessage,
MSAgentChatMessage, MSAgentChatMessage,
MSAgentImageMessage,
MSAgentInitMessage, MSAgentInitMessage,
MSAgentPromoteMessage, MSAgentPromoteMessage,
MSAgentProtocolMessage, MSAgentProtocolMessage,
@ -13,6 +14,7 @@ import { AgentConfig, ChatConfig } from './config.js';
import * as htmlentities from 'html-entities'; import * as htmlentities from 'html-entities';
import { Database } from './database.js'; import { Database } from './database.js';
import { DiscordLogger } from './discord.js'; import { DiscordLogger } from './discord.js';
import { ImageUploader } from './imageuploader.js';
export class MSAgentChatRoom { export class MSAgentChatRoom {
agents: AgentConfig[]; agents: AgentConfig[];
@ -21,14 +23,16 @@ export class MSAgentChatRoom {
msgId: number = 0; msgId: number = 0;
config: ChatConfig; config: ChatConfig;
db: Database; db: Database;
img: ImageUploader;
discord: DiscordLogger | null; 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.agents = agents;
this.clients = []; this.clients = [];
this.config = config; this.config = config;
this.tts = tts; this.tts = tts;
this.db = db; this.db = db;
this.img = img;
this.discord = discord; this.discord = discord;
} }
@ -96,6 +100,21 @@ export class MSAgentChatRoom {
} }
this.discord?.logMsg(client.username!, message); 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', () => { client.on('admin', () => {
let msg: MSAgentPromoteMessage = { let msg: MSAgentPromoteMessage = {
op: MSAgentProtocolMessageType.Promote, op: MSAgentProtocolMessageType.Promote,

View file

@ -122,6 +122,7 @@
<div id="chatBar"> <div id="chatBar">
<input type="text" id="chatInput" placeholder="Send a message" /> <input type="text" id="chatInput" placeholder="Send a message" />
<button id="imageUploadBtn">Image</button>
<button id="chatSendBtn">Send</button> <button id="chatSendBtn">Send</button>
</div> </div>
</div> </div>

View file

@ -11,12 +11,14 @@ import {
MSAgentAdminOperation, MSAgentAdminOperation,
MSAgentChatMessage, MSAgentChatMessage,
MSAgentErrorMessage, MSAgentErrorMessage,
MSAgentImageMessage,
MSAgentInitMessage, MSAgentInitMessage,
MSAgentJoinMessage, MSAgentJoinMessage,
MSAgentPromoteMessage, MSAgentPromoteMessage,
MSAgentProtocolMessage, MSAgentProtocolMessage,
MSAgentProtocolMessageType, MSAgentProtocolMessageType,
MSAgentRemoveUserMessage, MSAgentRemoveUserMessage,
MSAgentSendImageMessage,
MSAgentTalkMessage MSAgentTalkMessage
} from '@msagent-chat/protocol'; } from '@msagent-chat/protocol';
import { User } from './user'; import { User } from './user';
@ -49,6 +51,7 @@ export class MSAgentClient {
private charlimit: number = 0; private charlimit: number = 0;
private admin: boolean; private admin: boolean;
private loginCb: (e: KeyboardEvent) => void; private loginCb: (e: KeyboardEvent) => void;
private currentMsgId: number = 0;
private username: string | null = null; private username: string | null = null;
private agentContainer: HTMLElement; private agentContainer: HTMLElement;
@ -172,6 +175,31 @@ export class MSAgentClient {
this.send(talkMsg); 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() { getCharlimit() {
return this.charlimit; return this.charlimit;
} }
@ -317,10 +345,12 @@ export class MSAgentClient {
this.playingAudio.set(user!.username, audio); this.playingAudio.set(user!.username, audio);
let msgId = ++this.currentMsgId;
audio.addEventListener('ended', () => { audio.addEventListener('ended', () => {
// give a bit of time before the wordballoon disappears // give a bit of time before the wordballoon disappears
setTimeout(() => { setTimeout(() => {
if (this.playingAudio.get(user!.username) === audio) { if (this.currentMsgId === msgId) {
user!.agent.stopSpeaking(); user!.agent.stopSpeaking();
this.playingAudio.delete(user!.username); this.playingAudio.delete(user!.username);
} }
@ -332,6 +362,24 @@ export class MSAgentClient {
} }
break; 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: { case MSAgentProtocolMessageType.Admin: {
let adminMsg = msg as MSAgentAdminMessage; let adminMsg = msg as MSAgentAdminMessage;
switch (adminMsg.data.action) { switch (adminMsg.data.action) {

View file

@ -17,6 +17,7 @@ const elements = {
chatView: document.getElementById('chatView') as HTMLDivElement, chatView: document.getElementById('chatView') as HTMLDivElement,
chatInput: document.getElementById('chatInput') as HTMLInputElement, chatInput: document.getElementById('chatInput') as HTMLInputElement,
imageUploadBtn: document.getElementById('imageUploadBtn') as HTMLButtonElement,
chatSendBtn: document.getElementById('chatSendBtn') as HTMLButtonElement, chatSendBtn: document.getElementById('chatSendBtn') as HTMLButtonElement,
roomSettingsWindow: document.getElementById('roomSettingsWindow') as HTMLDivElement roomSettingsWindow: document.getElementById('roomSettingsWindow') as HTMLDivElement
@ -120,6 +121,24 @@ function talk() {
roomInit(); 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; let w = window as any;

338
yarn.lock
View file

@ -113,6 +113,15 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "@fastify/accept-negotiator@npm:^1.0.0":
version: 1.1.0 version: 1.1.0
resolution: "@fastify/accept-negotiator@npm:1.1.0" resolution: "@fastify/accept-negotiator@npm:1.1.0"
@ -131,6 +140,16 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "@fastify/error@npm:^3.3.0, @fastify/error@npm:^3.4.0":
version: 3.4.1 version: 3.4.1
resolution: "@fastify/error@npm:3.4.1" resolution: "@fastify/error@npm:3.4.1"
@ -194,6 +213,181 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "@isaacs/cliui@npm:^8.0.2":
version: 8.0.2 version: 8.0.2
resolution: "@isaacs/cliui@npm:8.0.2" resolution: "@isaacs/cliui@npm:8.0.2"
@ -306,6 +500,7 @@ __metadata:
version: 0.0.0-use.local version: 0.0.0-use.local
resolution: "@msagent-chat/server@workspace:server" resolution: "@msagent-chat/server@workspace:server"
dependencies: dependencies:
"@fastify/cors": "npm:^9.0.1"
"@fastify/static": "npm:^7.0.4" "@fastify/static": "npm:^7.0.4"
"@fastify/websocket": "npm:^10.0.1" "@fastify/websocket": "npm:^10.0.1"
"@types/fluent-ffmpeg": "npm:^2.1.24" "@types/fluent-ffmpeg": "npm:^2.1.24"
@ -313,9 +508,11 @@ __metadata:
"@types/ws": "npm:^8.5.10" "@types/ws": "npm:^8.5.10"
discord.js: "npm:^14.15.3" discord.js: "npm:^14.15.3"
fastify: "npm:^4.28.1" fastify: "npm:^4.28.1"
file-type: "npm:^19.1.1"
fluent-ffmpeg: "npm:^2.1.3" fluent-ffmpeg: "npm:^2.1.3"
html-entities: "npm:^2.5.2" html-entities: "npm:^2.5.2"
mysql2: "npm:^3.10.2" mysql2: "npm:^3.10.2"
sharp: "npm:^0.33.4"
toml: "npm:^3.0.0" toml: "npm:^3.0.0"
typescript: "npm:5.4.5" typescript: "npm:5.4.5"
ws: "npm:^8.17.1" ws: "npm:^8.17.1"
@ -1453,6 +1650,13 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "@trysound/sax@npm:0.2.0":
version: 0.2.0 version: 0.2.0
resolution: "@trysound/sax@npm:0.2.0" resolution: "@trysound/sax@npm:0.2.0"
@ -2065,7 +2269,7 @@ __metadata:
languageName: node languageName: node
linkType: hard 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 version: 2.0.3
resolution: "detect-libc@npm:2.0.3" resolution: "detect-libc@npm:2.0.3"
checksum: 10c0/88095bda8f90220c95f162bf92cad70bd0e424913e655c20578600e35b91edc261af27531cf160a331e185c0ced93944bc7e09939143225f56312d7fd800fdb7 checksum: 10c0/88095bda8f90220c95f162bf92cad70bd0e424913e655c20578600e35b91edc261af27531cf160a331e185c0ced93944bc7e09939143225f56312d7fd800fdb7
@ -2401,6 +2605,17 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "fill-range@npm:^7.1.1":
version: 7.1.1 version: 7.1.1
resolution: "fill-range@npm:7.1.1" resolution: "fill-range@npm:7.1.1"
@ -3273,6 +3488,15 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "ms@npm:2.1.2":
version: 2.1.2 version: 2.1.2
resolution: "ms@npm:2.1.2" resolution: "ms@npm:2.1.2"
@ -3504,6 +3728,13 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "on-exit-leak-free@npm:^2.1.0":
version: 2.1.2 version: 2.1.2
resolution: "on-exit-leak-free@npm:2.1.2" resolution: "on-exit-leak-free@npm:2.1.2"
@ -3605,6 +3836,13 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "picocolors@npm:^1.0.0, picocolors@npm:^1.0.1":
version: 1.0.1 version: 1.0.1
resolution: "picocolors@npm:1.0.1" resolution: "picocolors@npm:1.0.1"
@ -3967,7 +4205,7 @@ __metadata:
languageName: node languageName: node
linkType: hard 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 version: 7.6.2
resolution: "semver@npm:7.6.2" resolution: "semver@npm:7.6.2"
bin: bin:
@ -4014,6 +4252,75 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "shebang-command@npm:^2.0.0":
version: 2.0.0 version: 2.0.0
resolution: "shebang-command@npm:2.0.0" resolution: "shebang-command@npm:2.0.0"
@ -4229,6 +4536,16 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "supports-color@npm:^5.3.0":
version: 5.5.0 version: 5.5.0
resolution: "supports-color@npm:5.5.0" resolution: "supports-color@npm:5.5.0"
@ -4349,6 +4666,16 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "toml@npm:^3.0.0":
version: 3.0.0 version: 3.0.0
resolution: "toml@npm:3.0.0" resolution: "toml@npm:3.0.0"
@ -4433,6 +4760,13 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "undici-types@npm:~5.26.4":
version: 5.26.5 version: 5.26.5
resolution: "undici-types@npm:5.26.5" resolution: "undici-types@npm:5.26.5"