diff --git a/msagent.js/src/agent.ts b/msagent.js/src/agent.ts index ab6b631..c71399f 100644 --- a/msagent.js/src/agent.ts +++ b/msagent.js/src/agent.ts @@ -115,14 +115,14 @@ class AgentWordBalloonState { positionUpdated() { let size = this.char.getSize(); - this.balloonCanvas.style.left = -((this.balloonCanvas.width / 2) - (size.w / 2)) + 'px'; + this.balloonCanvas.style.left = -(this.balloonCanvas.width / 2 - size.w / 2) + 'px'; switch (this.position) { case AgentWordBalloonPosition.AboveCentered: { - this.balloonCanvas.style.top = -(this.balloonCanvas.height) + 'px'; + this.balloonCanvas.style.top = -this.balloonCanvas.height + 'px'; break; } case AgentWordBalloonPosition.BelowCentered: { - this.balloonCanvas.style.bottom = -(this.balloonCanvas.height) + 'px'; + this.balloonCanvas.style.bottom = -this.balloonCanvas.height + 'px'; break; } } @@ -307,8 +307,8 @@ export class Agent { if (this.wordballoonState != null) { this.stopSpeaking(); } - - this.wordballoonState = new AgentWordBalloonState(this, text, true, AgentWordBalloonPosition.AboveCentered, "#000000"); + + this.wordballoonState = new AgentWordBalloonState(this, text, true, AgentWordBalloonPosition.AboveCentered, '#000000'); this.wordballoonState.positionUpdated(); this.wordballoonState.show(); } diff --git a/msagent.js/src/buffer.ts b/msagent.js/src/buffer.ts index 49e1e63..20e935d 100644 --- a/msagent.js/src/buffer.ts +++ b/msagent.js/src/buffer.ts @@ -5,21 +5,21 @@ export enum SeekDir { BEG = 0, CUR = 1, END = 2 -}; +} // A helper over DataView to make it more ergonomic for parsing file data. export class BufferStream { private bufferImpl: Uint8Array; - private dataView: DataView; + private dataView: DataView; private readPointer: number = 0; constructor(buffer: Uint8Array, byteOffset?: number) { this.bufferImpl = buffer; - this.dataView = new DataView(this.bufferImpl.buffer, byteOffset); + this.dataView = new DataView(this.bufferImpl.buffer, byteOffset); } seek(where: number, whence: SeekDir) { - switch(whence) { + switch (whence) { case SeekDir.BEG: this.readPointer = where; break; @@ -29,8 +29,7 @@ export class BufferStream { break; case SeekDir.END: - if(where > 0) - throw new Error("Cannot use SeekDir.END with where greater than 0"); + if (where > 0) throw new Error('Cannot use SeekDir.END with where greater than 0'); this.readPointer = this.bufferImpl.length + whence; break; @@ -39,34 +38,56 @@ export class BufferStream { return this.readPointer; } - tell() { return this.seek(0, SeekDir.CUR); } + tell() { + return this.seek(0, SeekDir.CUR); + } // common impl function for read*() - private readImpl(func: (this: DataView, offset: number, le?: boolean|undefined) => T, size: number, le?: boolean|undefined) { + private readImpl(func: (this: DataView, offset: number, le?: boolean | undefined) => T, size: number, le?: boolean | undefined) { let res = func.call(this.dataView, this.readPointer, le); this.readPointer += size; return res; } - // Creates a view of a part of the buffer. - // THIS DOES NOT DEEP COPY! + // Creates a view of a part of the buffer. + // THIS DOES NOT DEEP COPY! subBuffer(len: number) { - let oldReadPointer = this.readPointer; + let oldReadPointer = this.readPointer; let buffer = this.bufferImpl.subarray(oldReadPointer, oldReadPointer + len); this.readPointer += len; return new BufferStream(buffer, oldReadPointer); } - readS8() { return this.readImpl(DataView.prototype.getInt8, 1); } - readU8() { return this.readImpl(DataView.prototype.getUint8, 1); } - readS16LE() { return this.readImpl(DataView.prototype.getInt16, 2, true); } - readS16BE() { return this.readImpl(DataView.prototype.getInt16, 2, false); } - readU16LE() { return this.readImpl(DataView.prototype.getUint16, 2, true); } - readU16BE() { return this.readImpl(DataView.prototype.getUint16, 2, false); } - readS32LE() { return this.readImpl(DataView.prototype.getInt32, 4, true); } - readS32BE() { return this.readImpl(DataView.prototype.getInt32, 4, false); } - readU32LE() { return this.readImpl(DataView.prototype.getUint32, 4, true); } - readU32BE() { return this.readImpl(DataView.prototype.getUint32, 4, false); } + readS8() { + return this.readImpl(DataView.prototype.getInt8, 1); + } + readU8() { + return this.readImpl(DataView.prototype.getUint8, 1); + } + readS16LE() { + return this.readImpl(DataView.prototype.getInt16, 2, true); + } + readS16BE() { + return this.readImpl(DataView.prototype.getInt16, 2, false); + } + readU16LE() { + return this.readImpl(DataView.prototype.getUint16, 2, true); + } + readU16BE() { + return this.readImpl(DataView.prototype.getUint16, 2, false); + } + readS32LE() { + return this.readImpl(DataView.prototype.getInt32, 4, true); + } + readS32BE() { + return this.readImpl(DataView.prototype.getInt32, 4, false); + } + readU32LE() { + return this.readImpl(DataView.prototype.getUint32, 4, true); + } + readU32BE() { + return this.readImpl(DataView.prototype.getUint32, 4, false); + } // Use this for temporary offset modification, e.g: when reading // a structure *pointed to* inside another structure. @@ -77,27 +98,25 @@ export class BufferStream { this.seek(last, SeekDir.BEG); } - readBool() : boolean { - let res = this.readU8(); - return res != 0; - } + readBool(): boolean { + let res = this.readU8(); + return res != 0; + } - readString(len: number, charReader: (this: BufferStream) => TChar): string { - let str = ""; + readString(len: number, charReader: (this: BufferStream) => TChar): string { + let str = ''; - for(let i = 0; i < len; ++i) - str += String.fromCharCode(charReader.call(this)); + for (let i = 0; i < len; ++i) str += String.fromCharCode(charReader.call(this)); - // dispose of a nul terminator. We don't support other bare Agent formats, + // dispose of a nul terminator. We don't support other bare Agent formats, // so we shouldn't need to add the "support" for that. - charReader.call(this); - return str; - } + charReader.call(this); + return str; + } readPascalString(lengthReader: (this: BufferStream) => number = BufferStream.prototype.readU32LE, charReader: (this: BufferStream) => number = BufferStream.prototype.readU16LE) { let len = lengthReader.call(this); - if(len == 0) - return ""; + if (len == 0) return ''; return this.readString(len, charReader); } @@ -107,25 +126,22 @@ export class BufferStream { return this.subBuffer(len); } - readDataChunk(lengthReader: (this: BufferStream) => number = BufferStream.prototype.readU32LE) { + readDataChunk(lengthReader: (this: BufferStream) => number = BufferStream.prototype.readU32LE) { return this.readDataChunkBuffer(lengthReader).raw(); } - // reads a counted list. The length reader is on the other end so you don't need to specify it - // (if it's u32) - readCountedList(objReader: (stream: BufferStream) => TObject, lengthReader: (this: BufferStream) => number = BufferStream.prototype.readU32LE): TObject[] { + // reads a counted list. The length reader is on the other end so you don't need to specify it + // (if it's u32) + readCountedList(objReader: (stream: BufferStream) => TObject, lengthReader: (this: BufferStream) => number = BufferStream.prototype.readU32LE): TObject[] { let len = lengthReader.call(this); let arr: TObject[] = []; - if(len == 0) - return arr; + if (len == 0) return arr; - for(let i = 0; i < len; ++i) - arr.push(objReader(this)); + for (let i = 0; i < len; ++i) arr.push(objReader(this)); return arr; } - raw() { return this.bufferImpl; } diff --git a/msagent.js/src/character.ts b/msagent.js/src/character.ts index 9ae7dff..02a333d 100644 --- a/msagent.js/src/character.ts +++ b/msagent.js/src/character.ts @@ -35,7 +35,6 @@ export function agentCharacterParseACS(buffer: BufferStream): AcsData { let imageInfoLocation = LOCATION.read(buffer); let audioInfoLocation = LOCATION.read(buffer); - buffer.withOffset(characterInfoLocation.offset, () => { acsData.characterInfo = AcsCharacterInfo.read(buffer); }); @@ -59,9 +58,9 @@ export function agentCreateCharacter(data: AcsData): Agent { return new Agent(data); } -export async function agentCreateCharacterFromUrl(url: string) : Promise { +export async function agentCreateCharacterFromUrl(url: string): Promise { // just return the cache object - if(acsDataCache.has(url)) { + if (acsDataCache.has(url)) { return agentCreateCharacter(acsDataCache.get(url)!); } else { let res = await fetch(url); diff --git a/msagent.js/src/contextmenu.ts b/msagent.js/src/contextmenu.ts index 5c3cecf..2d052d1 100644 --- a/msagent.js/src/contextmenu.ts +++ b/msagent.js/src/contextmenu.ts @@ -1,80 +1,84 @@ export class ContextMenuItem { - private element: HTMLLIElement; + private element: HTMLLIElement; - name: string; - cb: Function; + name: string; + cb: Function; - constructor(name: string, cb: Function) { - this.name = name; - this.cb = cb; - this.element = document.createElement("li"); - this.element.classList.add("context-menu-item"); - this.element.innerText = name; - this.element.addEventListener('mousedown', () => this.cb()); - } + constructor(name: string, cb: Function) { + this.name = name; + this.cb = cb; + this.element = document.createElement('li'); + this.element.classList.add('context-menu-item'); + this.element.innerText = name; + this.element.addEventListener('mousedown', () => this.cb()); + } - getElement() { - return this.element; - } + getElement() { + return this.element; + } - setName(name: string) { - this.name = name; - this.element.innerText = name; - } + setName(name: string) { + this.name = name; + this.element.innerText = name; + } - setCb(cb: Function) { - this.cb = cb; - } + setCb(cb: Function) { + this.cb = cb; + } } export class ContextMenu { - private element: HTMLDivElement; - private list: HTMLUListElement; + private element: HTMLDivElement; + private list: HTMLUListElement; - private items: Array + private items: Array; - constructor(parent: HTMLElement) { - this.element = document.createElement("div"); - this.list = document.createElement("ul"); - this.element.appendChild(this.list); - this.items = []; - this.element.classList.add("context-menu"); - this.element.style.display = "none"; - this.element.style.position = "fixed"; - parent.appendChild(this.element); - } + constructor(parent: HTMLElement) { + this.element = document.createElement('div'); + this.list = document.createElement('ul'); + this.element.appendChild(this.list); + this.items = []; + this.element.classList.add('context-menu'); + this.element.style.display = 'none'; + this.element.style.position = 'fixed'; + parent.appendChild(this.element); + } - show(x: number, y: number) { - this.element.style.left = x + "px"; - this.element.style.top = y + "px"; - document.addEventListener('mousedown', () => { - this.hide(); - }, {once: true}); - this.element.style.display = "block"; - } + show(x: number, y: number) { + this.element.style.left = x + 'px'; + this.element.style.top = y + 'px'; + document.addEventListener( + 'mousedown', + () => { + this.hide(); + }, + { once: true } + ); + this.element.style.display = 'block'; + } - hide() { - this.element.style.display = "none"; - } + hide() { + this.element.style.display = 'none'; + } - addItem(item: ContextMenuItem) { - this.items.push(item); - this.list.appendChild(item.getElement()); - } + addItem(item: ContextMenuItem) { + this.items.push(item); + this.list.appendChild(item.getElement()); + } - removeItem(item: ContextMenuItem) { - let i = this.items.indexOf(item); - if (i === -1) return; - this.items.splice(i, 1); - item.getElement().remove(); - } + removeItem(item: ContextMenuItem) { + let i = this.items.indexOf(item); + if (i === -1) return; + this.items.splice(i, 1); + item.getElement().remove(); + } - getItem(name: string) { - return this.items.find(i => i.name === name); - } + getItem(name: string) { + return this.items.find((i) => i.name === name); + } - clearItems() { - this.items.splice(0, this.items.length); - this.list.replaceChildren(); - } -} \ No newline at end of file + clearItems() { + this.items.splice(0, this.items.length); + this.list.replaceChildren(); + } +} diff --git a/msagent.js/src/decompress.ts b/msagent.js/src/decompress.ts index 4eaa93a..f02f49b 100644 --- a/msagent.js/src/decompress.ts +++ b/msagent.js/src/decompress.ts @@ -14,16 +14,14 @@ export async function compressInit() { compressWasm = await WebAssembly.instantiateStreaming(fetch(url)); } - function compressWasmGetExports() { - return (compressWasm.instance.exports as any) as CompressWasmExports; + return compressWasm.instance.exports as any as CompressWasmExports; } -function compressWASMGetMemory() : WebAssembly.Memory { +function compressWASMGetMemory(): WebAssembly.Memory { return compressWasmGetExports().memory; } - // debugging //(window as any).DEBUGcompressGetWASM = () => { // return compressWasm; @@ -35,10 +33,10 @@ export function compressDecompress(src: Uint8Array, dest: Uint8Array) { // Grow the WASM heap if needed. Funnily enough, this code is never hit in most // ACSes, so IDK if it's even needed let memory = compressWASMGetMemory(); - if(memory.buffer.byteLength < src.length + dest.length) { + if (memory.buffer.byteLength < src.length + dest.length) { // A WebAssembly page is 64kb, so we need to grow at least that much let npages = Math.floor((src.length + dest.length) / 65535) + 1; - console.log("Need to grow WASM heap", npages, "pages", "(current byteLength is", memory.buffer.byteLength, ", we need", src.length + dest.length, ")"); + console.log('Need to grow WASM heap', npages, 'pages', '(current byteLength is', memory.buffer.byteLength, ', we need', src.length + dest.length, ')'); memory.grow(npages); } @@ -50,8 +48,7 @@ export function compressDecompress(src: Uint8Array, dest: Uint8Array) { // Call the WASM compression routine let nrBytesDecompressed = compressWasmGetExports().agentDecompressWASM(0, src.length, src.length, dest.length); - if(nrBytesDecompressed != dest.length) - throw new Error(`decompression failed: ${nrBytesDecompressed} != ${dest.length}`); + if (nrBytesDecompressed != dest.length) throw new Error(`decompression failed: ${nrBytesDecompressed} != ${dest.length}`); // Dest will be memory[src.length..dest.length] dest.set(copyBuffer.slice(src.length, src.length + dest.length), 0); diff --git a/msagent.js/src/index.ts b/msagent.js/src/index.ts index 38c4c64..e30aaaa 100644 --- a/msagent.js/src/index.ts +++ b/msagent.js/src/index.ts @@ -1,16 +1,15 @@ -import { compressInit } from "./decompress.js"; -import { wordballoonInit } from "./wordballoon.js"; - -export * from "./types.js"; -export * from "./character.js"; -export * from "./decompress.js"; -export * from "./sprite.js"; -export * from "./wordballoon.js"; -export * from "./contextmenu.js"; +import { compressInit } from './decompress.js'; +import { wordballoonInit } from './wordballoon.js'; +export * from './types.js'; +export * from './character.js'; +export * from './decompress.js'; +export * from './sprite.js'; +export * from './wordballoon.js'; +export * from './contextmenu.js'; // Convinence function which initalizes all of msagent.js. export async function agentInit() { - await compressInit(); - await wordballoonInit(); + await compressInit(); + await wordballoonInit(); } diff --git a/msagent.js/src/structs/core.ts b/msagent.js/src/structs/core.ts index f593e56..d646ac6 100644 --- a/msagent.js/src/structs/core.ts +++ b/msagent.js/src/structs/core.ts @@ -108,7 +108,7 @@ export class RGBAColor { //quad.g = (val & 0x00ff0000) >> 16; //quad.b = (val & 0x0000ff00) >> 8; - quad.r = (val & 0x000000ff); + quad.r = val & 0x000000ff; quad.g = (val & 0x0000ff00) >> 8; quad.b = (val & 0x00ff0000) >> 16; diff --git a/msagent.js/src/structs/image.ts b/msagent.js/src/structs/image.ts index 9f1253c..7be57b7 100644 --- a/msagent.js/src/structs/image.ts +++ b/msagent.js/src/structs/image.ts @@ -29,17 +29,17 @@ export class AcsImage { image.data = data; } - let temp = COMPRESSED_DATABLOCK.read(buffer); + let temp = COMPRESSED_DATABLOCK.read(buffer); let tempBuffer = new BufferStream(temp.data); image.regionData = RGNDATA.read(tempBuffer); - return image; + return image; } } export class AcsImageEntry { - image = new AcsImage(); + image = new AcsImage(); static read(buffer: BufferStream) { let image = new AcsImageEntry(); @@ -48,9 +48,9 @@ export class AcsImageEntry { let loc = LOCATION.read(buffer); let checksum = buffer.readU32LE(); - buffer.withOffset(loc.offset, () => { - image.image = AcsImage.read(buffer); - }); + buffer.withOffset(loc.offset, () => { + image.image = AcsImage.read(buffer); + }); return image; } diff --git a/msagent.js/src/types.ts b/msagent.js/src/types.ts index ffceaa4..98e868d 100644 --- a/msagent.js/src/types.ts +++ b/msagent.js/src/types.ts @@ -1,16 +1,16 @@ export type Rect = { - x: number, - y: number, - w: number, - h: number + x: number; + y: number; + w: number; + h: number; }; export type Size = { - w: number, - h: number + w: number; + h: number; }; export type Point = { - x: number, - y: number + x: number; + y: number; }; diff --git a/msagent.js/src/wordballoon.ts b/msagent.js/src/wordballoon.ts index ec2e4f9..6c12f88 100644 --- a/msagent.js/src/wordballoon.ts +++ b/msagent.js/src/wordballoon.ts @@ -5,7 +5,6 @@ let corner_sprite: HTMLImageElement; let straight_sprite: HTMLImageElement; let tip_sprite: HTMLImageElement; - // Call *once* to initalize the wordballoon drawing system. // Do not call other wordballoon* functions WITHOUT doing so. export async function wordballoonInit() { @@ -68,7 +67,7 @@ export function wordballoonDraw(ctx: CanvasRenderingContext2D, at: Point, size: spriteDrawRotated(ctx, corner_sprite, 90, at.x + 12 * i, at.y); // Draw both the left and right sides of the box. We can do this in one pass - // so we do that for simplicity. + // so we do that for simplicity. let j = 1; for (; j < size.h / 12; ++j) { spriteDrawRotated(ctx, straight_sprite, 270, at.x, at.y + 12 * j); @@ -89,12 +88,11 @@ export function wordballoonDraw(ctx: CanvasRenderingContext2D, at: Point, size: // TODO: a tip point should be provided. We will pick the best corner to stick it on, // and the best y coordinate on that corner to stick it on. - // - // For now, we always simply use the center of the bottom.. + // + // For now, we always simply use the center of the bottom.. // Draw the tip. - if (hasTip) - spriteDraw(ctx, tip_sprite, at.x + size.w / 2, at.y + 12 * (j + 1) - 1); + if (hasTip) spriteDraw(ctx, tip_sprite, at.x + size.w / 2, at.y + 12 * (j + 1) - 1); ctx.restore(); @@ -109,7 +107,7 @@ export function wordballoonDraw(ctx: CanvasRenderingContext2D, at: Point, size: function wordWrapToStringList(text: string, maxLength: number) { // this was stolen off stackoverflow, it sucks but it (kind of) works - // it should probably be replaced at some point. + // it should probably be replaced at some point. var result = [], line: string[] = []; var length = 0; @@ -129,7 +127,7 @@ function wordWrapToStringList(text: string, maxLength: number) { } // This draws a wordballoon with text. This function respects the current context's font settings and does *not* modify them. -export function wordballoonDrawText(ctx: CanvasRenderingContext2D, at: Point, text: string, maxLen: number = 20, hasTip: boolean = true, color: string = "#000000"): Rect { +export function wordballoonDrawText(ctx: CanvasRenderingContext2D, at: Point, text: string, maxLen: number = 20, hasTip: boolean = true, color: string = '#000000'): Rect { let lines = wordWrapToStringList(text, maxLen); // Create metrics for each line @@ -158,10 +156,10 @@ export function wordballoonDrawText(ctx: CanvasRenderingContext2D, at: Point, te size.w = Math.floor(size.w + 12); size.h = Math.floor(size.h); - // Draw the word balloon and get the inner rect + // Draw the word balloon and get the inner rect let rectInner = wordballoonDraw(ctx, at, size, hasTip); - // Draw all the lines of text + // Draw all the lines of text let y = 0; for (let i in lines) { let metric = metrics[i]; @@ -175,7 +173,7 @@ export function wordballoonDrawText(ctx: CanvasRenderingContext2D, at: Point, te return { x: at.x, y: at.y, - w: rectInner.w + (12*3) + 12, - h: rectInner.h + (13*3) + 18 - } + w: rectInner.w + 12 * 3 + 12, + h: rectInner.h + 13 * 3 + 18 + }; } diff --git a/package.json b/package.json index a38efaa..8dbd6b2 100644 --- a/package.json +++ b/package.json @@ -6,11 +6,15 @@ "protocol", "msagent.js" ], + "scripts": { + "format": "prettier --write **/*.{ts,html,scss}" + }, "packageManager": "yarn@4.2.2", "devDependencies": { "@parcel/packager-ts": "2.12.0", "@parcel/transformer-sass": "2.12.0", "@parcel/transformer-typescript-types": "2.12.0", + "prettier": "^3.3.3", "typescript": ">=3.0.0" } } diff --git a/protocol/src/admin.ts b/protocol/src/admin.ts index fa1ca98..2190526 100644 --- a/protocol/src/admin.ts +++ b/protocol/src/admin.ts @@ -1,64 +1,64 @@ -import { MSAgentProtocolMessage, MSAgentProtocolMessageType } from "./protocol"; +import { MSAgentProtocolMessage, MSAgentProtocolMessageType } from './protocol'; export enum MSAgentAdminOperation { - // Client-to-server - Kick = "kick", - Ban = "ban", - // Bidirectional - Login = "login", - GetIP = "ip", + // Client-to-server + Kick = 'kick', + Ban = 'ban', + // Bidirectional + Login = 'login', + GetIP = 'ip' } export interface MSAgentAdminMessage extends MSAgentProtocolMessage { - op: MSAgentProtocolMessageType.Admin, - data: { - action: MSAgentAdminOperation - } + op: MSAgentProtocolMessageType.Admin; + data: { + action: MSAgentAdminOperation; + }; } // Client-to-server export interface MSAgentAdminLoginMessage extends MSAgentAdminMessage { - data: { - action: MSAgentAdminOperation.Login, - password: string - } + data: { + action: MSAgentAdminOperation.Login; + password: string; + }; } export interface MSAgentAdminGetIPMessage extends MSAgentAdminMessage { - data: { - action: MSAgentAdminOperation.GetIP, - username: string - } + data: { + action: MSAgentAdminOperation.GetIP; + username: string; + }; } export interface MSAgentAdminKickMessage extends MSAgentAdminMessage { - data: { - action: MSAgentAdminOperation.Kick, - username: string - } + data: { + action: MSAgentAdminOperation.Kick; + username: string; + }; } export interface MSAgentAdminBanMessage extends MSAgentAdminMessage { - data: { - action: MSAgentAdminOperation.Ban, - username: string - } + data: { + action: MSAgentAdminOperation.Ban; + username: string; + }; } // Server-to-client export interface MSAgentAdminLoginResponse extends MSAgentAdminMessage { - data: { - action: MSAgentAdminOperation.Login, - success: boolean - } + data: { + action: MSAgentAdminOperation.Login; + success: boolean; + }; } export interface MSAgentAdminGetIPResponse extends MSAgentAdminMessage { - data: { - action: MSAgentAdminOperation.GetIP, - username: string - ip: string - } -} \ No newline at end of file + data: { + action: MSAgentAdminOperation.GetIP; + username: string; + ip: string; + }; +} diff --git a/protocol/src/protocol.ts b/protocol/src/protocol.ts index b7bfe5a..bacbf56 100644 --- a/protocol/src/protocol.ts +++ b/protocol/src/protocol.ts @@ -1,91 +1,91 @@ export * from './admin.js'; export enum MSAgentProtocolMessageType { - // Client-to-server - KeepAlive = "nop", - Join = "join", - Talk = "talk", - Admin = "admin", - // Server-to-client - Init = "init", - AddUser = "adduser", - RemoveUser = "remuser", - Chat = "chat", - Promote = "promote", - Error = "error" + // Client-to-server + KeepAlive = 'nop', + Join = 'join', + Talk = 'talk', + Admin = 'admin', + // Server-to-client + Init = 'init', + AddUser = 'adduser', + RemoveUser = 'remuser', + Chat = 'chat', + Promote = 'promote', + Error = 'error' } export interface MSAgentProtocolMessage { - op: MSAgentProtocolMessageType + op: MSAgentProtocolMessageType; } // Client-to-server export interface MSAgentJoinMessage extends MSAgentProtocolMessage { - op: MSAgentProtocolMessageType.Join, - data: { - username: string; - agent: string; - } + op: MSAgentProtocolMessageType.Join; + data: { + username: string; + agent: string; + }; } export interface MSAgentTalkMessage extends MSAgentProtocolMessage { - op: MSAgentProtocolMessageType.Talk, - data: { - msg: string; - } + op: MSAgentProtocolMessageType.Talk; + data: { + msg: string; + }; } // Server-to-client export interface MSAgentInitMessage extends MSAgentProtocolMessage { - op: MSAgentProtocolMessageType.Init, - data: { - username: string - agent: string - charlimit: number - users: { - username: string, - agent: string, - admin: boolean - }[] - } + op: MSAgentProtocolMessageType.Init; + data: { + username: string; + agent: string; + charlimit: number; + users: { + username: string; + agent: string; + admin: boolean; + }[]; + }; } export interface MSAgentAddUserMessage extends MSAgentProtocolMessage { - op: MSAgentProtocolMessageType.AddUser, - data: { - username: string; - agent: string; - } + op: MSAgentProtocolMessageType.AddUser; + data: { + username: string; + agent: string; + }; } export interface MSAgentRemoveUserMessage extends MSAgentProtocolMessage { - op: MSAgentProtocolMessageType.RemoveUser, - data: { - username: string; - } + op: MSAgentProtocolMessageType.RemoveUser; + data: { + username: string; + }; } export interface MSAgentChatMessage extends MSAgentProtocolMessage { - op: MSAgentProtocolMessageType.Chat, - data: { - username: string; - message: string; - audio? : string | undefined; - } + op: MSAgentProtocolMessageType.Chat; + data: { + username: string; + message: string; + audio?: string | undefined; + }; } export interface MSAgentPromoteMessage extends MSAgentProtocolMessage { - op: MSAgentProtocolMessageType.Promote, - data: { - username: string; - } + op: MSAgentProtocolMessageType.Promote; + data: { + username: string; + }; } export interface MSAgentErrorMessage extends MSAgentProtocolMessage { - op: MSAgentProtocolMessageType.Error, - data: { - error: string; - } -} \ No newline at end of file + op: MSAgentProtocolMessageType.Error; + data: { + error: string; + }; +} diff --git a/server/src/client.ts b/server/src/client.ts index 9b54e59..b3e3579 100644 --- a/server/src/client.ts +++ b/server/src/client.ts @@ -1,236 +1,246 @@ -import EventEmitter from "events"; -import { WebSocket } from "ws"; -import { MSAgentAdminBanMessage, MSAgentAdminGetIPMessage, MSAgentAdminGetIPResponse, MSAgentAdminKickMessage, MSAgentAdminLoginMessage, MSAgentAdminLoginResponse, MSAgentAdminMessage, MSAgentAdminOperation, MSAgentErrorMessage, MSAgentJoinMessage, MSAgentProtocolMessage, MSAgentProtocolMessageType, MSAgentTalkMessage } from '@msagent-chat/protocol'; -import { MSAgentChatRoom } from "./room.js"; +import EventEmitter from 'events'; +import { WebSocket } from 'ws'; +import { + MSAgentAdminBanMessage, + MSAgentAdminGetIPMessage, + MSAgentAdminGetIPResponse, + MSAgentAdminKickMessage, + MSAgentAdminLoginMessage, + MSAgentAdminLoginResponse, + MSAgentAdminMessage, + MSAgentAdminOperation, + MSAgentErrorMessage, + MSAgentJoinMessage, + MSAgentProtocolMessage, + MSAgentProtocolMessageType, + MSAgentTalkMessage +} from '@msagent-chat/protocol'; +import { MSAgentChatRoom } from './room.js'; import * as htmlentities from 'html-entities'; -import RateLimiter from "./ratelimiter.js"; -import { createHash } from "crypto"; +import RateLimiter from './ratelimiter.js'; +import { createHash } from 'crypto'; // Event types 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: 'join', listener: () => void): this; + on(event: 'close', listener: () => void): this; + on(event: 'talk', listener: (msg: string) => void): this; - on(event: string, listener: Function): this; + on(event: string, listener: Function): this; } export class Client extends EventEmitter { - ip: string; - username: string | null; - agent: string | null; - admin: boolean; + ip: string; + username: string | null; + agent: string | null; + admin: boolean; - room: MSAgentChatRoom; - socket: WebSocket; + room: MSAgentChatRoom; + socket: WebSocket; - nopTimer: NodeJS.Timeout | undefined; - nopLevel: number; + nopTimer: NodeJS.Timeout | undefined; + nopLevel: number; - chatRateLimit: RateLimiter + chatRateLimit: RateLimiter; - constructor(socket: WebSocket, room: MSAgentChatRoom, ip: string) { - super(); - this.socket = socket; - this.ip = ip; - this.room = room; - this.username = null; - this.agent = null; - this.admin = false; - this.resetNop(); - this.nopLevel = 0; - - this.chatRateLimit = new RateLimiter(this.room.config.ratelimits.chat); + constructor(socket: WebSocket, room: MSAgentChatRoom, ip: string) { + super(); + this.socket = socket; + this.ip = ip; + this.room = room; + this.username = null; + this.agent = null; + this.admin = false; + this.resetNop(); + this.nopLevel = 0; - this.socket.on('message', (msg, isBinary) => { - if (isBinary) { - this.socket.close(); - return; - } - this.parseMessage(msg.toString("utf-8")); - }); - this.socket.on('error', () => {}); - this.socket.on('close', () => { - this.emit('close'); - }); - } + this.chatRateLimit = new RateLimiter(this.room.config.ratelimits.chat); - send(msg: MSAgentProtocolMessage) { - return new Promise((res, rej) => { - if (this.socket.readyState !== WebSocket.OPEN) { - res(); - return; - } - this.socket.send(JSON.stringify(msg), err => { - if (err) { - rej(err); - return; - } - res(); - }); - }); - } + this.socket.on('message', (msg, isBinary) => { + if (isBinary) { + this.socket.close(); + return; + } + this.parseMessage(msg.toString('utf-8')); + }); + this.socket.on('error', () => {}); + this.socket.on('close', () => { + this.emit('close'); + }); + } - private resetNop() { - clearInterval(this.nopTimer); - this.nopLevel = 0; - this.nopTimer = setInterval(() => { - if (this.nopLevel++ >= 3) { - this.socket.close(); - } else { - this.send({ - op: MSAgentProtocolMessageType.KeepAlive - }); - } - }, 10000) - } + send(msg: MSAgentProtocolMessage) { + return new Promise((res, rej) => { + if (this.socket.readyState !== WebSocket.OPEN) { + res(); + return; + } + this.socket.send(JSON.stringify(msg), (err) => { + if (err) { + rej(err); + return; + } + res(); + }); + }); + } - private async parseMessage(data: string) { - let msg: MSAgentProtocolMessage; - try { - msg = JSON.parse(data); - } catch { - this.socket.close(); - return; - } - this.resetNop(); - switch (msg.op) { - case MSAgentProtocolMessageType.Join: { - let joinMsg = msg as MSAgentJoinMessage; - if (!joinMsg.data || !joinMsg.data.username || !joinMsg.data.username) { - this.socket.close(); - return; - } - let username = joinMsg.data.username.trim(); - if (!validateUsername(username)) { - let msg: MSAgentErrorMessage = { - op: MSAgentProtocolMessageType.Error, - data: { - error: "Usernames can contain only numbers, letters, spaces, dashes, underscores, and dots, and it must be between 3 and 20 characters." - } - }; - await this.send(msg); - this.socket.close(); - return; - } - if (this.room.config.bannedWords.some(w => username.indexOf(w) !== -1)) { - this.socket.close(); - return; - } - if (this.room.clients.some(u => u.username === username)) { - let i = 1; - let uo = username; - do { - username = uo + i++; - } while (this.room.clients.some(u => u.username === username)) - } - if (!this.room.agents.some(a => a.filename === joinMsg.data.agent)) { - this.socket.close(); - return; - } - this.username = username; - this.agent = joinMsg.data.agent; - this.emit('join'); - break; - } - case MSAgentProtocolMessageType.Talk: { - let talkMsg = msg as MSAgentTalkMessage; - if (!talkMsg.data || !talkMsg.data.msg || !this.chatRateLimit.request()) { - return; - } - if (talkMsg.data.msg.length > this.room.config.charlimit) return; - if (this.room.config.bannedWords.some(w => talkMsg.data.msg.indexOf(w) !== -1)) { - return; - } - this.emit('talk', talkMsg.data.msg); - break; - } - case MSAgentProtocolMessageType.Admin: { - let adminMsg = msg as MSAgentAdminMessage; - if (!adminMsg.data) return; - switch (adminMsg.data.action) { - case MSAgentAdminOperation.Login: { - let loginMsg = adminMsg as MSAgentAdminLoginMessage; - if (this.admin || !loginMsg.data.password) return; - let sha256 = createHash("sha256"); - sha256.update(loginMsg.data.password); - let hash = sha256.digest("hex"); - sha256.destroy(); - let success = false; - if (hash === this.room.config.adminPasswordHash) { - this.admin = true; - success = true; - this.emit('admin'); - } - let res : MSAgentAdminLoginResponse = { - op: MSAgentProtocolMessageType.Admin, - data: { - action: MSAgentAdminOperation.Login, - success - } - } - this.send(res); - break; - } - case MSAgentAdminOperation.GetIP: { - let getIPMsg = adminMsg as MSAgentAdminGetIPMessage; - if (!this.admin || !getIPMsg.data || !getIPMsg.data.username) return; - let _user = this.room.clients.find(c => c.username === getIPMsg.data.username); - if (!_user) return; - let res: MSAgentAdminGetIPResponse = { - op: MSAgentProtocolMessageType.Admin, - data: { - action: MSAgentAdminOperation.GetIP, - username: _user.username!, - ip: _user.ip - } - }; - this.send(res); - break; - } - case MSAgentAdminOperation.Kick: { - let kickMsg = adminMsg as MSAgentAdminKickMessage; - if (!this.admin || !kickMsg.data || !kickMsg.data.username) return; - let _user = this.room.clients.find(c => c.username === kickMsg.data.username); - if (!_user) return; - let res: MSAgentErrorMessage = { - op: MSAgentProtocolMessageType.Error, - data: { - error: "You have been kicked." - } - }; - await _user.send(res); - _user.socket.close(); - break; - } - case MSAgentAdminOperation.Ban: { - let banMsg = adminMsg as MSAgentAdminBanMessage; - if (!this.admin || !banMsg.data || !banMsg.data.username) return; - let _user = this.room.clients.find(c => c.username === banMsg.data.username); - if (!_user) return; - let res: MSAgentErrorMessage = { - op: MSAgentProtocolMessageType.Error, - data: { - error: "You have been banned." - } - }; - await this.room.db.banUser(_user.ip, _user.username!); - await _user.send(res); - _user.socket.close(); - break; - } - } - break; - } - } - } + private resetNop() { + clearInterval(this.nopTimer); + this.nopLevel = 0; + this.nopTimer = setInterval(() => { + if (this.nopLevel++ >= 3) { + this.socket.close(); + } else { + this.send({ + op: MSAgentProtocolMessageType.KeepAlive + }); + } + }, 10000); + } + + private async parseMessage(data: string) { + let msg: MSAgentProtocolMessage; + try { + msg = JSON.parse(data); + } catch { + this.socket.close(); + return; + } + this.resetNop(); + switch (msg.op) { + case MSAgentProtocolMessageType.Join: { + let joinMsg = msg as MSAgentJoinMessage; + if (!joinMsg.data || !joinMsg.data.username || !joinMsg.data.username) { + this.socket.close(); + return; + } + let username = joinMsg.data.username.trim(); + if (!validateUsername(username)) { + let msg: MSAgentErrorMessage = { + op: MSAgentProtocolMessageType.Error, + data: { + error: 'Usernames can contain only numbers, letters, spaces, dashes, underscores, and dots, and it must be between 3 and 20 characters.' + } + }; + await this.send(msg); + this.socket.close(); + return; + } + if (this.room.config.bannedWords.some((w) => username.indexOf(w) !== -1)) { + this.socket.close(); + return; + } + if (this.room.clients.some((u) => u.username === username)) { + let i = 1; + let uo = username; + do { + username = uo + i++; + } while (this.room.clients.some((u) => u.username === username)); + } + if (!this.room.agents.some((a) => a.filename === joinMsg.data.agent)) { + this.socket.close(); + return; + } + this.username = username; + this.agent = joinMsg.data.agent; + this.emit('join'); + break; + } + case MSAgentProtocolMessageType.Talk: { + let talkMsg = msg as MSAgentTalkMessage; + if (!talkMsg.data || !talkMsg.data.msg || !this.chatRateLimit.request()) { + return; + } + if (talkMsg.data.msg.length > this.room.config.charlimit) return; + if (this.room.config.bannedWords.some((w) => talkMsg.data.msg.indexOf(w) !== -1)) { + return; + } + this.emit('talk', talkMsg.data.msg); + break; + } + case MSAgentProtocolMessageType.Admin: { + let adminMsg = msg as MSAgentAdminMessage; + if (!adminMsg.data) return; + switch (adminMsg.data.action) { + case MSAgentAdminOperation.Login: { + let loginMsg = adminMsg as MSAgentAdminLoginMessage; + if (this.admin || !loginMsg.data.password) return; + let sha256 = createHash('sha256'); + sha256.update(loginMsg.data.password); + let hash = sha256.digest('hex'); + sha256.destroy(); + let success = false; + if (hash === this.room.config.adminPasswordHash) { + this.admin = true; + success = true; + this.emit('admin'); + } + let res: MSAgentAdminLoginResponse = { + op: MSAgentProtocolMessageType.Admin, + data: { + action: MSAgentAdminOperation.Login, + success + } + }; + this.send(res); + break; + } + case MSAgentAdminOperation.GetIP: { + let getIPMsg = adminMsg as MSAgentAdminGetIPMessage; + if (!this.admin || !getIPMsg.data || !getIPMsg.data.username) return; + let _user = this.room.clients.find((c) => c.username === getIPMsg.data.username); + if (!_user) return; + let res: MSAgentAdminGetIPResponse = { + op: MSAgentProtocolMessageType.Admin, + data: { + action: MSAgentAdminOperation.GetIP, + username: _user.username!, + ip: _user.ip + } + }; + this.send(res); + break; + } + case MSAgentAdminOperation.Kick: { + let kickMsg = adminMsg as MSAgentAdminKickMessage; + if (!this.admin || !kickMsg.data || !kickMsg.data.username) return; + let _user = this.room.clients.find((c) => c.username === kickMsg.data.username); + if (!_user) return; + let res: MSAgentErrorMessage = { + op: MSAgentProtocolMessageType.Error, + data: { + error: 'You have been kicked.' + } + }; + await _user.send(res); + _user.socket.close(); + break; + } + case MSAgentAdminOperation.Ban: { + let banMsg = adminMsg as MSAgentAdminBanMessage; + if (!this.admin || !banMsg.data || !banMsg.data.username) return; + let _user = this.room.clients.find((c) => c.username === banMsg.data.username); + if (!_user) return; + let res: MSAgentErrorMessage = { + op: MSAgentProtocolMessageType.Error, + data: { + error: 'You have been banned.' + } + }; + await this.room.db.banUser(_user.ip, _user.username!); + await _user.send(res); + _user.socket.close(); + break; + } + } + break; + } + } + } } function validateUsername(username: string) { - return ( - username.length >= 3 && - username.length <= 20 && - /^[a-zA-Z0-9\ \-\_\.]+$/.test(username) - ); -} \ No newline at end of file + return username.length >= 3 && username.length <= 20 && /^[a-zA-Z0-9\ \-\_\.]+$/.test(username); +} diff --git a/server/src/config.ts b/server/src/config.ts index f9dbd19..c9b197b 100644 --- a/server/src/config.ts +++ b/server/src/config.ts @@ -1,60 +1,59 @@ export interface IConfig { - http: { - host: string; - port: number; - proxied: boolean; - } - mysql: MySQLConfig; - chat: ChatConfig; - motd: motdConfig; - discord: DiscordConfig; - tts: TTSConfig; - agents: AgentConfig[]; + http: { + host: string; + port: number; + proxied: boolean; + }; + mysql: MySQLConfig; + chat: ChatConfig; + motd: motdConfig; + discord: DiscordConfig; + tts: TTSConfig; + agents: AgentConfig[]; } export interface TTSConfig { - enabled: boolean; - server: string; - voice: string; - tempDir: string; - wavExpirySeconds: number; + enabled: boolean; + server: string; + voice: string; + tempDir: string; + wavExpirySeconds: number; } export interface ChatConfig { - charlimit: number; - agentsDir: string; - maxConnectionsPerIP: number; - adminPasswordHash: string; - bannedWords: string[]; - ratelimits: { - chat: RateLimitConfig; - } + charlimit: number; + agentsDir: string; + maxConnectionsPerIP: number; + adminPasswordHash: string; + bannedWords: string[]; + ratelimits: { + chat: RateLimitConfig; + }; } export interface motdConfig { - version: number; - html: string; + version: number; + html: string; } export interface AgentConfig { - friendlyName: string; - filename: string; + friendlyName: string; + filename: string; } - export interface RateLimitConfig { - seconds: number; - limit: number; + seconds: number; + limit: number; } export interface MySQLConfig { - host: string; - username: string; - password: string; - database: string; + host: string; + username: string; + password: string; + database: string; } export interface DiscordConfig { - enabled: boolean; - webhookURL: string; -} \ No newline at end of file + enabled: boolean; + webhookURL: string; +} diff --git a/server/src/database.ts b/server/src/database.ts index 04237aa..355fdcd 100644 --- a/server/src/database.ts +++ b/server/src/database.ts @@ -1,38 +1,38 @@ -import { MySQLConfig } from "./config.js"; +import { MySQLConfig } from './config.js'; import * as mysql from 'mysql2/promise'; export class Database { - private config: MySQLConfig; - private db: mysql.Pool; - - constructor(config: MySQLConfig) { - this.config = config; - this.db = mysql.createPool({ - host: this.config.host, - user: this.config.username, - password: this.config.password, - database: this.config.database, - connectionLimit: 10, - multipleStatements: false - }); - } + private config: MySQLConfig; + private db: mysql.Pool; - async init() { - let conn = await this.db.getConnection(); - await conn.execute("CREATE TABLE IF NOT EXISTS bans (ip VARCHAR(45) NOT NULL PRIMARY KEY, username TEXT NOT NULL, time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP());"); - conn.release(); - } + constructor(config: MySQLConfig) { + this.config = config; + this.db = mysql.createPool({ + host: this.config.host, + user: this.config.username, + password: this.config.password, + database: this.config.database, + connectionLimit: 10, + multipleStatements: false + }); + } - async banUser(ip: string, username: string) { - let conn = await this.db.getConnection(); - await conn.execute("INSERT INTO bans (ip, username) VALUES (?, ?)", [ip, username]); - conn.release(); - } + async init() { + let conn = await this.db.getConnection(); + await conn.execute('CREATE TABLE IF NOT EXISTS bans (ip VARCHAR(45) NOT NULL PRIMARY KEY, username TEXT NOT NULL, time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP());'); + conn.release(); + } - async isUserBanned(ip: string): Promise { - let conn = await this.db.getConnection(); - let res = await conn.query("SELECT COUNT(ip) AS cnt FROM bans WHERE ip = ?", [ip]) as mysql.RowDataPacket; - conn.release(); - return res[0][0]["cnt"] !== 0; - } -} \ No newline at end of file + async banUser(ip: string, username: string) { + let conn = await this.db.getConnection(); + await conn.execute('INSERT INTO bans (ip, username) VALUES (?, ?)', [ip, username]); + conn.release(); + } + + async isUserBanned(ip: string): Promise { + let conn = await this.db.getConnection(); + let res = (await conn.query('SELECT COUNT(ip) AS cnt FROM bans WHERE ip = ?', [ip])) as mysql.RowDataPacket; + conn.release(); + return res[0][0]['cnt'] !== 0; + } +} diff --git a/server/src/discord.ts b/server/src/discord.ts index 1a65ae3..41c73b0 100644 --- a/server/src/discord.ts +++ b/server/src/discord.ts @@ -1,18 +1,18 @@ -import { WebhookClient } from "discord.js"; -import { DiscordConfig } from "./config.js"; +import { WebhookClient } from 'discord.js'; +import { DiscordConfig } from './config.js'; export class DiscordLogger { - private webhook: WebhookClient; - - constructor(config: DiscordConfig) { - this.webhook = new WebhookClient({url: config.webhookURL}); - } + private webhook: WebhookClient; - logMsg(username: string, msg: string) { - this.webhook.send({ - username, - allowedMentions: {}, - content: msg, - }); - } -} \ No newline at end of file + constructor(config: DiscordConfig) { + this.webhook = new WebhookClient({ url: config.webhookURL }); + } + + logMsg(username: string, msg: string) { + this.webhook.send({ + username, + allowedMentions: {}, + content: msg + }); + } +} diff --git a/server/src/index.ts b/server/src/index.ts index b2df294..baed029 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -15,29 +15,27 @@ import { DiscordLogger } from './discord.js'; let config: IConfig; let configPath: string; -if (process.argv.length < 3) - configPath = "./config.toml"; -else - configPath = process.argv[2]; +if (process.argv.length < 3) configPath = './config.toml'; +else configPath = process.argv[2]; if (!fs.existsSync(configPath)) { - console.error(`${configPath} not found. Please copy config.example.toml and fill out fields.`); - process.exit(1); + console.error(`${configPath} not found. Please copy config.example.toml and fill out fields.`); + process.exit(1); } try { - let configRaw = fs.readFileSync(configPath, "utf-8"); - config = toml.parse(configRaw); + let configRaw = fs.readFileSync(configPath, 'utf-8'); + config = toml.parse(configRaw); } catch (e) { - console.error(`Failed to read or parse ${configPath}: ${(e as Error).message}`); - process.exit(1); + console.error(`Failed to read or parse ${configPath}: ${(e as Error).message}`); + process.exit(1); } let db = new Database(config.mysql); await db.init(); const app = Fastify({ - logger: true, + logger: true }); app.register(FastifyWS); @@ -45,100 +43,97 @@ app.register(FastifyWS); let tts = null; if (config.tts.enabled) { - tts = new TTSClient(config.tts); - app.register(FastifyStatic, { - root: config.tts.tempDir, - prefix: "/api/tts/", - decorateReply: false - }); + tts = new TTSClient(config.tts); + app.register(FastifyStatic, { + root: config.tts.tempDir, + prefix: '/api/tts/', + decorateReply: false + }); } -if (!config.chat.agentsDir.endsWith("/")) config.chat.agentsDir += "/"; +if (!config.chat.agentsDir.endsWith('/')) config.chat.agentsDir += '/'; if (!fs.existsSync(config.chat.agentsDir)) { - console.error(`Directory ${config.chat.agentsDir} does not exist.`); - process.exit(1); + console.error(`Directory ${config.chat.agentsDir} does not exist.`); + process.exit(1); } for (let agent of config.agents) { - if (!fs.existsSync(path.join(config.chat.agentsDir, agent.filename))) { - console.error(`${agent.filename} does not exist.`); - process.exit(1); - } + if (!fs.existsSync(path.join(config.chat.agentsDir, agent.filename))) { + console.error(`${agent.filename} does not exist.`); + process.exit(1); + } } app.register(FastifyStatic, { - root: path.resolve(config.chat.agentsDir), - prefix: "/api/agents/", - decorateReply: true, + root: path.resolve(config.chat.agentsDir), + prefix: '/api/agents/', + decorateReply: true }); -app.get("/api/agents", (req, res) => { - return config.agents; +app.get('/api/agents', (req, res) => { + return config.agents; }); // MOTD -app.get("/api/motd/version", (req, res) => { - res.header("Content-Type", "text/plain"); - return config.motd.version.toString(); +app.get('/api/motd/version', (req, res) => { + res.header('Content-Type', 'text/plain'); + return config.motd.version.toString(); }); -app.get("/api/motd/html", (req, res) => { - res.header("Content-Type", "text/html"); - return config.motd.html; +app.get('/api/motd/html', (req, res) => { + res.header('Content-Type', 'text/html'); + return config.motd.html; }); // Discord let discord = null; 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); -app.register(async app => { - app.get("/api/socket", {websocket: true}, async (socket, req) => { - // TODO: Do this pre-upgrade and return the appropriate status codes - let ip: string; - if (config.http.proxied) { - if (req.headers["x-forwarded-for"] === undefined) { - console.error(`Warning: X-Forwarded-For not set! This is likely a misconfiguration of your reverse proxy.`); - socket.close(); - return; - } - let xff = req.headers["x-forwarded-for"]; - if (xff instanceof Array) - ip = xff[0]; - else - ip = xff; - if (!isIP(ip)) { - console.error(`Warning: X-Forwarded-For malformed! This is likely a misconfiguration of your reverse proxy.`); - socket.close(); - return; - } - } else { - ip = req.ip; - } - if (await db.isUserBanned(ip)) { - let msg: MSAgentErrorMessage = { - op: MSAgentProtocolMessageType.Error, - data: { - error: "You have been banned." - } - } - socket.send(JSON.stringify(msg), () => { - socket.close(); - }); - return; - } - let o = room.clients.filter(c => c.ip === ip); - if (o.length >= config.chat.maxConnectionsPerIP) { - o[0].socket.close(); - } - let client = new Client(socket, room, ip); - room.addClient(client); - }); +app.register(async (app) => { + app.get('/api/socket', { websocket: true }, async (socket, req) => { + // TODO: Do this pre-upgrade and return the appropriate status codes + let ip: string; + if (config.http.proxied) { + if (req.headers['x-forwarded-for'] === undefined) { + console.error(`Warning: X-Forwarded-For not set! This is likely a misconfiguration of your reverse proxy.`); + socket.close(); + return; + } + let xff = req.headers['x-forwarded-for']; + if (xff instanceof Array) ip = xff[0]; + else ip = xff; + if (!isIP(ip)) { + console.error(`Warning: X-Forwarded-For malformed! This is likely a misconfiguration of your reverse proxy.`); + socket.close(); + return; + } + } else { + ip = req.ip; + } + if (await db.isUserBanned(ip)) { + let msg: MSAgentErrorMessage = { + op: MSAgentProtocolMessageType.Error, + data: { + error: 'You have been banned.' + } + }; + socket.send(JSON.stringify(msg), () => { + socket.close(); + }); + return; + } + let o = room.clients.filter((c) => c.ip === ip); + if (o.length >= config.chat.maxConnectionsPerIP) { + o[0].socket.close(); + } + let client = new Client(socket, room, ip); + room.addClient(client); + }); }); - -app.listen({host: config.http.host, port: config.http.port}); \ No newline at end of file +app.listen({ host: config.http.host, port: config.http.port }); diff --git a/server/src/ratelimiter.ts b/server/src/ratelimiter.ts index fac0546..8114bfc 100644 --- a/server/src/ratelimiter.ts +++ b/server/src/ratelimiter.ts @@ -32,4 +32,4 @@ export default class RateLimiter extends EventEmitter { } return true; } -} \ No newline at end of file +} diff --git a/server/src/room.ts b/server/src/room.ts index 86d13ae..484c4f5 100644 --- a/server/src/room.ts +++ b/server/src/room.ts @@ -1,103 +1,113 @@ -import { MSAgentAddUserMessage, MSAgentChatMessage, MSAgentInitMessage, MSAgentPromoteMessage, MSAgentProtocolMessage, MSAgentProtocolMessageType, MSAgentRemoveUserMessage } from "@msagent-chat/protocol"; -import { Client } from "./client.js"; -import { TTSClient } from "./tts.js"; -import { AgentConfig, ChatConfig } from "./config.js"; +import { + MSAgentAddUserMessage, + MSAgentChatMessage, + MSAgentInitMessage, + MSAgentPromoteMessage, + MSAgentProtocolMessage, + MSAgentProtocolMessageType, + MSAgentRemoveUserMessage +} from '@msagent-chat/protocol'; +import { Client } from './client.js'; +import { TTSClient } from './tts.js'; +import { AgentConfig, ChatConfig } from './config.js'; import * as htmlentities from 'html-entities'; -import { Database } from "./database.js"; -import { DiscordLogger } from "./discord.js"; +import { Database } from './database.js'; +import { DiscordLogger } from './discord.js'; export class MSAgentChatRoom { - agents: AgentConfig[]; - clients: Client[]; - tts: TTSClient | null; - msgId : number = 0; - config: ChatConfig; - db: Database; - discord: DiscordLogger | null; + agents: AgentConfig[]; + clients: Client[]; + tts: TTSClient | null; + msgId: number = 0; + config: ChatConfig; + db: Database; + discord: DiscordLogger | null; - constructor(config: ChatConfig, agents: AgentConfig[], db: Database, tts: TTSClient | null, discord: DiscordLogger | null) { - this.agents = agents; - this.clients = []; - this.config = config; - this.tts = tts; - this.db = db; - this.discord = discord; - } + constructor(config: ChatConfig, agents: AgentConfig[], db: Database, tts: TTSClient | null, discord: DiscordLogger | null) { + this.agents = agents; + this.clients = []; + this.config = config; + this.tts = tts; + this.db = db; + this.discord = discord; + } - addClient(client: Client) { - this.clients.push(client); - client.on('close', () => { - this.clients.splice(this.clients.indexOf(client), 1); - if (client.username === null) return; - let msg: MSAgentRemoveUserMessage = { - op: MSAgentProtocolMessageType.RemoveUser, - data: { - username: client.username - } - }; - for (const _client of this.getActiveClients()) { - _client.send(msg); - } - }); - client.on('join', () => { - let initmsg : MSAgentInitMessage = { - op: MSAgentProtocolMessageType.Init, - data: { - username: client.username!, - agent: client.agent!, - charlimit: this.config.charlimit, - users: this.clients.filter(c => c.username !== null).map(c => { - return { - username: c.username!, - agent: c.agent!, - admin: c.admin - } - }) - } - }; - client.send(initmsg); - let msg: MSAgentAddUserMessage = { - op: MSAgentProtocolMessageType.AddUser, - data: { - username: client.username!, - agent: client.agent! - } - } - for (const _client of this.getActiveClients().filter(c => c !== client)) { - _client.send(msg); - } - }); - client.on('talk', async message => { - let msg: MSAgentChatMessage = { - op: MSAgentProtocolMessageType.Chat, - data: { - username: client.username!, - message: message - } - }; - if (this.tts !== null) { - let filename = await this.tts.synthesizeToFile(message, (++this.msgId).toString(10)); - msg.data.audio = "/api/tts/" + filename; - } - for (const _client of this.getActiveClients()) { - _client.send(msg); - } - this.discord?.logMsg(client.username!, message); - }); - client.on('admin', () => { - let msg: MSAgentPromoteMessage = { - op: MSAgentProtocolMessageType.Promote, - data: { - username: client.username! - } - }; - for (const _client of this.getActiveClients()) { - _client.send(msg); - } - }); - } + addClient(client: Client) { + this.clients.push(client); + client.on('close', () => { + this.clients.splice(this.clients.indexOf(client), 1); + if (client.username === null) return; + let msg: MSAgentRemoveUserMessage = { + op: MSAgentProtocolMessageType.RemoveUser, + data: { + username: client.username + } + }; + for (const _client of this.getActiveClients()) { + _client.send(msg); + } + }); + client.on('join', () => { + let initmsg: MSAgentInitMessage = { + op: MSAgentProtocolMessageType.Init, + data: { + username: client.username!, + agent: client.agent!, + charlimit: this.config.charlimit, + users: this.clients + .filter((c) => c.username !== null) + .map((c) => { + return { + username: c.username!, + agent: c.agent!, + admin: c.admin + }; + }) + } + }; + client.send(initmsg); + let msg: MSAgentAddUserMessage = { + op: MSAgentProtocolMessageType.AddUser, + data: { + username: client.username!, + agent: client.agent! + } + }; + for (const _client of this.getActiveClients().filter((c) => c !== client)) { + _client.send(msg); + } + }); + client.on('talk', async (message) => { + let msg: MSAgentChatMessage = { + op: MSAgentProtocolMessageType.Chat, + data: { + username: client.username!, + message: message + } + }; + if (this.tts !== null) { + let filename = await this.tts.synthesizeToFile(message, (++this.msgId).toString(10)); + msg.data.audio = '/api/tts/' + filename; + } + for (const _client of this.getActiveClients()) { + _client.send(msg); + } + this.discord?.logMsg(client.username!, message); + }); + client.on('admin', () => { + let msg: MSAgentPromoteMessage = { + op: MSAgentProtocolMessageType.Promote, + data: { + username: client.username! + } + }; + for (const _client of this.getActiveClients()) { + _client.send(msg); + } + }); + } - private getActiveClients() { - return this.clients.filter(c => c.username !== null); - } -} \ No newline at end of file + private getActiveClients() { + return this.clients.filter((c) => c.username !== null); + } +} diff --git a/server/src/tts.ts b/server/src/tts.ts index 3bf7a91..9d74137 100644 --- a/server/src/tts.ts +++ b/server/src/tts.ts @@ -1,70 +1,73 @@ -import path from "path"; +import path from 'path'; import * as fs from 'fs/promises'; -import { TTSConfig } from "./config.js"; -import { Readable } from "node:stream"; +import { TTSConfig } from './config.js'; +import { Readable } from 'node:stream'; import { ReadableStream } from 'node:stream/web'; -import { finished } from "node:stream/promises"; +import { finished } from 'node:stream/promises'; export class TTSClient { - private config: TTSConfig; - private deleteOps: Map + private config: TTSConfig; + private deleteOps: Map; - constructor(config: TTSConfig) { - this.config = config; - if (!this.config.tempDir.endsWith('/')) this.config.tempDir += '/'; - this.deleteOps = new Map(); - } + constructor(config: TTSConfig) { + this.config = config; + if (!this.config.tempDir.endsWith('/')) this.config.tempDir += '/'; + this.deleteOps = new Map(); + } - async ensureDirectoryExists() { - let stat; - try { - stat = await fs.stat(this.config.tempDir); - } catch (e) { - let error = e as NodeJS.ErrnoException; - switch (error.code) { - case "ENOTDIR": { - console.warn("File exists at TTS temp directory path. Unlinking..."); - await fs.unlink(this.config.tempDir.substring(0, this.config.tempDir.length - 1)); - // intentional fall-through - } - case "ENOENT": { - await fs.mkdir(this.config.tempDir, {recursive: true}); - break; - } - default: { - console.error(`Cannot access TTS Temp dir: ${error.message}`); - process.exit(1); - break; - } - } - } - } + async ensureDirectoryExists() { + let stat; + try { + stat = await fs.stat(this.config.tempDir); + } catch (e) { + let error = e as NodeJS.ErrnoException; + switch (error.code) { + case 'ENOTDIR': { + console.warn('File exists at TTS temp directory path. Unlinking...'); + await fs.unlink(this.config.tempDir.substring(0, this.config.tempDir.length - 1)); + // intentional fall-through + } + case 'ENOENT': { + await fs.mkdir(this.config.tempDir, { recursive: true }); + break; + } + default: { + console.error(`Cannot access TTS Temp dir: ${error.message}`); + process.exit(1); + break; + } + } + } + } - async synthesizeToFile(text: string, id: string) : Promise { - this.ensureDirectoryExists(); - let wavFilename = id + ".wav" - let wavPath = path.join(this.config.tempDir, wavFilename); - try { - await fs.unlink(wavPath); - } catch {} - let file = await fs.open(wavPath, fs.constants.O_CREAT | fs.constants.O_TRUNC | fs.constants.O_WRONLY); - let stream = file.createWriteStream(); - let res = await fetch(this.config.server + "/api/synthesize", { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ - text, - voice: this.config.voice - }) - }); - await finished(Readable.fromWeb(res.body as ReadableStream).pipe(stream)); - await file.close(); - this.deleteOps.set(wavPath, setTimeout(async () => { - await fs.unlink(wavPath); - this.deleteOps.delete(wavPath); - }, this.config.wavExpirySeconds * 1000)); - return wavFilename; - } -} \ No newline at end of file + async synthesizeToFile(text: string, id: string): Promise { + this.ensureDirectoryExists(); + let wavFilename = id + '.wav'; + let wavPath = path.join(this.config.tempDir, wavFilename); + try { + await fs.unlink(wavPath); + } catch {} + let file = await fs.open(wavPath, fs.constants.O_CREAT | fs.constants.O_TRUNC | fs.constants.O_WRONLY); + let stream = file.createWriteStream(); + let res = await fetch(this.config.server + '/api/synthesize', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + text, + voice: this.config.voice + }) + }); + await finished(Readable.fromWeb(res.body as ReadableStream).pipe(stream)); + await file.close(); + this.deleteOps.set( + wavPath, + setTimeout(async () => { + await fs.unlink(wavPath); + this.deleteOps.delete(wavPath); + }, this.config.wavExpirySeconds * 1000) + ); + return wavFilename; + } +} diff --git a/webapp/config.ts b/webapp/config.ts index bd75dce..98439ed 100644 --- a/webapp/config.ts +++ b/webapp/config.ts @@ -1,4 +1,4 @@ export const Config = { - // The server address for the webapp to connect to. The below default is the same address the webapp is hosted at. - serverAddress: `${window.location.protocol}//${window.location.host}` -} \ No newline at end of file + // The server address for the webapp to connect to. The below default is the same address the webapp is hosted at. + serverAddress: `${window.location.protocol}//${window.location.host}` +}; diff --git a/webapp/src/html/index.html b/webapp/src/html/index.html index 78e2232..96159a3 100644 --- a/webapp/src/html/index.html +++ b/webapp/src/html/index.html @@ -1,130 +1,130 @@ - + - - MSAgent Chat - - - - - - - - - -
-
-
-
Log on to Agent Chat
-
-
- -
-
- - -
-
- - -
-
- - -
-
-
-
+ + MSAgent Chat + + + + + + + + + +
+
+
+
Log on to Agent Chat
+
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+
+
-
-
-
- Welcome to Agent Chat -
-
- -
-
-
-
-
-
+
+
+
Welcome to Agent Chat
+
+ +
+
+
+
+
+
- -
-
-
-
-
Room Settings
-
- - -
-
-
- -
  • Wallpaper
  • -
    -
    -
    -
    -
    -
    - -
    -
    -
    - Select a picture or enter a URL to use as a wallpaper: -
    -
    -
      - -
    • None
    • -
    • Blue Lace 16
    • -
    • Boiling Point
    • -
    • Chateau
    • -
    • Coffee Bean
    • -
    • Fall Memories
    • -
    • FeatherTexture
    • -
    - -
    -
    -
    - - -
    -
    - - -
    -
    -
    -
    -
    -
    -
    - - - -
    -
    -
    - -
    - - -
    -
    - - - \ No newline at end of file + +
    +
    +
    +
    +
    Room Settings
    +
    + + +
    +
    +
    + +
  • Wallpaper
  • +
    +
    +
    +
    +
    +
    + +
    +
    +
    + Select a picture or enter a URL to use as a wallpaper: +
    +
    +
      + +
    • None
    • +
    • Blue Lace 16
    • +
    • Boiling Point
    • +
    • Chateau
    • +
    • Coffee Bean
    • +
    • Fall Memories
    • +
    • FeatherTexture
    • +
    + +
    +
    +
    + + +
    +
    + + +
    +
    +
    +
    +
    +
    +
    + + + +
    +
    +
    + +
    + + +
    +
    + + + diff --git a/webapp/src/html/testbed.html b/webapp/src/html/testbed.html index 2e995fc..87fac57 100644 --- a/webapp/src/html/testbed.html +++ b/webapp/src/html/testbed.html @@ -1,4 +1,4 @@ - + MSAgent Chat - testbed @@ -19,13 +19,12 @@
    -
    -
    - -
    +
    +
    + +
    -
    -
    +
    diff --git a/webapp/src/ts/MSWindow.ts b/webapp/src/ts/MSWindow.ts index 434027c..852e145 100644 --- a/webapp/src/ts/MSWindow.ts +++ b/webapp/src/ts/MSWindow.ts @@ -1,110 +1,111 @@ export interface MSWindowConfig { - minWidth: number, - minHeight: number, - maxWidth?: number | undefined, - maxHeight?: number | undefined, - startPosition: MSWindowStartPosition // TODO: Should be a union with the enum and a "Point" (containing X and Y) + minWidth: number; + minHeight: number; + maxWidth?: number | undefined; + maxHeight?: number | undefined; + startPosition: MSWindowStartPosition; // TODO: Should be a union with the enum and a "Point" (containing X and Y) } export enum MSWindowStartPosition { - TopLeft, - Center + TopLeft, + Center } export class MSWindow { - wnd: HTMLDivElement; - closeBtn: HTMLButtonElement | undefined; - config: MSWindowConfig; - titlebar: HTMLDivElement; - body: HTMLDivElement; - shown: boolean; - dragging: boolean; - x: number; - y: number; - constructor(wnd: HTMLDivElement, config: MSWindowConfig) { - this.wnd = wnd; - this.shown = false; - this.config = config; - this.wnd.style.minWidth = config.minWidth + "px"; - this.wnd.style.minHeight = config.minHeight + "px"; + wnd: HTMLDivElement; + closeBtn: HTMLButtonElement | undefined; + config: MSWindowConfig; + titlebar: HTMLDivElement; + body: HTMLDivElement; + shown: boolean; + dragging: boolean; + x: number; + y: number; + constructor(wnd: HTMLDivElement, config: MSWindowConfig) { + this.wnd = wnd; + this.shown = false; + this.config = config; + this.wnd.style.minWidth = config.minWidth + 'px'; + this.wnd.style.minHeight = config.minHeight + 'px'; - if (config.maxWidth) - this.wnd.style.maxWidth = config.maxWidth + "px"; - if (config.maxHeight) - this.wnd.style.maxHeight = config.maxHeight + "px"; + if (config.maxWidth) this.wnd.style.maxWidth = config.maxWidth + 'px'; + if (config.maxHeight) this.wnd.style.maxHeight = config.maxHeight + 'px'; - this.wnd.classList.add("d-none"); + this.wnd.classList.add('d-none'); - let titlebar = this.wnd.querySelector("div.title-bar"); - let body = this.wnd.querySelector("div.window-body"); - if (!titlebar || !body) - throw new Error("MSWindow is missing titlebar or body element."); + let titlebar = this.wnd.querySelector('div.title-bar'); + let body = this.wnd.querySelector('div.window-body'); + if (!titlebar || !body) throw new Error('MSWindow is missing titlebar or body element.'); - this.titlebar = titlebar as HTMLDivElement; - this.body = body as HTMLDivElement; + this.titlebar = titlebar as HTMLDivElement; + this.body = body as HTMLDivElement; - let closeBtn = this.titlebar.querySelector("div.title-bar-controls > button[aria-label='Close']") as HTMLButtonElement; - if (closeBtn) { - this.closeBtn = closeBtn; - closeBtn.addEventListener('click', () => { - this.hide(); - }); - } + let closeBtn = this.titlebar.querySelector("div.title-bar-controls > button[aria-label='Close']") as HTMLButtonElement; + if (closeBtn) { + this.closeBtn = closeBtn; + closeBtn.addEventListener('click', () => { + this.hide(); + }); + } - // Register window move handlers - this.dragging = false; - switch (this.config.startPosition) { - case MSWindowStartPosition.TopLeft: { - this.x = 0; - this.y = 0; - break; - } - case MSWindowStartPosition.Center: { - this.x = (document.documentElement.clientWidth / 2) - (this.config.minWidth / 2); - this.y = (document.documentElement.clientHeight / 2) - (this.config.minHeight / 2); - break; - } - default: { - throw new Error("Invalid start position"); - } - } - this.setLoc(); + // Register window move handlers + this.dragging = false; + switch (this.config.startPosition) { + case MSWindowStartPosition.TopLeft: { + this.x = 0; + this.y = 0; + break; + } + case MSWindowStartPosition.Center: { + this.x = document.documentElement.clientWidth / 2 - this.config.minWidth / 2; + this.y = document.documentElement.clientHeight / 2 - this.config.minHeight / 2; + break; + } + default: { + throw new Error('Invalid start position'); + } + } + this.setLoc(); - this.titlebar.addEventListener('mousedown', () => { - this.dragging = true; - document.addEventListener('mouseup', () => { - this.dragging = false; - }, {once: true}); - }); + this.titlebar.addEventListener('mousedown', () => { + this.dragging = true; + document.addEventListener( + 'mouseup', + () => { + this.dragging = false; + }, + { once: true } + ); + }); - document.addEventListener('mousemove', e => { - if (!this.dragging) return; - this.x += e.movementX; - this.y += e.movementY; - this.setLoc(); - }); + document.addEventListener('mousemove', (e) => { + if (!this.dragging) return; + this.x += e.movementX; + this.y += e.movementY; + this.setLoc(); + }); - window.addEventListener('resize', () => { - this.setLoc(); - }); - } + window.addEventListener('resize', () => { + this.setLoc(); + }); + } - show() { - this.wnd.classList.remove("d-none"); - this.shown = true; - } + show() { + this.wnd.classList.remove('d-none'); + this.shown = true; + } - hide() { - this.wnd.classList.add("d-none"); - this.shown = false; - } + hide() { + this.wnd.classList.add('d-none'); + this.shown = false; + } - private setLoc() { - if (this.x < 0) this.x = 0; - if (this.y < 0) this.y = 0; - if (this.x > document.documentElement.clientWidth - this.config.minWidth) this.x = document.documentElement.clientWidth - this.config.minWidth; - if (this.y > document.documentElement.clientHeight - this.config.minHeight) this.y = document.documentElement.clientHeight - this.config.minHeight; - this.wnd.style.top = this.y + "px"; - this.wnd.style.left = this.x + "px"; - } -} \ No newline at end of file + private setLoc() { + if (this.x < 0) this.x = 0; + if (this.y < 0) this.y = 0; + if (this.x > document.documentElement.clientWidth - this.config.minWidth) this.x = document.documentElement.clientWidth - this.config.minWidth; + if (this.y > document.documentElement.clientHeight - this.config.minHeight) this.y = document.documentElement.clientHeight - this.config.minHeight; + this.wnd.style.top = this.y + 'px'; + this.wnd.style.left = this.x + 'px'; + } +} diff --git a/webapp/src/ts/client.ts b/webapp/src/ts/client.ts index 90b4fa7..949e310 100644 --- a/webapp/src/ts/client.ts +++ b/webapp/src/ts/client.ts @@ -62,14 +62,17 @@ export class MSAgentClient { this.users = []; this.admin = false; - document.addEventListener('keydown', this.loginCb = (e: KeyboardEvent) => { - if (e.key === "l" && e.ctrlKey) { - e.preventDefault(); - let password = window.prompt("Papers, please"); - if (!password) return; - this.login(password); - } - }); + document.addEventListener( + 'keydown', + (this.loginCb = (e: KeyboardEvent) => { + if (e.key === 'l' && e.ctrlKey) { + e.preventDefault(); + let password = window.prompt('Papers, please'); + if (!password) return; + this.login(password); + } + }) + ); } on(event: E, callback: MSAgentClientEvents[E]): Unsubscribe { @@ -82,13 +85,13 @@ export class MSAgentClient { } async getMotd(): Promise { - let res = await fetch(this.url + "/api/motd/version"); + let res = await fetch(this.url + '/api/motd/version'); let vs = await res.text(); let version = parseInt(vs); if (isNaN(version)) { - throw new Error("Version was NaN"); + throw new Error('Version was NaN'); } - res = await fetch(this.url + "/api/motd/html"); + res = await fetch(this.url + '/api/motd/html'); let html = await res.text(); return { version, @@ -178,12 +181,12 @@ export class MSAgentClient { ctx.clearItems(); // Mute let _user = user; - let mute = new ContextMenuItem("Mute", () => { + let mute = new ContextMenuItem('Mute', () => { if (_user.muted) { - mute.setName("Mute"); + mute.setName('Mute'); _user.muted = false; } else { - mute.setName("Unmute"); + mute.setName('Unmute'); _user.muted = true; _user.agent.stopSpeaking(); this.playingAudio.get(_user.username)?.pause(); @@ -193,7 +196,7 @@ export class MSAgentClient { // Admin if (this.admin) { // Get IP - let getip = new ContextMenuItem("Get IP", () => { + let getip = new ContextMenuItem('Get IP', () => { let msg: MSAgentAdminGetIPMessage = { op: MSAgentProtocolMessageType.Admin, data: { @@ -205,7 +208,7 @@ export class MSAgentClient { }); ctx.addItem(getip); // Kick - let kick = new ContextMenuItem("Kick", () => { + let kick = new ContextMenuItem('Kick', () => { let msg: MSAgentAdminKickMessage = { op: MSAgentProtocolMessageType.Admin, data: { @@ -217,7 +220,7 @@ export class MSAgentClient { }); ctx.addItem(kick); // Ban - let ban = new ContextMenuItem("Ban", () => { + let ban = new ContextMenuItem('Ban', () => { let msg: MSAgentAdminBanMessage = { op: MSAgentProtocolMessageType.Admin, data: { @@ -264,7 +267,7 @@ export class MSAgentClient { this.charlimit = initMsg.data.charlimit; for (let _user of initMsg.data.users) { let agent = await agentCreateCharacterFromUrl(this.url + '/api/agents/' + _user.agent); - agent.setUsername(_user.username, _user.admin ? "#FF0000" : "#000000"); + agent.setUsername(_user.username, _user.admin ? '#FF0000' : '#000000'); agent.addToDom(this.agentContainer); agent.show(); let user = new User(_user.username, agent); @@ -277,7 +280,7 @@ export class MSAgentClient { case MSAgentProtocolMessageType.AddUser: { let addUserMsg = msg as MSAgentAddUserMessage; let agent = await agentCreateCharacterFromUrl(this.url + '/api/agents/' + addUserMsg.data.agent); - agent.setUsername(addUserMsg.data.username, "#000000"); + agent.setUsername(addUserMsg.data.username, '#000000'); agent.addToDom(this.agentContainer); agent.show(); let user = new User(addUserMsg.data.username, agent); @@ -292,7 +295,7 @@ export class MSAgentClient { if (!user) return; user.agent.hide(true); if (this.playingAudio.has(user!.username)) { - this.playingAudio.get(user!.username)?.pause(); + this.playingAudio.get(user!.username)?.pause(); this.playingAudio.delete(user!.username); } this.users.splice(this.users.indexOf(user), 1); @@ -317,10 +320,10 @@ export class MSAgentClient { audio.addEventListener('ended', () => { // give a bit of time before the wordballoon disappears setTimeout(() => { - if (this.playingAudio.get(user!.username) === audio) { - user!.agent.stopSpeaking(); - this.playingAudio.delete(user!.username); - } + if (this.playingAudio.get(user!.username) === audio) { + user!.agent.stopSpeaking(); + this.playingAudio.delete(user!.username); + } }, 1000); }); @@ -338,7 +341,7 @@ export class MSAgentClient { this.admin = true; for (const user of this.users) this.setContextMenu(user); } else { - alert("Incorrect password!"); + alert('Incorrect password!'); } break; } @@ -352,10 +355,10 @@ export class MSAgentClient { } case MSAgentProtocolMessageType.Promote: { let promoteMsg = msg as MSAgentPromoteMessage; - let user = this.users.find(u => u.username === promoteMsg.data.username); + let user = this.users.find((u) => u.username === promoteMsg.data.username); if (!user) return; user.admin = true; - user.agent.setUsername(user.username, "#ff0000"); + user.agent.setUsername(user.username, '#ff0000'); break; } case MSAgentProtocolMessageType.Error: { diff --git a/webapp/src/ts/main.ts b/webapp/src/ts/main.ts index 38d0731..2b90187 100644 --- a/webapp/src/ts/main.ts +++ b/webapp/src/ts/main.ts @@ -1,122 +1,121 @@ -import { MSWindow, MSWindowStartPosition } from "./MSWindow.js"; -import { agentInit } from "@msagent-chat/msagent.js"; -import { MSAgentClient } from "./client.js"; -import { Config } from "../../config.js"; - +import { MSWindow, MSWindowStartPosition } from './MSWindow.js'; +import { agentInit } from '@msagent-chat/msagent.js'; +import { MSAgentClient } from './client.js'; +import { Config } from '../../config.js'; const elements = { - motdWindow: document.getElementById("motdWindow") as HTMLDivElement, - motdContainer: document.getElementById("motdContainer") as HTMLDivElement, - rulesLink: document.getElementById("rulesLink") as HTMLAnchorElement, + motdWindow: document.getElementById('motdWindow') as HTMLDivElement, + motdContainer: document.getElementById('motdContainer') as HTMLDivElement, + rulesLink: document.getElementById('rulesLink') as HTMLAnchorElement, - logonView: document.getElementById("logonView") as HTMLDivElement, - logonWindow: document.getElementById("logonWindow") as HTMLDivElement, - logonForm: document.getElementById("logonForm") as HTMLFormElement, - logonUsername: document.getElementById("logonUsername") as HTMLInputElement, - logonButton: document.getElementById("logonButton") as HTMLButtonElement, - agentSelect: document.getElementById("agentSelect") as HTMLSelectElement, + logonView: document.getElementById('logonView') as HTMLDivElement, + logonWindow: document.getElementById('logonWindow') as HTMLDivElement, + logonForm: document.getElementById('logonForm') as HTMLFormElement, + logonUsername: document.getElementById('logonUsername') as HTMLInputElement, + logonButton: document.getElementById('logonButton') as HTMLButtonElement, + agentSelect: document.getElementById('agentSelect') as HTMLSelectElement, - chatView: document.getElementById("chatView") as HTMLDivElement, - chatInput: document.getElementById("chatInput") as HTMLInputElement, - chatSendBtn: document.getElementById("chatSendBtn") as HTMLButtonElement, + chatView: document.getElementById('chatView') as HTMLDivElement, + chatInput: document.getElementById('chatInput') as HTMLInputElement, + chatSendBtn: document.getElementById('chatSendBtn') as HTMLButtonElement, - roomSettingsWindow: document.getElementById("roomSettingsWindow") as HTMLDivElement -} + roomSettingsWindow: document.getElementById('roomSettingsWindow') as HTMLDivElement +}; -let Room : MSAgentClient; +let Room: MSAgentClient; function roomInit() { - Room = new MSAgentClient(Config.serverAddress, elements.chatView); - Room.on('close', () => { - for (let user of Room.getUsers()) { - user.agent.remove(); - } - roomInit(); - loggingIn = false; - elements.logonButton.disabled = false; - logonWindow.show(); - elements.logonView.style.display = "block"; - elements.chatView.style.display = "none"; - }); + Room = new MSAgentClient(Config.serverAddress, elements.chatView); + Room.on('close', () => { + for (let user of Room.getUsers()) { + user.agent.remove(); + } + roomInit(); + loggingIn = false; + elements.logonButton.disabled = false; + logonWindow.show(); + elements.logonView.style.display = 'block'; + elements.chatView.style.display = 'none'; + }); } let motdWindow = new MSWindow(elements.motdWindow, { - minWidth: 600, - minHeight: 300, - maxWidth: 600, - startPosition: MSWindowStartPosition.Center + minWidth: 600, + minHeight: 300, + maxWidth: 600, + startPosition: MSWindowStartPosition.Center }); let logonWindow = new MSWindow(elements.logonWindow, { - minWidth: 500, - minHeight: 275, - startPosition: MSWindowStartPosition.Center + minWidth: 500, + minHeight: 275, + startPosition: MSWindowStartPosition.Center }); let roomSettingsWindow = new MSWindow(elements.roomSettingsWindow, { - minWidth: 398, - minHeight: 442, - startPosition: MSWindowStartPosition.Center + minWidth: 398, + minHeight: 442, + startPosition: MSWindowStartPosition.Center }); logonWindow.show(); // roomSettingsWindow.show(); let loggingIn = false; -elements.logonForm.addEventListener('submit', e => { - e.preventDefault(); - connectToRoom(); +elements.logonForm.addEventListener('submit', (e) => { + e.preventDefault(); + connectToRoom(); }); -elements.chatInput.addEventListener('keypress', e => { - // enter - if (e.key === "Enter") talk(); +elements.chatInput.addEventListener('keypress', (e) => { + // enter + if (e.key === 'Enter') talk(); }); elements.chatSendBtn.addEventListener('click', () => { - talk(); + talk(); }); async function connectToRoom() { - if (!elements.agentSelect.value) { - alert("Please select an agent."); - return; - } - if (loggingIn) return; - loggingIn = true; - elements.logonButton.disabled = true; - await Room.connect(); - await Room.join(elements.logonUsername.value, elements.agentSelect.value); - elements.chatInput.maxLength = Room.getCharlimit(); - logonWindow.hide(); - elements.logonView.style.display = "none"; - elements.chatView.style.display = "block"; -}; + if (!elements.agentSelect.value) { + alert('Please select an agent.'); + return; + } + if (loggingIn) return; + loggingIn = true; + elements.logonButton.disabled = true; + await Room.connect(); + await Room.join(elements.logonUsername.value, elements.agentSelect.value); + elements.chatInput.maxLength = Room.getCharlimit(); + logonWindow.hide(); + elements.logonView.style.display = 'none'; + elements.chatView.style.display = 'block'; +} document.addEventListener('DOMContentLoaded', async () => { - await agentInit(); - for (const agent of await Room.getAgents()) { - let option = document.createElement("option"); - option.innerText = agent.friendlyName; - option.value = agent.filename; - elements.agentSelect.appendChild(option); - } - let motd = await Room.getMotd(); - elements.motdContainer.innerHTML = motd.html; - let ver = localStorage.getItem("msagent-chat-motd-version"); - if (!ver || parseInt(ver) !== motd.version) { - motdWindow.show(); - localStorage.setItem("msagent-chat-motd-version", motd.version.toString()); - } - elements.rulesLink.addEventListener('click', () => { - motdWindow.show(); - }) + await agentInit(); + for (const agent of await Room.getAgents()) { + let option = document.createElement('option'); + option.innerText = agent.friendlyName; + option.value = agent.filename; + elements.agentSelect.appendChild(option); + } + let motd = await Room.getMotd(); + elements.motdContainer.innerHTML = motd.html; + let ver = localStorage.getItem('msagent-chat-motd-version'); + if (!ver || parseInt(ver) !== motd.version) { + motdWindow.show(); + localStorage.setItem('msagent-chat-motd-version', motd.version.toString()); + } + elements.rulesLink.addEventListener('click', () => { + motdWindow.show(); + }); }); function talk() { - if (Room === null) return; - Room.talk(elements.chatInput.value); - elements.chatInput.value = ""; + if (Room === null) return; + Room.talk(elements.chatInput.value); + elements.chatInput.value = ''; } -roomInit(); \ No newline at end of file +roomInit(); diff --git a/webapp/src/ts/testbed.ts b/webapp/src/ts/testbed.ts index 93b74ab..a2df379 100644 --- a/webapp/src/ts/testbed.ts +++ b/webapp/src/ts/testbed.ts @@ -1,40 +1,40 @@ // Testbed code // This will go away when it isn't needed -import * as msagent from "@msagent-chat/msagent.js"; +import * as msagent from '@msagent-chat/msagent.js'; let w = window as any; w.agents = []; -let input = document.getElementById("testbed-input") as HTMLInputElement; +let input = document.getElementById('testbed-input') as HTMLInputElement; -let mount = document.getElementById("agent-mount") as HTMLDivElement; +let mount = document.getElementById('agent-mount') as HTMLDivElement; -input.addEventListener("change", async () => { - let buffer = await input.files![0].arrayBuffer(); +input.addEventListener('change', async () => { + let buffer = await input.files![0].arrayBuffer(); - console.log("Creating agent"); - let agent = msagent.agentCreateCharacter(new Uint8Array(buffer)); + console.log('Creating agent'); + let agent = msagent.agentCreateCharacter(new Uint8Array(buffer)); - w.agents.push(agent); + w.agents.push(agent); - agent.addToDom(mount); + agent.addToDom(mount); - agent.show(); - console.log("Agent created"); -}) - -document.addEventListener("DOMContentLoaded", async () => { - await msagent.agentInit(); - console.log("msagent initalized!"); -}) - -let form = document.getElementById("acsUrlForm") as HTMLFormElement; -form.addEventListener('submit', e => { - e.preventDefault(); - let url = (document.getElementById("acsUrl") as HTMLInputElement).value; - msagent.agentCreateCharacterFromUrl(url).then(agent => { - w.agents.push(agent); - agent.addToDom(document.body); - agent.show(); - console.log(`Loaded agent from ${url}`); - }); + agent.show(); + console.log('Agent created'); +}); + +document.addEventListener('DOMContentLoaded', async () => { + await msagent.agentInit(); + console.log('msagent initalized!'); +}); + +let form = document.getElementById('acsUrlForm') as HTMLFormElement; +form.addEventListener('submit', (e) => { + e.preventDefault(); + let url = (document.getElementById('acsUrl') as HTMLInputElement).value; + msagent.agentCreateCharacterFromUrl(url).then((agent) => { + w.agents.push(agent); + agent.addToDom(document.body); + agent.show(); + console.log(`Loaded agent from ${url}`); + }); }); diff --git a/webapp/src/ts/user.ts b/webapp/src/ts/user.ts index 640166a..9c6daf3 100644 --- a/webapp/src/ts/user.ts +++ b/webapp/src/ts/user.ts @@ -1,15 +1,15 @@ -import { Agent } from "@msagent-chat/msagent.js"; +import { Agent } from '@msagent-chat/msagent.js'; export class User { - username: string; - agent: Agent; - muted: boolean; - admin: boolean; - - constructor(username: string, agent: Agent) { - this.username = username; - this.agent = agent; - this.muted = false; - this.admin = false; - } -} \ No newline at end of file + username: string; + agent: Agent; + muted: boolean; + admin: boolean; + + constructor(username: string, agent: Agent) { + this.username = username; + this.agent = agent; + this.muted = false; + this.admin = false; + } +} diff --git a/yarn.lock b/yarn.lock index 92b9f53..9f95c91 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3259,6 +3259,7 @@ __metadata: "@parcel/packager-ts": "npm:2.12.0" "@parcel/transformer-sass": "npm:2.12.0" "@parcel/transformer-typescript-types": "npm:2.12.0" + prettier: "npm:^3.3.3" typescript: "npm:>=3.0.0" languageName: unknown linkType: soft @@ -3694,6 +3695,15 @@ __metadata: languageName: node linkType: hard +"prettier@npm:^3.3.3": + version: 3.3.3 + resolution: "prettier@npm:3.3.3" + bin: + prettier: bin/prettier.cjs + checksum: 10c0/b85828b08e7505716324e4245549b9205c0cacb25342a030ba8885aba2039a115dbcf75a0b7ca3b37bc9d101ee61fab8113fc69ca3359f2a226f1ecc07ad2e26 + languageName: node + linkType: hard + "proc-log@npm:^4.1.0, proc-log@npm:^4.2.0": version: 4.2.0 resolution: "proc-log@npm:4.2.0"