format code with prettier
This commit is contained in:
parent
cc2a9db92e
commit
1ad9ee14fe
30 changed files with 1224 additions and 1178 deletions
|
@ -115,14 +115,14 @@ class AgentWordBalloonState {
|
||||||
|
|
||||||
positionUpdated() {
|
positionUpdated() {
|
||||||
let size = this.char.getSize();
|
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) {
|
switch (this.position) {
|
||||||
case AgentWordBalloonPosition.AboveCentered: {
|
case AgentWordBalloonPosition.AboveCentered: {
|
||||||
this.balloonCanvas.style.top = -(this.balloonCanvas.height) + 'px';
|
this.balloonCanvas.style.top = -this.balloonCanvas.height + 'px';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case AgentWordBalloonPosition.BelowCentered: {
|
case AgentWordBalloonPosition.BelowCentered: {
|
||||||
this.balloonCanvas.style.bottom = -(this.balloonCanvas.height) + 'px';
|
this.balloonCanvas.style.bottom = -this.balloonCanvas.height + 'px';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -307,8 +307,8 @@ export class Agent {
|
||||||
if (this.wordballoonState != null) {
|
if (this.wordballoonState != null) {
|
||||||
this.stopSpeaking();
|
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.positionUpdated();
|
||||||
this.wordballoonState.show();
|
this.wordballoonState.show();
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,21 +5,21 @@ export enum SeekDir {
|
||||||
BEG = 0,
|
BEG = 0,
|
||||||
CUR = 1,
|
CUR = 1,
|
||||||
END = 2
|
END = 2
|
||||||
};
|
}
|
||||||
|
|
||||||
// A helper over DataView to make it more ergonomic for parsing file data.
|
// A helper over DataView to make it more ergonomic for parsing file data.
|
||||||
export class BufferStream {
|
export class BufferStream {
|
||||||
private bufferImpl: Uint8Array;
|
private bufferImpl: Uint8Array;
|
||||||
private dataView: DataView;
|
private dataView: DataView;
|
||||||
private readPointer: number = 0;
|
private readPointer: number = 0;
|
||||||
|
|
||||||
constructor(buffer: Uint8Array, byteOffset?: number) {
|
constructor(buffer: Uint8Array, byteOffset?: number) {
|
||||||
this.bufferImpl = buffer;
|
this.bufferImpl = buffer;
|
||||||
this.dataView = new DataView(this.bufferImpl.buffer, byteOffset);
|
this.dataView = new DataView(this.bufferImpl.buffer, byteOffset);
|
||||||
}
|
}
|
||||||
|
|
||||||
seek(where: number, whence: SeekDir) {
|
seek(where: number, whence: SeekDir) {
|
||||||
switch(whence) {
|
switch (whence) {
|
||||||
case SeekDir.BEG:
|
case SeekDir.BEG:
|
||||||
this.readPointer = where;
|
this.readPointer = where;
|
||||||
break;
|
break;
|
||||||
|
@ -29,8 +29,7 @@ export class BufferStream {
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case SeekDir.END:
|
case SeekDir.END:
|
||||||
if(where > 0)
|
if (where > 0) throw new Error('Cannot use SeekDir.END with where greater than 0');
|
||||||
throw new Error("Cannot use SeekDir.END with where greater than 0");
|
|
||||||
|
|
||||||
this.readPointer = this.bufferImpl.length + whence;
|
this.readPointer = this.bufferImpl.length + whence;
|
||||||
break;
|
break;
|
||||||
|
@ -39,34 +38,56 @@ export class BufferStream {
|
||||||
return this.readPointer;
|
return this.readPointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
tell() { return this.seek(0, SeekDir.CUR); }
|
tell() {
|
||||||
|
return this.seek(0, SeekDir.CUR);
|
||||||
|
}
|
||||||
|
|
||||||
// common impl function for read*()
|
// common impl function for read*()
|
||||||
private readImpl<T>(func: (this: DataView, offset: number, le?: boolean|undefined) => T, size: number, le?: boolean|undefined) {
|
private readImpl<T>(func: (this: DataView, offset: number, le?: boolean | undefined) => T, size: number, le?: boolean | undefined) {
|
||||||
let res = func.call(this.dataView, this.readPointer, le);
|
let res = func.call(this.dataView, this.readPointer, le);
|
||||||
this.readPointer += size;
|
this.readPointer += size;
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates a view of a part of the buffer.
|
// Creates a view of a part of the buffer.
|
||||||
// THIS DOES NOT DEEP COPY!
|
// THIS DOES NOT DEEP COPY!
|
||||||
subBuffer(len: number) {
|
subBuffer(len: number) {
|
||||||
let oldReadPointer = this.readPointer;
|
let oldReadPointer = this.readPointer;
|
||||||
let buffer = this.bufferImpl.subarray(oldReadPointer, oldReadPointer + len);
|
let buffer = this.bufferImpl.subarray(oldReadPointer, oldReadPointer + len);
|
||||||
this.readPointer += len;
|
this.readPointer += len;
|
||||||
return new BufferStream(buffer, oldReadPointer);
|
return new BufferStream(buffer, oldReadPointer);
|
||||||
}
|
}
|
||||||
|
|
||||||
readS8() { return this.readImpl(DataView.prototype.getInt8, 1); }
|
readS8() {
|
||||||
readU8() { return this.readImpl(DataView.prototype.getUint8, 1); }
|
return this.readImpl(DataView.prototype.getInt8, 1);
|
||||||
readS16LE() { return this.readImpl(DataView.prototype.getInt16, 2, true); }
|
}
|
||||||
readS16BE() { return this.readImpl(DataView.prototype.getInt16, 2, false); }
|
readU8() {
|
||||||
readU16LE() { return this.readImpl(DataView.prototype.getUint16, 2, true); }
|
return this.readImpl(DataView.prototype.getUint8, 1);
|
||||||
readU16BE() { return this.readImpl(DataView.prototype.getUint16, 2, false); }
|
}
|
||||||
readS32LE() { return this.readImpl(DataView.prototype.getInt32, 4, true); }
|
readS16LE() {
|
||||||
readS32BE() { return this.readImpl(DataView.prototype.getInt32, 4, false); }
|
return this.readImpl(DataView.prototype.getInt16, 2, true);
|
||||||
readU32LE() { return this.readImpl(DataView.prototype.getUint32, 4, true); }
|
}
|
||||||
readU32BE() { return this.readImpl(DataView.prototype.getUint32, 4, false); }
|
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
|
// Use this for temporary offset modification, e.g: when reading
|
||||||
// a structure *pointed to* inside another structure.
|
// a structure *pointed to* inside another structure.
|
||||||
|
@ -77,27 +98,25 @@ export class BufferStream {
|
||||||
this.seek(last, SeekDir.BEG);
|
this.seek(last, SeekDir.BEG);
|
||||||
}
|
}
|
||||||
|
|
||||||
readBool() : boolean {
|
readBool(): boolean {
|
||||||
let res = this.readU8();
|
let res = this.readU8();
|
||||||
return res != 0;
|
return res != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
readString<TChar extends number>(len: number, charReader: (this: BufferStream) => TChar): string {
|
readString<TChar extends number>(len: number, charReader: (this: BufferStream) => TChar): string {
|
||||||
let str = "";
|
let str = '';
|
||||||
|
|
||||||
for(let i = 0; i < len; ++i)
|
for (let i = 0; i < len; ++i) str += String.fromCharCode(charReader.call(this));
|
||||||
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.
|
// so we shouldn't need to add the "support" for that.
|
||||||
charReader.call(this);
|
charReader.call(this);
|
||||||
return str;
|
return str;
|
||||||
}
|
}
|
||||||
|
|
||||||
readPascalString(lengthReader: (this: BufferStream) => number = BufferStream.prototype.readU32LE, charReader: (this: BufferStream) => number = BufferStream.prototype.readU16LE) {
|
readPascalString(lengthReader: (this: BufferStream) => number = BufferStream.prototype.readU32LE, charReader: (this: BufferStream) => number = BufferStream.prototype.readU16LE) {
|
||||||
let len = lengthReader.call(this);
|
let len = lengthReader.call(this);
|
||||||
if(len == 0)
|
if (len == 0) return '';
|
||||||
return "";
|
|
||||||
|
|
||||||
return this.readString(len, charReader);
|
return this.readString(len, charReader);
|
||||||
}
|
}
|
||||||
|
@ -107,25 +126,22 @@ export class BufferStream {
|
||||||
return this.subBuffer(len);
|
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();
|
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
|
// reads a counted list. The length reader is on the other end so you don't need to specify it
|
||||||
// (if it's u32)
|
// (if it's u32)
|
||||||
readCountedList<TObject>(objReader: (stream: BufferStream) => TObject, lengthReader: (this: BufferStream) => number = BufferStream.prototype.readU32LE): TObject[] {
|
readCountedList<TObject>(objReader: (stream: BufferStream) => TObject, lengthReader: (this: BufferStream) => number = BufferStream.prototype.readU32LE): TObject[] {
|
||||||
let len = lengthReader.call(this);
|
let len = lengthReader.call(this);
|
||||||
let arr: TObject[] = [];
|
let arr: TObject[] = [];
|
||||||
if(len == 0)
|
if (len == 0) return arr;
|
||||||
return arr;
|
|
||||||
|
|
||||||
for(let i = 0; i < len; ++i)
|
for (let i = 0; i < len; ++i) arr.push(objReader(this));
|
||||||
arr.push(objReader(this));
|
|
||||||
|
|
||||||
return arr;
|
return arr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
raw() {
|
raw() {
|
||||||
return this.bufferImpl;
|
return this.bufferImpl;
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,7 +35,6 @@ export function agentCharacterParseACS(buffer: BufferStream): AcsData {
|
||||||
let imageInfoLocation = LOCATION.read(buffer);
|
let imageInfoLocation = LOCATION.read(buffer);
|
||||||
let audioInfoLocation = LOCATION.read(buffer);
|
let audioInfoLocation = LOCATION.read(buffer);
|
||||||
|
|
||||||
|
|
||||||
buffer.withOffset(characterInfoLocation.offset, () => {
|
buffer.withOffset(characterInfoLocation.offset, () => {
|
||||||
acsData.characterInfo = AcsCharacterInfo.read(buffer);
|
acsData.characterInfo = AcsCharacterInfo.read(buffer);
|
||||||
});
|
});
|
||||||
|
@ -59,9 +58,9 @@ export function agentCreateCharacter(data: AcsData): Agent {
|
||||||
return new Agent(data);
|
return new Agent(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function agentCreateCharacterFromUrl(url: string) : Promise<Agent> {
|
export async function agentCreateCharacterFromUrl(url: string): Promise<Agent> {
|
||||||
// just return the cache object
|
// just return the cache object
|
||||||
if(acsDataCache.has(url)) {
|
if (acsDataCache.has(url)) {
|
||||||
return agentCreateCharacter(acsDataCache.get(url)!);
|
return agentCreateCharacter(acsDataCache.get(url)!);
|
||||||
} else {
|
} else {
|
||||||
let res = await fetch(url);
|
let res = await fetch(url);
|
||||||
|
|
|
@ -1,80 +1,84 @@
|
||||||
export class ContextMenuItem {
|
export class ContextMenuItem {
|
||||||
private element: HTMLLIElement;
|
private element: HTMLLIElement;
|
||||||
|
|
||||||
name: string;
|
name: string;
|
||||||
cb: Function;
|
cb: Function;
|
||||||
|
|
||||||
constructor(name: string, cb: Function) {
|
constructor(name: string, cb: Function) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.cb = cb;
|
this.cb = cb;
|
||||||
this.element = document.createElement("li");
|
this.element = document.createElement('li');
|
||||||
this.element.classList.add("context-menu-item");
|
this.element.classList.add('context-menu-item');
|
||||||
this.element.innerText = name;
|
this.element.innerText = name;
|
||||||
this.element.addEventListener('mousedown', () => this.cb());
|
this.element.addEventListener('mousedown', () => this.cb());
|
||||||
}
|
}
|
||||||
|
|
||||||
getElement() {
|
getElement() {
|
||||||
return this.element;
|
return this.element;
|
||||||
}
|
}
|
||||||
|
|
||||||
setName(name: string) {
|
setName(name: string) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.element.innerText = name;
|
this.element.innerText = name;
|
||||||
}
|
}
|
||||||
|
|
||||||
setCb(cb: Function) {
|
setCb(cb: Function) {
|
||||||
this.cb = cb;
|
this.cb = cb;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ContextMenu {
|
export class ContextMenu {
|
||||||
private element: HTMLDivElement;
|
private element: HTMLDivElement;
|
||||||
private list: HTMLUListElement;
|
private list: HTMLUListElement;
|
||||||
|
|
||||||
private items: Array<ContextMenuItem>
|
private items: Array<ContextMenuItem>;
|
||||||
|
|
||||||
constructor(parent: HTMLElement) {
|
constructor(parent: HTMLElement) {
|
||||||
this.element = document.createElement("div");
|
this.element = document.createElement('div');
|
||||||
this.list = document.createElement("ul");
|
this.list = document.createElement('ul');
|
||||||
this.element.appendChild(this.list);
|
this.element.appendChild(this.list);
|
||||||
this.items = [];
|
this.items = [];
|
||||||
this.element.classList.add("context-menu");
|
this.element.classList.add('context-menu');
|
||||||
this.element.style.display = "none";
|
this.element.style.display = 'none';
|
||||||
this.element.style.position = "fixed";
|
this.element.style.position = 'fixed';
|
||||||
parent.appendChild(this.element);
|
parent.appendChild(this.element);
|
||||||
}
|
}
|
||||||
|
|
||||||
show(x: number, y: number) {
|
show(x: number, y: number) {
|
||||||
this.element.style.left = x + "px";
|
this.element.style.left = x + 'px';
|
||||||
this.element.style.top = y + "px";
|
this.element.style.top = y + 'px';
|
||||||
document.addEventListener('mousedown', () => {
|
document.addEventListener(
|
||||||
this.hide();
|
'mousedown',
|
||||||
}, {once: true});
|
() => {
|
||||||
this.element.style.display = "block";
|
this.hide();
|
||||||
}
|
},
|
||||||
|
{ once: true }
|
||||||
|
);
|
||||||
|
this.element.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
hide() {
|
hide() {
|
||||||
this.element.style.display = "none";
|
this.element.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
addItem(item: ContextMenuItem) {
|
addItem(item: ContextMenuItem) {
|
||||||
this.items.push(item);
|
this.items.push(item);
|
||||||
this.list.appendChild(item.getElement());
|
this.list.appendChild(item.getElement());
|
||||||
}
|
}
|
||||||
|
|
||||||
removeItem(item: ContextMenuItem) {
|
removeItem(item: ContextMenuItem) {
|
||||||
let i = this.items.indexOf(item);
|
let i = this.items.indexOf(item);
|
||||||
if (i === -1) return;
|
if (i === -1) return;
|
||||||
this.items.splice(i, 1);
|
this.items.splice(i, 1);
|
||||||
item.getElement().remove();
|
item.getElement().remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
getItem(name: string) {
|
getItem(name: string) {
|
||||||
return this.items.find(i => i.name === name);
|
return this.items.find((i) => i.name === name);
|
||||||
}
|
}
|
||||||
|
|
||||||
clearItems() {
|
clearItems() {
|
||||||
this.items.splice(0, this.items.length);
|
this.items.splice(0, this.items.length);
|
||||||
this.list.replaceChildren();
|
this.list.replaceChildren();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,16 +14,14 @@ export async function compressInit() {
|
||||||
compressWasm = await WebAssembly.instantiateStreaming(fetch(url));
|
compressWasm = await WebAssembly.instantiateStreaming(fetch(url));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function compressWasmGetExports() {
|
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;
|
return compressWasmGetExports().memory;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// debugging
|
// debugging
|
||||||
//(window as any).DEBUGcompressGetWASM = () => {
|
//(window as any).DEBUGcompressGetWASM = () => {
|
||||||
// return compressWasm;
|
// 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
|
// Grow the WASM heap if needed. Funnily enough, this code is never hit in most
|
||||||
// ACSes, so IDK if it's even needed
|
// ACSes, so IDK if it's even needed
|
||||||
let memory = compressWASMGetMemory();
|
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
|
// A WebAssembly page is 64kb, so we need to grow at least that much
|
||||||
let npages = Math.floor((src.length + dest.length) / 65535) + 1;
|
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);
|
memory.grow(npages);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,8 +48,7 @@ export function compressDecompress(src: Uint8Array, dest: Uint8Array) {
|
||||||
// Call the WASM compression routine
|
// Call the WASM compression routine
|
||||||
let nrBytesDecompressed = compressWasmGetExports().agentDecompressWASM(0, src.length, src.length, dest.length);
|
let nrBytesDecompressed = compressWasmGetExports().agentDecompressWASM(0, src.length, src.length, dest.length);
|
||||||
|
|
||||||
if(nrBytesDecompressed != dest.length)
|
if (nrBytesDecompressed != dest.length) throw new Error(`decompression failed: ${nrBytesDecompressed} != ${dest.length}`);
|
||||||
throw new Error(`decompression failed: ${nrBytesDecompressed} != ${dest.length}`);
|
|
||||||
|
|
||||||
// Dest will be memory[src.length..dest.length]
|
// Dest will be memory[src.length..dest.length]
|
||||||
dest.set(copyBuffer.slice(src.length, src.length + dest.length), 0);
|
dest.set(copyBuffer.slice(src.length, src.length + dest.length), 0);
|
||||||
|
|
|
@ -1,16 +1,15 @@
|
||||||
import { compressInit } from "./decompress.js";
|
import { compressInit } from './decompress.js';
|
||||||
import { wordballoonInit } from "./wordballoon.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";
|
|
||||||
|
|
||||||
|
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.
|
// Convinence function which initalizes all of msagent.js.
|
||||||
export async function agentInit() {
|
export async function agentInit() {
|
||||||
await compressInit();
|
await compressInit();
|
||||||
await wordballoonInit();
|
await wordballoonInit();
|
||||||
}
|
}
|
||||||
|
|
|
@ -108,7 +108,7 @@ export class RGBAColor {
|
||||||
//quad.g = (val & 0x00ff0000) >> 16;
|
//quad.g = (val & 0x00ff0000) >> 16;
|
||||||
//quad.b = (val & 0x0000ff00) >> 8;
|
//quad.b = (val & 0x0000ff00) >> 8;
|
||||||
|
|
||||||
quad.r = (val & 0x000000ff);
|
quad.r = val & 0x000000ff;
|
||||||
quad.g = (val & 0x0000ff00) >> 8;
|
quad.g = (val & 0x0000ff00) >> 8;
|
||||||
quad.b = (val & 0x00ff0000) >> 16;
|
quad.b = (val & 0x00ff0000) >> 16;
|
||||||
|
|
||||||
|
|
|
@ -29,17 +29,17 @@ export class AcsImage {
|
||||||
image.data = data;
|
image.data = data;
|
||||||
}
|
}
|
||||||
|
|
||||||
let temp = COMPRESSED_DATABLOCK.read(buffer);
|
let temp = COMPRESSED_DATABLOCK.read(buffer);
|
||||||
let tempBuffer = new BufferStream(temp.data);
|
let tempBuffer = new BufferStream(temp.data);
|
||||||
|
|
||||||
image.regionData = RGNDATA.read(tempBuffer);
|
image.regionData = RGNDATA.read(tempBuffer);
|
||||||
|
|
||||||
return image;
|
return image;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AcsImageEntry {
|
export class AcsImageEntry {
|
||||||
image = new AcsImage();
|
image = new AcsImage();
|
||||||
|
|
||||||
static read(buffer: BufferStream) {
|
static read(buffer: BufferStream) {
|
||||||
let image = new AcsImageEntry();
|
let image = new AcsImageEntry();
|
||||||
|
@ -48,9 +48,9 @@ export class AcsImageEntry {
|
||||||
let loc = LOCATION.read(buffer);
|
let loc = LOCATION.read(buffer);
|
||||||
let checksum = buffer.readU32LE();
|
let checksum = buffer.readU32LE();
|
||||||
|
|
||||||
buffer.withOffset(loc.offset, () => {
|
buffer.withOffset(loc.offset, () => {
|
||||||
image.image = AcsImage.read(buffer);
|
image.image = AcsImage.read(buffer);
|
||||||
});
|
});
|
||||||
|
|
||||||
return image;
|
return image;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
export type Rect = {
|
export type Rect = {
|
||||||
x: number,
|
x: number;
|
||||||
y: number,
|
y: number;
|
||||||
w: number,
|
w: number;
|
||||||
h: number
|
h: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Size = {
|
export type Size = {
|
||||||
w: number,
|
w: number;
|
||||||
h: number
|
h: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Point = {
|
export type Point = {
|
||||||
x: number,
|
x: number;
|
||||||
y: number
|
y: number;
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,7 +5,6 @@ let corner_sprite: HTMLImageElement;
|
||||||
let straight_sprite: HTMLImageElement;
|
let straight_sprite: HTMLImageElement;
|
||||||
let tip_sprite: HTMLImageElement;
|
let tip_sprite: HTMLImageElement;
|
||||||
|
|
||||||
|
|
||||||
// Call *once* to initalize the wordballoon drawing system.
|
// Call *once* to initalize the wordballoon drawing system.
|
||||||
// Do not call other wordballoon* functions WITHOUT doing so.
|
// Do not call other wordballoon* functions WITHOUT doing so.
|
||||||
export async function wordballoonInit() {
|
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);
|
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
|
// 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;
|
let j = 1;
|
||||||
for (; j < size.h / 12; ++j) {
|
for (; j < size.h / 12; ++j) {
|
||||||
spriteDrawRotated(ctx, straight_sprite, 270, at.x, at.y + 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,
|
// 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.
|
// 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.
|
// Draw the tip.
|
||||||
if (hasTip)
|
if (hasTip) spriteDraw(ctx, tip_sprite, at.x + size.w / 2, at.y + 12 * (j + 1) - 1);
|
||||||
spriteDraw(ctx, tip_sprite, at.x + size.w / 2, at.y + 12 * (j + 1) - 1);
|
|
||||||
|
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
|
|
||||||
|
@ -109,7 +107,7 @@ export function wordballoonDraw(ctx: CanvasRenderingContext2D, at: Point, size:
|
||||||
|
|
||||||
function wordWrapToStringList(text: string, maxLength: number) {
|
function wordWrapToStringList(text: string, maxLength: number) {
|
||||||
// this was stolen off stackoverflow, it sucks but it (kind of) works
|
// 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 = [],
|
var result = [],
|
||||||
line: string[] = [];
|
line: string[] = [];
|
||||||
var length = 0;
|
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.
|
// 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);
|
let lines = wordWrapToStringList(text, maxLen);
|
||||||
|
|
||||||
// Create metrics for each line
|
// 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.w = Math.floor(size.w + 12);
|
||||||
size.h = Math.floor(size.h);
|
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);
|
let rectInner = wordballoonDraw(ctx, at, size, hasTip);
|
||||||
|
|
||||||
// Draw all the lines of text
|
// Draw all the lines of text
|
||||||
let y = 0;
|
let y = 0;
|
||||||
for (let i in lines) {
|
for (let i in lines) {
|
||||||
let metric = metrics[i];
|
let metric = metrics[i];
|
||||||
|
@ -175,7 +173,7 @@ export function wordballoonDrawText(ctx: CanvasRenderingContext2D, at: Point, te
|
||||||
return {
|
return {
|
||||||
x: at.x,
|
x: at.x,
|
||||||
y: at.y,
|
y: at.y,
|
||||||
w: rectInner.w + (12*3) + 12,
|
w: rectInner.w + 12 * 3 + 12,
|
||||||
h: rectInner.h + (13*3) + 18
|
h: rectInner.h + 13 * 3 + 18
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,11 +6,15 @@
|
||||||
"protocol",
|
"protocol",
|
||||||
"msagent.js"
|
"msagent.js"
|
||||||
],
|
],
|
||||||
|
"scripts": {
|
||||||
|
"format": "prettier --write **/*.{ts,html,scss}"
|
||||||
|
},
|
||||||
"packageManager": "yarn@4.2.2",
|
"packageManager": "yarn@4.2.2",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@parcel/packager-ts": "2.12.0",
|
"@parcel/packager-ts": "2.12.0",
|
||||||
"@parcel/transformer-sass": "2.12.0",
|
"@parcel/transformer-sass": "2.12.0",
|
||||||
"@parcel/transformer-typescript-types": "2.12.0",
|
"@parcel/transformer-typescript-types": "2.12.0",
|
||||||
|
"prettier": "^3.3.3",
|
||||||
"typescript": ">=3.0.0"
|
"typescript": ">=3.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,64 +1,64 @@
|
||||||
import { MSAgentProtocolMessage, MSAgentProtocolMessageType } from "./protocol";
|
import { MSAgentProtocolMessage, MSAgentProtocolMessageType } from './protocol';
|
||||||
|
|
||||||
export enum MSAgentAdminOperation {
|
export enum MSAgentAdminOperation {
|
||||||
// Client-to-server
|
// Client-to-server
|
||||||
Kick = "kick",
|
Kick = 'kick',
|
||||||
Ban = "ban",
|
Ban = 'ban',
|
||||||
// Bidirectional
|
// Bidirectional
|
||||||
Login = "login",
|
Login = 'login',
|
||||||
GetIP = "ip",
|
GetIP = 'ip'
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MSAgentAdminMessage extends MSAgentProtocolMessage {
|
export interface MSAgentAdminMessage extends MSAgentProtocolMessage {
|
||||||
op: MSAgentProtocolMessageType.Admin,
|
op: MSAgentProtocolMessageType.Admin;
|
||||||
data: {
|
data: {
|
||||||
action: MSAgentAdminOperation
|
action: MSAgentAdminOperation;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Client-to-server
|
// Client-to-server
|
||||||
|
|
||||||
export interface MSAgentAdminLoginMessage extends MSAgentAdminMessage {
|
export interface MSAgentAdminLoginMessage extends MSAgentAdminMessage {
|
||||||
data: {
|
data: {
|
||||||
action: MSAgentAdminOperation.Login,
|
action: MSAgentAdminOperation.Login;
|
||||||
password: string
|
password: string;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MSAgentAdminGetIPMessage extends MSAgentAdminMessage {
|
export interface MSAgentAdminGetIPMessage extends MSAgentAdminMessage {
|
||||||
data: {
|
data: {
|
||||||
action: MSAgentAdminOperation.GetIP,
|
action: MSAgentAdminOperation.GetIP;
|
||||||
username: string
|
username: string;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MSAgentAdminKickMessage extends MSAgentAdminMessage {
|
export interface MSAgentAdminKickMessage extends MSAgentAdminMessage {
|
||||||
data: {
|
data: {
|
||||||
action: MSAgentAdminOperation.Kick,
|
action: MSAgentAdminOperation.Kick;
|
||||||
username: string
|
username: string;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MSAgentAdminBanMessage extends MSAgentAdminMessage {
|
export interface MSAgentAdminBanMessage extends MSAgentAdminMessage {
|
||||||
data: {
|
data: {
|
||||||
action: MSAgentAdminOperation.Ban,
|
action: MSAgentAdminOperation.Ban;
|
||||||
username: string
|
username: string;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Server-to-client
|
// Server-to-client
|
||||||
|
|
||||||
export interface MSAgentAdminLoginResponse extends MSAgentAdminMessage {
|
export interface MSAgentAdminLoginResponse extends MSAgentAdminMessage {
|
||||||
data: {
|
data: {
|
||||||
action: MSAgentAdminOperation.Login,
|
action: MSAgentAdminOperation.Login;
|
||||||
success: boolean
|
success: boolean;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MSAgentAdminGetIPResponse extends MSAgentAdminMessage {
|
export interface MSAgentAdminGetIPResponse extends MSAgentAdminMessage {
|
||||||
data: {
|
data: {
|
||||||
action: MSAgentAdminOperation.GetIP,
|
action: MSAgentAdminOperation.GetIP;
|
||||||
username: string
|
username: string;
|
||||||
ip: string
|
ip: string;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,91 +1,91 @@
|
||||||
export * from './admin.js';
|
export * from './admin.js';
|
||||||
|
|
||||||
export enum MSAgentProtocolMessageType {
|
export enum MSAgentProtocolMessageType {
|
||||||
// Client-to-server
|
// Client-to-server
|
||||||
KeepAlive = "nop",
|
KeepAlive = 'nop',
|
||||||
Join = "join",
|
Join = 'join',
|
||||||
Talk = "talk",
|
Talk = 'talk',
|
||||||
Admin = "admin",
|
Admin = 'admin',
|
||||||
// Server-to-client
|
// Server-to-client
|
||||||
Init = "init",
|
Init = 'init',
|
||||||
AddUser = "adduser",
|
AddUser = 'adduser',
|
||||||
RemoveUser = "remuser",
|
RemoveUser = 'remuser',
|
||||||
Chat = "chat",
|
Chat = 'chat',
|
||||||
Promote = "promote",
|
Promote = 'promote',
|
||||||
Error = "error"
|
Error = 'error'
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MSAgentProtocolMessage {
|
export interface MSAgentProtocolMessage {
|
||||||
op: MSAgentProtocolMessageType
|
op: MSAgentProtocolMessageType;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Client-to-server
|
// Client-to-server
|
||||||
|
|
||||||
export interface MSAgentJoinMessage extends MSAgentProtocolMessage {
|
export interface MSAgentJoinMessage extends MSAgentProtocolMessage {
|
||||||
op: MSAgentProtocolMessageType.Join,
|
op: MSAgentProtocolMessageType.Join;
|
||||||
data: {
|
data: {
|
||||||
username: string;
|
username: string;
|
||||||
agent: string;
|
agent: string;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MSAgentTalkMessage extends MSAgentProtocolMessage {
|
export interface MSAgentTalkMessage extends MSAgentProtocolMessage {
|
||||||
op: MSAgentProtocolMessageType.Talk,
|
op: MSAgentProtocolMessageType.Talk;
|
||||||
data: {
|
data: {
|
||||||
msg: string;
|
msg: string;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Server-to-client
|
// Server-to-client
|
||||||
|
|
||||||
export interface MSAgentInitMessage extends MSAgentProtocolMessage {
|
export interface MSAgentInitMessage extends MSAgentProtocolMessage {
|
||||||
op: MSAgentProtocolMessageType.Init,
|
op: MSAgentProtocolMessageType.Init;
|
||||||
data: {
|
data: {
|
||||||
username: string
|
username: string;
|
||||||
agent: string
|
agent: string;
|
||||||
charlimit: number
|
charlimit: number;
|
||||||
users: {
|
users: {
|
||||||
username: string,
|
username: string;
|
||||||
agent: string,
|
agent: string;
|
||||||
admin: boolean
|
admin: boolean;
|
||||||
}[]
|
}[];
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MSAgentAddUserMessage extends MSAgentProtocolMessage {
|
export interface MSAgentAddUserMessage extends MSAgentProtocolMessage {
|
||||||
op: MSAgentProtocolMessageType.AddUser,
|
op: MSAgentProtocolMessageType.AddUser;
|
||||||
data: {
|
data: {
|
||||||
username: string;
|
username: string;
|
||||||
agent: string;
|
agent: string;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MSAgentRemoveUserMessage extends MSAgentProtocolMessage {
|
export interface MSAgentRemoveUserMessage extends MSAgentProtocolMessage {
|
||||||
op: MSAgentProtocolMessageType.RemoveUser,
|
op: MSAgentProtocolMessageType.RemoveUser;
|
||||||
data: {
|
data: {
|
||||||
username: string;
|
username: string;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MSAgentChatMessage extends MSAgentProtocolMessage {
|
export interface MSAgentChatMessage extends MSAgentProtocolMessage {
|
||||||
op: MSAgentProtocolMessageType.Chat,
|
op: MSAgentProtocolMessageType.Chat;
|
||||||
data: {
|
data: {
|
||||||
username: string;
|
username: string;
|
||||||
message: string;
|
message: string;
|
||||||
audio? : string | undefined;
|
audio?: string | undefined;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MSAgentPromoteMessage extends MSAgentProtocolMessage {
|
export interface MSAgentPromoteMessage extends MSAgentProtocolMessage {
|
||||||
op: MSAgentProtocolMessageType.Promote,
|
op: MSAgentProtocolMessageType.Promote;
|
||||||
data: {
|
data: {
|
||||||
username: string;
|
username: string;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MSAgentErrorMessage extends MSAgentProtocolMessage {
|
export interface MSAgentErrorMessage extends MSAgentProtocolMessage {
|
||||||
op: MSAgentProtocolMessageType.Error,
|
op: MSAgentProtocolMessageType.Error;
|
||||||
data: {
|
data: {
|
||||||
error: string;
|
error: string;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,236 +1,246 @@
|
||||||
import EventEmitter from "events";
|
import EventEmitter from 'events';
|
||||||
import { WebSocket } from "ws";
|
import { WebSocket } from 'ws';
|
||||||
import { MSAgentAdminBanMessage, MSAgentAdminGetIPMessage, MSAgentAdminGetIPResponse, MSAgentAdminKickMessage, MSAgentAdminLoginMessage, MSAgentAdminLoginResponse, MSAgentAdminMessage, MSAgentAdminOperation, MSAgentErrorMessage, MSAgentJoinMessage, MSAgentProtocolMessage, MSAgentProtocolMessageType, MSAgentTalkMessage } from '@msagent-chat/protocol';
|
import {
|
||||||
import { MSAgentChatRoom } from "./room.js";
|
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 * as htmlentities from 'html-entities';
|
||||||
import RateLimiter from "./ratelimiter.js";
|
import RateLimiter from './ratelimiter.js';
|
||||||
import { createHash } from "crypto";
|
import { createHash } from 'crypto';
|
||||||
|
|
||||||
// Event types
|
// Event types
|
||||||
|
|
||||||
export interface Client {
|
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: string, listener: Function): this;
|
on(event: string, listener: Function): this;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Client extends EventEmitter {
|
export class Client extends EventEmitter {
|
||||||
ip: string;
|
ip: string;
|
||||||
username: string | null;
|
username: string | null;
|
||||||
agent: string | null;
|
agent: string | null;
|
||||||
admin: boolean;
|
admin: boolean;
|
||||||
|
|
||||||
room: MSAgentChatRoom;
|
room: MSAgentChatRoom;
|
||||||
socket: WebSocket;
|
socket: WebSocket;
|
||||||
|
|
||||||
nopTimer: NodeJS.Timeout | undefined;
|
nopTimer: NodeJS.Timeout | undefined;
|
||||||
nopLevel: number;
|
nopLevel: number;
|
||||||
|
|
||||||
chatRateLimit: RateLimiter
|
chatRateLimit: RateLimiter;
|
||||||
|
|
||||||
constructor(socket: WebSocket, room: MSAgentChatRoom, ip: string) {
|
constructor(socket: WebSocket, room: MSAgentChatRoom, ip: string) {
|
||||||
super();
|
super();
|
||||||
this.socket = socket;
|
this.socket = socket;
|
||||||
this.ip = ip;
|
this.ip = ip;
|
||||||
this.room = room;
|
this.room = room;
|
||||||
this.username = null;
|
this.username = null;
|
||||||
this.agent = null;
|
this.agent = null;
|
||||||
this.admin = false;
|
this.admin = false;
|
||||||
this.resetNop();
|
this.resetNop();
|
||||||
this.nopLevel = 0;
|
this.nopLevel = 0;
|
||||||
|
|
||||||
this.chatRateLimit = new RateLimiter(this.room.config.ratelimits.chat);
|
|
||||||
|
|
||||||
this.socket.on('message', (msg, isBinary) => {
|
this.chatRateLimit = new RateLimiter(this.room.config.ratelimits.chat);
|
||||||
if (isBinary) {
|
|
||||||
this.socket.close();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.parseMessage(msg.toString("utf-8"));
|
|
||||||
});
|
|
||||||
this.socket.on('error', () => {});
|
|
||||||
this.socket.on('close', () => {
|
|
||||||
this.emit('close');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
send(msg: MSAgentProtocolMessage) {
|
this.socket.on('message', (msg, isBinary) => {
|
||||||
return new Promise<void>((res, rej) => {
|
if (isBinary) {
|
||||||
if (this.socket.readyState !== WebSocket.OPEN) {
|
this.socket.close();
|
||||||
res();
|
return;
|
||||||
return;
|
}
|
||||||
}
|
this.parseMessage(msg.toString('utf-8'));
|
||||||
this.socket.send(JSON.stringify(msg), err => {
|
});
|
||||||
if (err) {
|
this.socket.on('error', () => {});
|
||||||
rej(err);
|
this.socket.on('close', () => {
|
||||||
return;
|
this.emit('close');
|
||||||
}
|
});
|
||||||
res();
|
}
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private resetNop() {
|
send(msg: MSAgentProtocolMessage) {
|
||||||
clearInterval(this.nopTimer);
|
return new Promise<void>((res, rej) => {
|
||||||
this.nopLevel = 0;
|
if (this.socket.readyState !== WebSocket.OPEN) {
|
||||||
this.nopTimer = setInterval(() => {
|
res();
|
||||||
if (this.nopLevel++ >= 3) {
|
return;
|
||||||
this.socket.close();
|
}
|
||||||
} else {
|
this.socket.send(JSON.stringify(msg), (err) => {
|
||||||
this.send({
|
if (err) {
|
||||||
op: MSAgentProtocolMessageType.KeepAlive
|
rej(err);
|
||||||
});
|
return;
|
||||||
}
|
}
|
||||||
}, 10000)
|
res();
|
||||||
}
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private async parseMessage(data: string) {
|
private resetNop() {
|
||||||
let msg: MSAgentProtocolMessage;
|
clearInterval(this.nopTimer);
|
||||||
try {
|
this.nopLevel = 0;
|
||||||
msg = JSON.parse(data);
|
this.nopTimer = setInterval(() => {
|
||||||
} catch {
|
if (this.nopLevel++ >= 3) {
|
||||||
this.socket.close();
|
this.socket.close();
|
||||||
return;
|
} else {
|
||||||
}
|
this.send({
|
||||||
this.resetNop();
|
op: MSAgentProtocolMessageType.KeepAlive
|
||||||
switch (msg.op) {
|
});
|
||||||
case MSAgentProtocolMessageType.Join: {
|
}
|
||||||
let joinMsg = msg as MSAgentJoinMessage;
|
}, 10000);
|
||||||
if (!joinMsg.data || !joinMsg.data.username || !joinMsg.data.username) {
|
}
|
||||||
this.socket.close();
|
|
||||||
return;
|
private async parseMessage(data: string) {
|
||||||
}
|
let msg: MSAgentProtocolMessage;
|
||||||
let username = joinMsg.data.username.trim();
|
try {
|
||||||
if (!validateUsername(username)) {
|
msg = JSON.parse(data);
|
||||||
let msg: MSAgentErrorMessage = {
|
} catch {
|
||||||
op: MSAgentProtocolMessageType.Error,
|
this.socket.close();
|
||||||
data: {
|
return;
|
||||||
error: "Usernames can contain only numbers, letters, spaces, dashes, underscores, and dots, and it must be between 3 and 20 characters."
|
}
|
||||||
}
|
this.resetNop();
|
||||||
};
|
switch (msg.op) {
|
||||||
await this.send(msg);
|
case MSAgentProtocolMessageType.Join: {
|
||||||
this.socket.close();
|
let joinMsg = msg as MSAgentJoinMessage;
|
||||||
return;
|
if (!joinMsg.data || !joinMsg.data.username || !joinMsg.data.username) {
|
||||||
}
|
this.socket.close();
|
||||||
if (this.room.config.bannedWords.some(w => username.indexOf(w) !== -1)) {
|
return;
|
||||||
this.socket.close();
|
}
|
||||||
return;
|
let username = joinMsg.data.username.trim();
|
||||||
}
|
if (!validateUsername(username)) {
|
||||||
if (this.room.clients.some(u => u.username === username)) {
|
let msg: MSAgentErrorMessage = {
|
||||||
let i = 1;
|
op: MSAgentProtocolMessageType.Error,
|
||||||
let uo = username;
|
data: {
|
||||||
do {
|
error: 'Usernames can contain only numbers, letters, spaces, dashes, underscores, and dots, and it must be between 3 and 20 characters.'
|
||||||
username = uo + i++;
|
}
|
||||||
} while (this.room.clients.some(u => u.username === username))
|
};
|
||||||
}
|
await this.send(msg);
|
||||||
if (!this.room.agents.some(a => a.filename === joinMsg.data.agent)) {
|
this.socket.close();
|
||||||
this.socket.close();
|
return;
|
||||||
return;
|
}
|
||||||
}
|
if (this.room.config.bannedWords.some((w) => username.indexOf(w) !== -1)) {
|
||||||
this.username = username;
|
this.socket.close();
|
||||||
this.agent = joinMsg.data.agent;
|
return;
|
||||||
this.emit('join');
|
}
|
||||||
break;
|
if (this.room.clients.some((u) => u.username === username)) {
|
||||||
}
|
let i = 1;
|
||||||
case MSAgentProtocolMessageType.Talk: {
|
let uo = username;
|
||||||
let talkMsg = msg as MSAgentTalkMessage;
|
do {
|
||||||
if (!talkMsg.data || !talkMsg.data.msg || !this.chatRateLimit.request()) {
|
username = uo + i++;
|
||||||
return;
|
} while (this.room.clients.some((u) => u.username === username));
|
||||||
}
|
}
|
||||||
if (talkMsg.data.msg.length > this.room.config.charlimit) return;
|
if (!this.room.agents.some((a) => a.filename === joinMsg.data.agent)) {
|
||||||
if (this.room.config.bannedWords.some(w => talkMsg.data.msg.indexOf(w) !== -1)) {
|
this.socket.close();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.emit('talk', talkMsg.data.msg);
|
this.username = username;
|
||||||
break;
|
this.agent = joinMsg.data.agent;
|
||||||
}
|
this.emit('join');
|
||||||
case MSAgentProtocolMessageType.Admin: {
|
break;
|
||||||
let adminMsg = msg as MSAgentAdminMessage;
|
}
|
||||||
if (!adminMsg.data) return;
|
case MSAgentProtocolMessageType.Talk: {
|
||||||
switch (adminMsg.data.action) {
|
let talkMsg = msg as MSAgentTalkMessage;
|
||||||
case MSAgentAdminOperation.Login: {
|
if (!talkMsg.data || !talkMsg.data.msg || !this.chatRateLimit.request()) {
|
||||||
let loginMsg = adminMsg as MSAgentAdminLoginMessage;
|
return;
|
||||||
if (this.admin || !loginMsg.data.password) return;
|
}
|
||||||
let sha256 = createHash("sha256");
|
if (talkMsg.data.msg.length > this.room.config.charlimit) return;
|
||||||
sha256.update(loginMsg.data.password);
|
if (this.room.config.bannedWords.some((w) => talkMsg.data.msg.indexOf(w) !== -1)) {
|
||||||
let hash = sha256.digest("hex");
|
return;
|
||||||
sha256.destroy();
|
}
|
||||||
let success = false;
|
this.emit('talk', talkMsg.data.msg);
|
||||||
if (hash === this.room.config.adminPasswordHash) {
|
break;
|
||||||
this.admin = true;
|
}
|
||||||
success = true;
|
case MSAgentProtocolMessageType.Admin: {
|
||||||
this.emit('admin');
|
let adminMsg = msg as MSAgentAdminMessage;
|
||||||
}
|
if (!adminMsg.data) return;
|
||||||
let res : MSAgentAdminLoginResponse = {
|
switch (adminMsg.data.action) {
|
||||||
op: MSAgentProtocolMessageType.Admin,
|
case MSAgentAdminOperation.Login: {
|
||||||
data: {
|
let loginMsg = adminMsg as MSAgentAdminLoginMessage;
|
||||||
action: MSAgentAdminOperation.Login,
|
if (this.admin || !loginMsg.data.password) return;
|
||||||
success
|
let sha256 = createHash('sha256');
|
||||||
}
|
sha256.update(loginMsg.data.password);
|
||||||
}
|
let hash = sha256.digest('hex');
|
||||||
this.send(res);
|
sha256.destroy();
|
||||||
break;
|
let success = false;
|
||||||
}
|
if (hash === this.room.config.adminPasswordHash) {
|
||||||
case MSAgentAdminOperation.GetIP: {
|
this.admin = true;
|
||||||
let getIPMsg = adminMsg as MSAgentAdminGetIPMessage;
|
success = true;
|
||||||
if (!this.admin || !getIPMsg.data || !getIPMsg.data.username) return;
|
this.emit('admin');
|
||||||
let _user = this.room.clients.find(c => c.username === getIPMsg.data.username);
|
}
|
||||||
if (!_user) return;
|
let res: MSAgentAdminLoginResponse = {
|
||||||
let res: MSAgentAdminGetIPResponse = {
|
op: MSAgentProtocolMessageType.Admin,
|
||||||
op: MSAgentProtocolMessageType.Admin,
|
data: {
|
||||||
data: {
|
action: MSAgentAdminOperation.Login,
|
||||||
action: MSAgentAdminOperation.GetIP,
|
success
|
||||||
username: _user.username!,
|
}
|
||||||
ip: _user.ip
|
};
|
||||||
}
|
this.send(res);
|
||||||
};
|
break;
|
||||||
this.send(res);
|
}
|
||||||
break;
|
case MSAgentAdminOperation.GetIP: {
|
||||||
}
|
let getIPMsg = adminMsg as MSAgentAdminGetIPMessage;
|
||||||
case MSAgentAdminOperation.Kick: {
|
if (!this.admin || !getIPMsg.data || !getIPMsg.data.username) return;
|
||||||
let kickMsg = adminMsg as MSAgentAdminKickMessage;
|
let _user = this.room.clients.find((c) => c.username === getIPMsg.data.username);
|
||||||
if (!this.admin || !kickMsg.data || !kickMsg.data.username) return;
|
if (!_user) return;
|
||||||
let _user = this.room.clients.find(c => c.username === kickMsg.data.username);
|
let res: MSAgentAdminGetIPResponse = {
|
||||||
if (!_user) return;
|
op: MSAgentProtocolMessageType.Admin,
|
||||||
let res: MSAgentErrorMessage = {
|
data: {
|
||||||
op: MSAgentProtocolMessageType.Error,
|
action: MSAgentAdminOperation.GetIP,
|
||||||
data: {
|
username: _user.username!,
|
||||||
error: "You have been kicked."
|
ip: _user.ip
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
await _user.send(res);
|
this.send(res);
|
||||||
_user.socket.close();
|
break;
|
||||||
break;
|
}
|
||||||
}
|
case MSAgentAdminOperation.Kick: {
|
||||||
case MSAgentAdminOperation.Ban: {
|
let kickMsg = adminMsg as MSAgentAdminKickMessage;
|
||||||
let banMsg = adminMsg as MSAgentAdminBanMessage;
|
if (!this.admin || !kickMsg.data || !kickMsg.data.username) return;
|
||||||
if (!this.admin || !banMsg.data || !banMsg.data.username) return;
|
let _user = this.room.clients.find((c) => c.username === kickMsg.data.username);
|
||||||
let _user = this.room.clients.find(c => c.username === banMsg.data.username);
|
if (!_user) return;
|
||||||
if (!_user) return;
|
let res: MSAgentErrorMessage = {
|
||||||
let res: MSAgentErrorMessage = {
|
op: MSAgentProtocolMessageType.Error,
|
||||||
op: MSAgentProtocolMessageType.Error,
|
data: {
|
||||||
data: {
|
error: 'You have been kicked.'
|
||||||
error: "You have been banned."
|
}
|
||||||
}
|
};
|
||||||
};
|
await _user.send(res);
|
||||||
await this.room.db.banUser(_user.ip, _user.username!);
|
_user.socket.close();
|
||||||
await _user.send(res);
|
break;
|
||||||
_user.socket.close();
|
}
|
||||||
break;
|
case MSAgentAdminOperation.Ban: {
|
||||||
}
|
let banMsg = adminMsg as MSAgentAdminBanMessage;
|
||||||
}
|
if (!this.admin || !banMsg.data || !banMsg.data.username) return;
|
||||||
break;
|
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) {
|
function validateUsername(username: string) {
|
||||||
return (
|
return username.length >= 3 && username.length <= 20 && /^[a-zA-Z0-9\ \-\_\.]+$/.test(username);
|
||||||
username.length >= 3 &&
|
}
|
||||||
username.length <= 20 &&
|
|
||||||
/^[a-zA-Z0-9\ \-\_\.]+$/.test(username)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,60 +1,59 @@
|
||||||
export interface IConfig {
|
export interface IConfig {
|
||||||
http: {
|
http: {
|
||||||
host: string;
|
host: string;
|
||||||
port: number;
|
port: number;
|
||||||
proxied: boolean;
|
proxied: boolean;
|
||||||
}
|
};
|
||||||
mysql: MySQLConfig;
|
mysql: MySQLConfig;
|
||||||
chat: ChatConfig;
|
chat: ChatConfig;
|
||||||
motd: motdConfig;
|
motd: motdConfig;
|
||||||
discord: DiscordConfig;
|
discord: DiscordConfig;
|
||||||
tts: TTSConfig;
|
tts: TTSConfig;
|
||||||
agents: AgentConfig[];
|
agents: AgentConfig[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TTSConfig {
|
export interface TTSConfig {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
server: string;
|
server: string;
|
||||||
voice: string;
|
voice: string;
|
||||||
tempDir: string;
|
tempDir: string;
|
||||||
wavExpirySeconds: number;
|
wavExpirySeconds: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ChatConfig {
|
export interface ChatConfig {
|
||||||
charlimit: number;
|
charlimit: number;
|
||||||
agentsDir: string;
|
agentsDir: string;
|
||||||
maxConnectionsPerIP: number;
|
maxConnectionsPerIP: number;
|
||||||
adminPasswordHash: string;
|
adminPasswordHash: string;
|
||||||
bannedWords: string[];
|
bannedWords: string[];
|
||||||
ratelimits: {
|
ratelimits: {
|
||||||
chat: RateLimitConfig;
|
chat: RateLimitConfig;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface motdConfig {
|
export interface motdConfig {
|
||||||
version: number;
|
version: number;
|
||||||
html: string;
|
html: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AgentConfig {
|
export interface AgentConfig {
|
||||||
friendlyName: string;
|
friendlyName: string;
|
||||||
filename: string;
|
filename: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface RateLimitConfig {
|
export interface RateLimitConfig {
|
||||||
seconds: number;
|
seconds: number;
|
||||||
limit: number;
|
limit: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MySQLConfig {
|
export interface MySQLConfig {
|
||||||
host: string;
|
host: string;
|
||||||
username: string;
|
username: string;
|
||||||
password: string;
|
password: string;
|
||||||
database: string;
|
database: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DiscordConfig {
|
export interface DiscordConfig {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
webhookURL: string;
|
webhookURL: string;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,38 +1,38 @@
|
||||||
import { MySQLConfig } from "./config.js";
|
import { MySQLConfig } from './config.js';
|
||||||
import * as mysql from 'mysql2/promise';
|
import * as mysql from 'mysql2/promise';
|
||||||
|
|
||||||
export class Database {
|
export class Database {
|
||||||
private config: MySQLConfig;
|
private config: MySQLConfig;
|
||||||
private db: mysql.Pool;
|
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
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async init() {
|
constructor(config: MySQLConfig) {
|
||||||
let conn = await this.db.getConnection();
|
this.config = config;
|
||||||
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());");
|
this.db = mysql.createPool({
|
||||||
conn.release();
|
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) {
|
async init() {
|
||||||
let conn = await this.db.getConnection();
|
let conn = await this.db.getConnection();
|
||||||
await conn.execute("INSERT INTO bans (ip, username) VALUES (?, ?)", [ip, username]);
|
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();
|
conn.release();
|
||||||
}
|
}
|
||||||
|
|
||||||
async isUserBanned(ip: string): Promise<boolean> {
|
async banUser(ip: string, username: string) {
|
||||||
let conn = await this.db.getConnection();
|
let conn = await this.db.getConnection();
|
||||||
let res = await conn.query("SELECT COUNT(ip) AS cnt FROM bans WHERE ip = ?", [ip]) as mysql.RowDataPacket;
|
await conn.execute('INSERT INTO bans (ip, username) VALUES (?, ?)', [ip, username]);
|
||||||
conn.release();
|
conn.release();
|
||||||
return res[0][0]["cnt"] !== 0;
|
}
|
||||||
}
|
|
||||||
}
|
async isUserBanned(ip: string): Promise<boolean> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
import { WebhookClient } from "discord.js";
|
import { WebhookClient } from 'discord.js';
|
||||||
import { DiscordConfig } from "./config.js";
|
import { DiscordConfig } from './config.js';
|
||||||
|
|
||||||
export class DiscordLogger {
|
export class DiscordLogger {
|
||||||
private webhook: WebhookClient;
|
private webhook: WebhookClient;
|
||||||
|
|
||||||
constructor(config: DiscordConfig) {
|
|
||||||
this.webhook = new WebhookClient({url: config.webhookURL});
|
|
||||||
}
|
|
||||||
|
|
||||||
logMsg(username: string, msg: string) {
|
constructor(config: DiscordConfig) {
|
||||||
this.webhook.send({
|
this.webhook = new WebhookClient({ url: config.webhookURL });
|
||||||
username,
|
}
|
||||||
allowedMentions: {},
|
|
||||||
content: msg,
|
logMsg(username: string, msg: string) {
|
||||||
});
|
this.webhook.send({
|
||||||
}
|
username,
|
||||||
}
|
allowedMentions: {},
|
||||||
|
content: msg
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -15,29 +15,27 @@ import { DiscordLogger } from './discord.js';
|
||||||
|
|
||||||
let config: IConfig;
|
let config: IConfig;
|
||||||
let configPath: string;
|
let configPath: string;
|
||||||
if (process.argv.length < 3)
|
if (process.argv.length < 3) configPath = './config.toml';
|
||||||
configPath = "./config.toml";
|
else configPath = process.argv[2];
|
||||||
else
|
|
||||||
configPath = process.argv[2];
|
|
||||||
|
|
||||||
if (!fs.existsSync(configPath)) {
|
if (!fs.existsSync(configPath)) {
|
||||||
console.error(`${configPath} not found. Please copy config.example.toml and fill out fields.`);
|
console.error(`${configPath} not found. Please copy config.example.toml and fill out fields.`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let configRaw = fs.readFileSync(configPath, "utf-8");
|
let configRaw = fs.readFileSync(configPath, 'utf-8');
|
||||||
config = toml.parse(configRaw);
|
config = toml.parse(configRaw);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`Failed to read or parse ${configPath}: ${(e as Error).message}`);
|
console.error(`Failed to read or parse ${configPath}: ${(e as Error).message}`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
let db = new Database(config.mysql);
|
let db = new Database(config.mysql);
|
||||||
await db.init();
|
await db.init();
|
||||||
|
|
||||||
const app = Fastify({
|
const app = Fastify({
|
||||||
logger: true,
|
logger: true
|
||||||
});
|
});
|
||||||
|
|
||||||
app.register(FastifyWS);
|
app.register(FastifyWS);
|
||||||
|
@ -45,100 +43,97 @@ app.register(FastifyWS);
|
||||||
let tts = null;
|
let tts = null;
|
||||||
|
|
||||||
if (config.tts.enabled) {
|
if (config.tts.enabled) {
|
||||||
tts = new TTSClient(config.tts);
|
tts = new TTSClient(config.tts);
|
||||||
app.register(FastifyStatic, {
|
app.register(FastifyStatic, {
|
||||||
root: config.tts.tempDir,
|
root: config.tts.tempDir,
|
||||||
prefix: "/api/tts/",
|
prefix: '/api/tts/',
|
||||||
decorateReply: false
|
decorateReply: false
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!config.chat.agentsDir.endsWith("/")) config.chat.agentsDir += "/";
|
if (!config.chat.agentsDir.endsWith('/')) config.chat.agentsDir += '/';
|
||||||
if (!fs.existsSync(config.chat.agentsDir)) {
|
if (!fs.existsSync(config.chat.agentsDir)) {
|
||||||
console.error(`Directory ${config.chat.agentsDir} does not exist.`);
|
console.error(`Directory ${config.chat.agentsDir} does not exist.`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let agent of config.agents) {
|
for (let agent of config.agents) {
|
||||||
if (!fs.existsSync(path.join(config.chat.agentsDir, agent.filename))) {
|
if (!fs.existsSync(path.join(config.chat.agentsDir, agent.filename))) {
|
||||||
console.error(`${agent.filename} does not exist.`);
|
console.error(`${agent.filename} does not exist.`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
app.register(FastifyStatic, {
|
app.register(FastifyStatic, {
|
||||||
root: path.resolve(config.chat.agentsDir),
|
root: path.resolve(config.chat.agentsDir),
|
||||||
prefix: "/api/agents/",
|
prefix: '/api/agents/',
|
||||||
decorateReply: true,
|
decorateReply: true
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get("/api/agents", (req, res) => {
|
app.get('/api/agents', (req, res) => {
|
||||||
return config.agents;
|
return config.agents;
|
||||||
});
|
});
|
||||||
|
|
||||||
// MOTD
|
// MOTD
|
||||||
|
|
||||||
app.get("/api/motd/version", (req, res) => {
|
app.get('/api/motd/version', (req, res) => {
|
||||||
res.header("Content-Type", "text/plain");
|
res.header('Content-Type', 'text/plain');
|
||||||
return config.motd.version.toString();
|
return config.motd.version.toString();
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get("/api/motd/html", (req, res) => {
|
app.get('/api/motd/html', (req, res) => {
|
||||||
res.header("Content-Type", "text/html");
|
res.header('Content-Type', 'text/html');
|
||||||
return config.motd.html;
|
return config.motd.html;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Discord
|
// Discord
|
||||||
let discord = null;
|
let discord = null;
|
||||||
if (config.discord.enabled) {
|
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);
|
let room = new MSAgentChatRoom(config.chat, config.agents, db, 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) => {
|
||||||
// TODO: Do this pre-upgrade and return the appropriate status codes
|
// TODO: Do this pre-upgrade and return the appropriate status codes
|
||||||
let ip: string;
|
let ip: string;
|
||||||
if (config.http.proxied) {
|
if (config.http.proxied) {
|
||||||
if (req.headers["x-forwarded-for"] === undefined) {
|
if (req.headers['x-forwarded-for'] === undefined) {
|
||||||
console.error(`Warning: X-Forwarded-For not set! This is likely a misconfiguration of your reverse proxy.`);
|
console.error(`Warning: X-Forwarded-For not set! This is likely a misconfiguration of your reverse proxy.`);
|
||||||
socket.close();
|
socket.close();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let xff = req.headers["x-forwarded-for"];
|
let xff = req.headers['x-forwarded-for'];
|
||||||
if (xff instanceof Array)
|
if (xff instanceof Array) ip = xff[0];
|
||||||
ip = xff[0];
|
else ip = xff;
|
||||||
else
|
if (!isIP(ip)) {
|
||||||
ip = xff;
|
console.error(`Warning: X-Forwarded-For malformed! This is likely a misconfiguration of your reverse proxy.`);
|
||||||
if (!isIP(ip)) {
|
socket.close();
|
||||||
console.error(`Warning: X-Forwarded-For malformed! This is likely a misconfiguration of your reverse proxy.`);
|
return;
|
||||||
socket.close();
|
}
|
||||||
return;
|
} else {
|
||||||
}
|
ip = req.ip;
|
||||||
} else {
|
}
|
||||||
ip = req.ip;
|
if (await db.isUserBanned(ip)) {
|
||||||
}
|
let msg: MSAgentErrorMessage = {
|
||||||
if (await db.isUserBanned(ip)) {
|
op: MSAgentProtocolMessageType.Error,
|
||||||
let msg: MSAgentErrorMessage = {
|
data: {
|
||||||
op: MSAgentProtocolMessageType.Error,
|
error: 'You have been banned.'
|
||||||
data: {
|
}
|
||||||
error: "You have been banned."
|
};
|
||||||
}
|
socket.send(JSON.stringify(msg), () => {
|
||||||
}
|
socket.close();
|
||||||
socket.send(JSON.stringify(msg), () => {
|
});
|
||||||
socket.close();
|
return;
|
||||||
});
|
}
|
||||||
return;
|
let o = room.clients.filter((c) => c.ip === ip);
|
||||||
}
|
if (o.length >= config.chat.maxConnectionsPerIP) {
|
||||||
let o = room.clients.filter(c => c.ip === ip);
|
o[0].socket.close();
|
||||||
if (o.length >= config.chat.maxConnectionsPerIP) {
|
}
|
||||||
o[0].socket.close();
|
let client = new Client(socket, room, ip);
|
||||||
}
|
room.addClient(client);
|
||||||
let client = new Client(socket, room, ip);
|
});
|
||||||
room.addClient(client);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.listen({ host: config.http.host, port: config.http.port });
|
||||||
app.listen({host: config.http.host, port: config.http.port});
|
|
||||||
|
|
|
@ -32,4 +32,4 @@ export default class RateLimiter extends EventEmitter {
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,103 +1,113 @@
|
||||||
import { MSAgentAddUserMessage, MSAgentChatMessage, MSAgentInitMessage, MSAgentPromoteMessage, MSAgentProtocolMessage, MSAgentProtocolMessageType, MSAgentRemoveUserMessage } from "@msagent-chat/protocol";
|
import {
|
||||||
import { Client } from "./client.js";
|
MSAgentAddUserMessage,
|
||||||
import { TTSClient } from "./tts.js";
|
MSAgentChatMessage,
|
||||||
import { AgentConfig, ChatConfig } from "./config.js";
|
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 * 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';
|
||||||
|
|
||||||
export class MSAgentChatRoom {
|
export class MSAgentChatRoom {
|
||||||
agents: AgentConfig[];
|
agents: AgentConfig[];
|
||||||
clients: Client[];
|
clients: Client[];
|
||||||
tts: TTSClient | null;
|
tts: TTSClient | null;
|
||||||
msgId : number = 0;
|
msgId: number = 0;
|
||||||
config: ChatConfig;
|
config: ChatConfig;
|
||||||
db: Database;
|
db: Database;
|
||||||
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, 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.discord = discord;
|
this.discord = discord;
|
||||||
}
|
}
|
||||||
|
|
||||||
addClient(client: Client) {
|
addClient(client: Client) {
|
||||||
this.clients.push(client);
|
this.clients.push(client);
|
||||||
client.on('close', () => {
|
client.on('close', () => {
|
||||||
this.clients.splice(this.clients.indexOf(client), 1);
|
this.clients.splice(this.clients.indexOf(client), 1);
|
||||||
if (client.username === null) return;
|
if (client.username === null) return;
|
||||||
let msg: MSAgentRemoveUserMessage = {
|
let msg: MSAgentRemoveUserMessage = {
|
||||||
op: MSAgentProtocolMessageType.RemoveUser,
|
op: MSAgentProtocolMessageType.RemoveUser,
|
||||||
data: {
|
data: {
|
||||||
username: client.username
|
username: client.username
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
for (const _client of this.getActiveClients()) {
|
for (const _client of this.getActiveClients()) {
|
||||||
_client.send(msg);
|
_client.send(msg);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
client.on('join', () => {
|
client.on('join', () => {
|
||||||
let initmsg : MSAgentInitMessage = {
|
let initmsg: MSAgentInitMessage = {
|
||||||
op: MSAgentProtocolMessageType.Init,
|
op: MSAgentProtocolMessageType.Init,
|
||||||
data: {
|
data: {
|
||||||
username: client.username!,
|
username: client.username!,
|
||||||
agent: client.agent!,
|
agent: client.agent!,
|
||||||
charlimit: this.config.charlimit,
|
charlimit: this.config.charlimit,
|
||||||
users: this.clients.filter(c => c.username !== null).map(c => {
|
users: this.clients
|
||||||
return {
|
.filter((c) => c.username !== null)
|
||||||
username: c.username!,
|
.map((c) => {
|
||||||
agent: c.agent!,
|
return {
|
||||||
admin: c.admin
|
username: c.username!,
|
||||||
}
|
agent: c.agent!,
|
||||||
})
|
admin: c.admin
|
||||||
}
|
};
|
||||||
};
|
})
|
||||||
client.send(initmsg);
|
}
|
||||||
let msg: MSAgentAddUserMessage = {
|
};
|
||||||
op: MSAgentProtocolMessageType.AddUser,
|
client.send(initmsg);
|
||||||
data: {
|
let msg: MSAgentAddUserMessage = {
|
||||||
username: client.username!,
|
op: MSAgentProtocolMessageType.AddUser,
|
||||||
agent: client.agent!
|
data: {
|
||||||
}
|
username: client.username!,
|
||||||
}
|
agent: client.agent!
|
||||||
for (const _client of this.getActiveClients().filter(c => c !== client)) {
|
}
|
||||||
_client.send(msg);
|
};
|
||||||
}
|
for (const _client of this.getActiveClients().filter((c) => c !== client)) {
|
||||||
});
|
_client.send(msg);
|
||||||
client.on('talk', async message => {
|
}
|
||||||
let msg: MSAgentChatMessage = {
|
});
|
||||||
op: MSAgentProtocolMessageType.Chat,
|
client.on('talk', async (message) => {
|
||||||
data: {
|
let msg: MSAgentChatMessage = {
|
||||||
username: client.username!,
|
op: MSAgentProtocolMessageType.Chat,
|
||||||
message: message
|
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;
|
if (this.tts !== null) {
|
||||||
}
|
let filename = await this.tts.synthesizeToFile(message, (++this.msgId).toString(10));
|
||||||
for (const _client of this.getActiveClients()) {
|
msg.data.audio = '/api/tts/' + filename;
|
||||||
_client.send(msg);
|
}
|
||||||
}
|
for (const _client of this.getActiveClients()) {
|
||||||
this.discord?.logMsg(client.username!, message);
|
_client.send(msg);
|
||||||
});
|
}
|
||||||
client.on('admin', () => {
|
this.discord?.logMsg(client.username!, message);
|
||||||
let msg: MSAgentPromoteMessage = {
|
});
|
||||||
op: MSAgentProtocolMessageType.Promote,
|
client.on('admin', () => {
|
||||||
data: {
|
let msg: MSAgentPromoteMessage = {
|
||||||
username: client.username!
|
op: MSAgentProtocolMessageType.Promote,
|
||||||
}
|
data: {
|
||||||
};
|
username: client.username!
|
||||||
for (const _client of this.getActiveClients()) {
|
}
|
||||||
_client.send(msg);
|
};
|
||||||
}
|
for (const _client of this.getActiveClients()) {
|
||||||
});
|
_client.send(msg);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private getActiveClients() {
|
private getActiveClients() {
|
||||||
return this.clients.filter(c => c.username !== null);
|
return this.clients.filter((c) => c.username !== null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,70 +1,73 @@
|
||||||
import path from "path";
|
import path from 'path';
|
||||||
import * as fs from 'fs/promises';
|
import * as fs from 'fs/promises';
|
||||||
import { TTSConfig } from "./config.js";
|
import { TTSConfig } from './config.js';
|
||||||
import { Readable } from "node:stream";
|
import { Readable } from 'node:stream';
|
||||||
import { ReadableStream } from 'node:stream/web';
|
import { ReadableStream } from 'node:stream/web';
|
||||||
import { finished } from "node:stream/promises";
|
import { finished } from 'node:stream/promises';
|
||||||
|
|
||||||
export class TTSClient {
|
export class TTSClient {
|
||||||
private config: TTSConfig;
|
private config: TTSConfig;
|
||||||
private deleteOps: Map<string, NodeJS.Timeout>
|
private deleteOps: Map<string, NodeJS.Timeout>;
|
||||||
|
|
||||||
constructor(config: TTSConfig) {
|
constructor(config: TTSConfig) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
if (!this.config.tempDir.endsWith('/')) this.config.tempDir += '/';
|
if (!this.config.tempDir.endsWith('/')) this.config.tempDir += '/';
|
||||||
this.deleteOps = new Map();
|
this.deleteOps = new Map();
|
||||||
}
|
}
|
||||||
|
|
||||||
async ensureDirectoryExists() {
|
async ensureDirectoryExists() {
|
||||||
let stat;
|
let stat;
|
||||||
try {
|
try {
|
||||||
stat = await fs.stat(this.config.tempDir);
|
stat = await fs.stat(this.config.tempDir);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
let error = e as NodeJS.ErrnoException;
|
let error = e as NodeJS.ErrnoException;
|
||||||
switch (error.code) {
|
switch (error.code) {
|
||||||
case "ENOTDIR": {
|
case 'ENOTDIR': {
|
||||||
console.warn("File exists at TTS temp directory path. Unlinking...");
|
console.warn('File exists at TTS temp directory path. Unlinking...');
|
||||||
await fs.unlink(this.config.tempDir.substring(0, this.config.tempDir.length - 1));
|
await fs.unlink(this.config.tempDir.substring(0, this.config.tempDir.length - 1));
|
||||||
// intentional fall-through
|
// intentional fall-through
|
||||||
}
|
}
|
||||||
case "ENOENT": {
|
case 'ENOENT': {
|
||||||
await fs.mkdir(this.config.tempDir, {recursive: true});
|
await fs.mkdir(this.config.tempDir, { recursive: true });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
console.error(`Cannot access TTS Temp dir: ${error.message}`);
|
console.error(`Cannot access TTS Temp dir: ${error.message}`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async synthesizeToFile(text: string, id: string) : Promise<string> {
|
async synthesizeToFile(text: string, id: string): Promise<string> {
|
||||||
this.ensureDirectoryExists();
|
this.ensureDirectoryExists();
|
||||||
let wavFilename = id + ".wav"
|
let wavFilename = id + '.wav';
|
||||||
let wavPath = path.join(this.config.tempDir, wavFilename);
|
let wavPath = path.join(this.config.tempDir, wavFilename);
|
||||||
try {
|
try {
|
||||||
await fs.unlink(wavPath);
|
await fs.unlink(wavPath);
|
||||||
} catch {}
|
} catch {}
|
||||||
let file = await fs.open(wavPath, fs.constants.O_CREAT | fs.constants.O_TRUNC | fs.constants.O_WRONLY);
|
let file = await fs.open(wavPath, fs.constants.O_CREAT | fs.constants.O_TRUNC | fs.constants.O_WRONLY);
|
||||||
let stream = file.createWriteStream();
|
let stream = file.createWriteStream();
|
||||||
let res = await fetch(this.config.server + "/api/synthesize", {
|
let res = await fetch(this.config.server + '/api/synthesize', {
|
||||||
method: "POST",
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json"
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
text,
|
text,
|
||||||
voice: this.config.voice
|
voice: this.config.voice
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
await finished(Readable.fromWeb(res.body as ReadableStream<any>).pipe(stream));
|
await finished(Readable.fromWeb(res.body as ReadableStream<any>).pipe(stream));
|
||||||
await file.close();
|
await file.close();
|
||||||
this.deleteOps.set(wavPath, setTimeout(async () => {
|
this.deleteOps.set(
|
||||||
await fs.unlink(wavPath);
|
wavPath,
|
||||||
this.deleteOps.delete(wavPath);
|
setTimeout(async () => {
|
||||||
}, this.config.wavExpirySeconds * 1000));
|
await fs.unlink(wavPath);
|
||||||
return wavFilename;
|
this.deleteOps.delete(wavPath);
|
||||||
}
|
}, this.config.wavExpirySeconds * 1000)
|
||||||
}
|
);
|
||||||
|
return wavFilename;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export const Config = {
|
export const Config = {
|
||||||
// The server address for the webapp to connect to. The below default is the same address the webapp is hosted at.
|
// 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}`
|
serverAddress: `${window.location.protocol}//${window.location.host}`
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,130 +1,130 @@
|
||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en" prefix="og: https://ogp.me/ns#">
|
<html lang="en" prefix="og: https://ogp.me/ns#">
|
||||||
<head>
|
<head>
|
||||||
<title>MSAgent Chat</title>
|
<title>MSAgent Chat</title>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta property="og:title" content="MSAgent Chat" />
|
<meta property="og:title" content="MSAgent Chat" />
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
<meta property="og:site_name" content="Computernewb's Internet Pub Hub" />
|
<meta property="og:site_name" content="Computernewb's Internet Pub Hub" />
|
||||||
<link rel="stylesheet" href="https://unpkg.com/98.css" />
|
<link rel="stylesheet" href="https://unpkg.com/98.css" />
|
||||||
<link type="text/css" rel="stylesheet" href="../css/style.scss" />
|
<link type="text/css" rel="stylesheet" href="../css/style.scss" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="logonView">
|
<div id="logonView">
|
||||||
<div class="window" id="logonWindow">
|
<div class="window" id="logonWindow">
|
||||||
<div class="title-bar">
|
<div class="title-bar">
|
||||||
<div class="title-bar-text">Log on to Agent Chat</div>
|
<div class="title-bar-text">Log on to Agent Chat</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="window-body">
|
<div class="window-body">
|
||||||
<div id="logonWindowLogo">
|
<div id="logonWindowLogo">
|
||||||
<img src="../../assets/agentchat.png?width=361&height=82"/>
|
<img src="../../assets/agentchat.png?width=361&height=82" />
|
||||||
</div>
|
</div>
|
||||||
<form id="logonForm">
|
<form id="logonForm">
|
||||||
<div id="logonUsernameContainer">
|
<div id="logonUsernameContainer">
|
||||||
<label for="logonUsername">User name:</label>
|
<label for="logonUsername">User name:</label>
|
||||||
<input type="text" id="logonUsername" required/>
|
<input type="text" id="logonUsername" required />
|
||||||
</div>
|
</div>
|
||||||
<div id="logonRoomContainer">
|
<div id="logonRoomContainer">
|
||||||
<label for="logonRoom">Room name:</label>
|
<label for="logonRoom">Room name:</label>
|
||||||
<input type="text" id="logonRoom" placeholder="Coming Soon" disabled/>
|
<input type="text" id="logonRoom" placeholder="Coming Soon" disabled />
|
||||||
</div>
|
</div>
|
||||||
<div id="logonButtonsContainer">
|
<div id="logonButtonsContainer">
|
||||||
<select id="agentSelect">
|
<select id="agentSelect">
|
||||||
<option value="" disabled selected hidden>Agent</option>
|
<option value="" disabled selected hidden>Agent</option>
|
||||||
</select>
|
</select>
|
||||||
<button type="submit" id="logonButton">Log on</button>
|
<button type="submit" id="logonButton">Log on</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="window" id="motdWindow">
|
<div class="window" id="motdWindow">
|
||||||
<div class="title-bar">
|
<div class="title-bar">
|
||||||
<div class="title-bar-text">
|
<div class="title-bar-text">Welcome to Agent Chat</div>
|
||||||
Welcome to Agent Chat
|
<div class="title-bar-controls">
|
||||||
</div>
|
<button aria-label="Close"></button>
|
||||||
<div class="title-bar-controls">
|
</div>
|
||||||
<button aria-label="Close"></button>
|
</div>
|
||||||
</div>
|
<div class="window-body">
|
||||||
</div>
|
<div id="motdContainer"></div>
|
||||||
<div class="window-body">
|
</div>
|
||||||
<div id="motdContainer"></div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ul id="bottomLinks">
|
<ul id="bottomLinks">
|
||||||
<li><a href="#" id="rulesLink">Rules</a></li><li><a target="blank" href="https://discord.gg/a4kqb4mGyX">Discord</a></li><li><a target="blank" href="https://git.computernewb.com/computernewb/msagent-chat">Source Code</a></li>
|
<li><a href="#" id="rulesLink">Rules</a></li>
|
||||||
</ul>
|
<li><a target="blank" href="https://discord.gg/a4kqb4mGyX">Discord</a></li>
|
||||||
</div>
|
<li><a target="blank" href="https://git.computernewb.com/computernewb/msagent-chat">Source Code</a></li>
|
||||||
<div id="chatView">
|
</ul>
|
||||||
<div class="window" id="roomSettingsWindow">
|
</div>
|
||||||
<div class="title-bar">
|
<div id="chatView">
|
||||||
<div class="title-bar-text">Room Settings</div>
|
<div class="window" id="roomSettingsWindow">
|
||||||
<div class="title-bar-controls">
|
<div class="title-bar">
|
||||||
<button aria-label="Minimize"></button>
|
<div class="title-bar-text">Room Settings</div>
|
||||||
<button aria-label="Close"></button>
|
<div class="title-bar-controls">
|
||||||
</div>
|
<button aria-label="Minimize"></button>
|
||||||
</div>
|
<button aria-label="Close"></button>
|
||||||
<div class="window-body" id="roomSettingsBody">
|
</div>
|
||||||
<menu role="tablist">
|
</div>
|
||||||
<li role="tab" aria-selected="true"><a>Wallpaper</a></li>
|
<div class="window-body" id="roomSettingsBody">
|
||||||
</menu>
|
<menu role="tablist">
|
||||||
<div class="window" role="tabpanel" id="roomSettingsTabContent">
|
<li role="tab" aria-selected="true"><a>Wallpaper</a></li>
|
||||||
<div class="window-body" id="roomSettingsWallpaperTab">
|
</menu>
|
||||||
<div class="wallpaperMonitor">
|
<div class="window" role="tabpanel" id="roomSettingsTabContent">
|
||||||
<div>
|
<div class="window-body" id="roomSettingsWallpaperTab">
|
||||||
<div class="wallpaperMonitorFg" id="roomWallpaperPreview"></div>
|
<div class="wallpaperMonitor">
|
||||||
<img src="../../assets/monitor.png"/>
|
<div>
|
||||||
</div>
|
<div class="wallpaperMonitorFg" id="roomWallpaperPreview"></div>
|
||||||
</div>
|
<img src="../../assets/monitor.png" />
|
||||||
<div>
|
</div>
|
||||||
<span>Select a picture or enter a URL to use as a wallpaper:</span>
|
</div>
|
||||||
<div id="roomSettingsWallpaperSelect">
|
<div>
|
||||||
<div class="mainCol">
|
<span>Select a picture or enter a URL to use as a wallpaper:</span>
|
||||||
<ul class="tree-view">
|
<div id="roomSettingsWallpaperSelect">
|
||||||
<!-- Should be actual links later but this is just a mockup for now :P -->
|
<div class="mainCol">
|
||||||
<li>None</li>
|
<ul class="tree-view">
|
||||||
<li>Blue Lace 16</li>
|
<!-- Should be actual links later but this is just a mockup for now :P -->
|
||||||
<li>Boiling Point</li>
|
<li>None</li>
|
||||||
<li>Chateau</li>
|
<li>Blue Lace 16</li>
|
||||||
<li>Coffee Bean</li>
|
<li>Boiling Point</li>
|
||||||
<li>Fall Memories</li>
|
<li>Chateau</li>
|
||||||
<li>FeatherTexture</li>
|
<li>Coffee Bean</li>
|
||||||
</ul>
|
<li>Fall Memories</li>
|
||||||
<input type="text" placeholder="Wallpaper URL" id="roomSettingsWallpaperURL"/>
|
<li>FeatherTexture</li>
|
||||||
</div>
|
</ul>
|
||||||
<div>
|
<input type="text" placeholder="Wallpaper URL" id="roomSettingsWallpaperURL" />
|
||||||
<div class="field-row-stacked">
|
</div>
|
||||||
<label for="roomSettingsWallpaperDisplay">Picture Display:</label>
|
<div>
|
||||||
<select id="roomSettingsWallpaperDisplay">
|
<div class="field-row-stacked">
|
||||||
<option selected>Center</option>
|
<label for="roomSettingsWallpaperDisplay">Picture Display:</label>
|
||||||
<option>Tile</option>
|
<select id="roomSettingsWallpaperDisplay">
|
||||||
<option>Stretch</option>
|
<option selected>Center</option>
|
||||||
</select>
|
<option>Tile</option>
|
||||||
</div>
|
<option>Stretch</option>
|
||||||
<div class="field-row">
|
</select>
|
||||||
<!-- *Should* be a HTML color select, but 98.css doesn't support those grr -->
|
</div>
|
||||||
<button>Color</button>
|
<div class="field-row">
|
||||||
</div>
|
<!-- *Should* be a HTML color select, but 98.css doesn't support those grr -->
|
||||||
</div>
|
<button>Color</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field-row fieldRowRight">
|
</div>
|
||||||
<button>OK</button>
|
</div>
|
||||||
<button>Cancel</button>
|
<div class="field-row fieldRowRight">
|
||||||
<button>Apply</button>
|
<button>OK</button>
|
||||||
</div>
|
<button>Cancel</button>
|
||||||
</div>
|
<button>Apply</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div id="chatBar">
|
</div>
|
||||||
<input type="text" id="chatInput" placeholder="Send a message"/>
|
|
||||||
<button id="chatSendBtn">Send</button>
|
<div id="chatBar">
|
||||||
</div>
|
<input type="text" id="chatInput" placeholder="Send a message" />
|
||||||
</div>
|
<button id="chatSendBtn">Send</button>
|
||||||
<script type="module" src="../ts/main.ts"></script>
|
</div>
|
||||||
</body>
|
</div>
|
||||||
</html>
|
<script type="module" src="../ts/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en" prefix="og: https://ogp.me/ns#">
|
<html lang="en" prefix="og: https://ogp.me/ns#">
|
||||||
<head>
|
<head>
|
||||||
<title>MSAgent Chat - testbed</title>
|
<title>MSAgent Chat - testbed</title>
|
||||||
|
@ -19,13 +19,12 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="field-row-stacked" style="width: 400px">
|
<div class="field-row-stacked" style="width: 400px">
|
||||||
<form id="acsUrlForm">
|
<form id="acsUrlForm">
|
||||||
<label for="acsUrl">Load ACS from URL</label><br>
|
<label for="acsUrl">Load ACS from URL</label><br />
|
||||||
<input type="url" id="acsUrl" required/><br>
|
<input type="url" id="acsUrl" required /><br />
|
||||||
<input type="submit" value="Load"/>
|
<input type="submit" value="Load" />
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<!-- This is where all Agents go -->
|
<!-- This is where all Agents go -->
|
||||||
<div id="agent-mount">
|
<div id="agent-mount"></div>
|
||||||
</div>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,110 +1,111 @@
|
||||||
export interface MSWindowConfig {
|
export interface MSWindowConfig {
|
||||||
minWidth: number,
|
minWidth: number;
|
||||||
minHeight: number,
|
minHeight: number;
|
||||||
maxWidth?: number | undefined,
|
maxWidth?: number | undefined;
|
||||||
maxHeight?: number | undefined,
|
maxHeight?: number | undefined;
|
||||||
startPosition: MSWindowStartPosition // TODO: Should be a union with the enum and a "Point" (containing X and Y)
|
startPosition: MSWindowStartPosition; // TODO: Should be a union with the enum and a "Point" (containing X and Y)
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum MSWindowStartPosition {
|
export enum MSWindowStartPosition {
|
||||||
TopLeft,
|
TopLeft,
|
||||||
Center
|
Center
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MSWindow {
|
export class MSWindow {
|
||||||
wnd: HTMLDivElement;
|
wnd: HTMLDivElement;
|
||||||
closeBtn: HTMLButtonElement | undefined;
|
closeBtn: HTMLButtonElement | undefined;
|
||||||
config: MSWindowConfig;
|
config: MSWindowConfig;
|
||||||
titlebar: HTMLDivElement;
|
titlebar: HTMLDivElement;
|
||||||
body: HTMLDivElement;
|
body: HTMLDivElement;
|
||||||
shown: boolean;
|
shown: boolean;
|
||||||
dragging: boolean;
|
dragging: boolean;
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
constructor(wnd: HTMLDivElement, config: MSWindowConfig) {
|
constructor(wnd: HTMLDivElement, config: MSWindowConfig) {
|
||||||
this.wnd = wnd;
|
this.wnd = wnd;
|
||||||
this.shown = false;
|
this.shown = false;
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.wnd.style.minWidth = config.minWidth + "px";
|
this.wnd.style.minWidth = config.minWidth + 'px';
|
||||||
this.wnd.style.minHeight = config.minHeight + "px";
|
this.wnd.style.minHeight = config.minHeight + 'px';
|
||||||
|
|
||||||
if (config.maxWidth)
|
if (config.maxWidth) this.wnd.style.maxWidth = config.maxWidth + 'px';
|
||||||
this.wnd.style.maxWidth = config.maxWidth + "px";
|
if (config.maxHeight) this.wnd.style.maxHeight = config.maxHeight + '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 titlebar = this.wnd.querySelector('div.title-bar');
|
||||||
let body = this.wnd.querySelector("div.window-body");
|
let body = this.wnd.querySelector('div.window-body');
|
||||||
if (!titlebar || !body)
|
if (!titlebar || !body) throw new Error('MSWindow is missing titlebar or body element.');
|
||||||
throw new Error("MSWindow is missing titlebar or body element.");
|
|
||||||
|
|
||||||
this.titlebar = titlebar as HTMLDivElement;
|
this.titlebar = titlebar as HTMLDivElement;
|
||||||
this.body = body as HTMLDivElement;
|
this.body = body as HTMLDivElement;
|
||||||
|
|
||||||
let closeBtn = this.titlebar.querySelector("div.title-bar-controls > button[aria-label='Close']") as HTMLButtonElement;
|
let closeBtn = this.titlebar.querySelector("div.title-bar-controls > button[aria-label='Close']") as HTMLButtonElement;
|
||||||
if (closeBtn) {
|
if (closeBtn) {
|
||||||
this.closeBtn = closeBtn;
|
this.closeBtn = closeBtn;
|
||||||
closeBtn.addEventListener('click', () => {
|
closeBtn.addEventListener('click', () => {
|
||||||
this.hide();
|
this.hide();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register window move handlers
|
// Register window move handlers
|
||||||
this.dragging = false;
|
this.dragging = false;
|
||||||
switch (this.config.startPosition) {
|
switch (this.config.startPosition) {
|
||||||
case MSWindowStartPosition.TopLeft: {
|
case MSWindowStartPosition.TopLeft: {
|
||||||
this.x = 0;
|
this.x = 0;
|
||||||
this.y = 0;
|
this.y = 0;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case MSWindowStartPosition.Center: {
|
case MSWindowStartPosition.Center: {
|
||||||
this.x = (document.documentElement.clientWidth / 2) - (this.config.minWidth / 2);
|
this.x = document.documentElement.clientWidth / 2 - this.config.minWidth / 2;
|
||||||
this.y = (document.documentElement.clientHeight / 2) - (this.config.minHeight / 2);
|
this.y = document.documentElement.clientHeight / 2 - this.config.minHeight / 2;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
throw new Error("Invalid start position");
|
throw new Error('Invalid start position');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.setLoc();
|
this.setLoc();
|
||||||
|
|
||||||
this.titlebar.addEventListener('mousedown', () => {
|
this.titlebar.addEventListener('mousedown', () => {
|
||||||
this.dragging = true;
|
this.dragging = true;
|
||||||
document.addEventListener('mouseup', () => {
|
document.addEventListener(
|
||||||
this.dragging = false;
|
'mouseup',
|
||||||
}, {once: true});
|
() => {
|
||||||
});
|
this.dragging = false;
|
||||||
|
},
|
||||||
|
{ once: true }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
document.addEventListener('mousemove', e => {
|
document.addEventListener('mousemove', (e) => {
|
||||||
if (!this.dragging) return;
|
if (!this.dragging) return;
|
||||||
this.x += e.movementX;
|
this.x += e.movementX;
|
||||||
this.y += e.movementY;
|
this.y += e.movementY;
|
||||||
this.setLoc();
|
this.setLoc();
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener('resize', () => {
|
window.addEventListener('resize', () => {
|
||||||
this.setLoc();
|
this.setLoc();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
show() {
|
show() {
|
||||||
this.wnd.classList.remove("d-none");
|
this.wnd.classList.remove('d-none');
|
||||||
this.shown = true;
|
this.shown = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
hide() {
|
hide() {
|
||||||
this.wnd.classList.add("d-none");
|
this.wnd.classList.add('d-none');
|
||||||
this.shown = false;
|
this.shown = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private setLoc() {
|
private setLoc() {
|
||||||
if (this.x < 0) this.x = 0;
|
if (this.x < 0) this.x = 0;
|
||||||
if (this.y < 0) this.y = 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.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;
|
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.top = this.y + 'px';
|
||||||
this.wnd.style.left = this.x + "px";
|
this.wnd.style.left = this.x + 'px';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,14 +62,17 @@ export class MSAgentClient {
|
||||||
this.users = [];
|
this.users = [];
|
||||||
this.admin = false;
|
this.admin = false;
|
||||||
|
|
||||||
document.addEventListener('keydown', this.loginCb = (e: KeyboardEvent) => {
|
document.addEventListener(
|
||||||
if (e.key === "l" && e.ctrlKey) {
|
'keydown',
|
||||||
e.preventDefault();
|
(this.loginCb = (e: KeyboardEvent) => {
|
||||||
let password = window.prompt("Papers, please");
|
if (e.key === 'l' && e.ctrlKey) {
|
||||||
if (!password) return;
|
e.preventDefault();
|
||||||
this.login(password);
|
let password = window.prompt('Papers, please');
|
||||||
}
|
if (!password) return;
|
||||||
});
|
this.login(password);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
on<E extends keyof MSAgentClientEvents>(event: E, callback: MSAgentClientEvents[E]): Unsubscribe {
|
on<E extends keyof MSAgentClientEvents>(event: E, callback: MSAgentClientEvents[E]): Unsubscribe {
|
||||||
|
@ -82,13 +85,13 @@ export class MSAgentClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMotd(): Promise<MOTD> {
|
async getMotd(): Promise<MOTD> {
|
||||||
let res = await fetch(this.url + "/api/motd/version");
|
let res = await fetch(this.url + '/api/motd/version');
|
||||||
let vs = await res.text();
|
let vs = await res.text();
|
||||||
let version = parseInt(vs);
|
let version = parseInt(vs);
|
||||||
if (isNaN(version)) {
|
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();
|
let html = await res.text();
|
||||||
return {
|
return {
|
||||||
version,
|
version,
|
||||||
|
@ -178,12 +181,12 @@ export class MSAgentClient {
|
||||||
ctx.clearItems();
|
ctx.clearItems();
|
||||||
// Mute
|
// Mute
|
||||||
let _user = user;
|
let _user = user;
|
||||||
let mute = new ContextMenuItem("Mute", () => {
|
let mute = new ContextMenuItem('Mute', () => {
|
||||||
if (_user.muted) {
|
if (_user.muted) {
|
||||||
mute.setName("Mute");
|
mute.setName('Mute');
|
||||||
_user.muted = false;
|
_user.muted = false;
|
||||||
} else {
|
} else {
|
||||||
mute.setName("Unmute");
|
mute.setName('Unmute');
|
||||||
_user.muted = true;
|
_user.muted = true;
|
||||||
_user.agent.stopSpeaking();
|
_user.agent.stopSpeaking();
|
||||||
this.playingAudio.get(_user.username)?.pause();
|
this.playingAudio.get(_user.username)?.pause();
|
||||||
|
@ -193,7 +196,7 @@ export class MSAgentClient {
|
||||||
// Admin
|
// Admin
|
||||||
if (this.admin) {
|
if (this.admin) {
|
||||||
// Get IP
|
// Get IP
|
||||||
let getip = new ContextMenuItem("Get IP", () => {
|
let getip = new ContextMenuItem('Get IP', () => {
|
||||||
let msg: MSAgentAdminGetIPMessage = {
|
let msg: MSAgentAdminGetIPMessage = {
|
||||||
op: MSAgentProtocolMessageType.Admin,
|
op: MSAgentProtocolMessageType.Admin,
|
||||||
data: {
|
data: {
|
||||||
|
@ -205,7 +208,7 @@ export class MSAgentClient {
|
||||||
});
|
});
|
||||||
ctx.addItem(getip);
|
ctx.addItem(getip);
|
||||||
// Kick
|
// Kick
|
||||||
let kick = new ContextMenuItem("Kick", () => {
|
let kick = new ContextMenuItem('Kick', () => {
|
||||||
let msg: MSAgentAdminKickMessage = {
|
let msg: MSAgentAdminKickMessage = {
|
||||||
op: MSAgentProtocolMessageType.Admin,
|
op: MSAgentProtocolMessageType.Admin,
|
||||||
data: {
|
data: {
|
||||||
|
@ -217,7 +220,7 @@ export class MSAgentClient {
|
||||||
});
|
});
|
||||||
ctx.addItem(kick);
|
ctx.addItem(kick);
|
||||||
// Ban
|
// Ban
|
||||||
let ban = new ContextMenuItem("Ban", () => {
|
let ban = new ContextMenuItem('Ban', () => {
|
||||||
let msg: MSAgentAdminBanMessage = {
|
let msg: MSAgentAdminBanMessage = {
|
||||||
op: MSAgentProtocolMessageType.Admin,
|
op: MSAgentProtocolMessageType.Admin,
|
||||||
data: {
|
data: {
|
||||||
|
@ -264,7 +267,7 @@ export class MSAgentClient {
|
||||||
this.charlimit = initMsg.data.charlimit;
|
this.charlimit = initMsg.data.charlimit;
|
||||||
for (let _user of initMsg.data.users) {
|
for (let _user of initMsg.data.users) {
|
||||||
let agent = await agentCreateCharacterFromUrl(this.url + '/api/agents/' + _user.agent);
|
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.addToDom(this.agentContainer);
|
||||||
agent.show();
|
agent.show();
|
||||||
let user = new User(_user.username, agent);
|
let user = new User(_user.username, agent);
|
||||||
|
@ -277,7 +280,7 @@ export class MSAgentClient {
|
||||||
case MSAgentProtocolMessageType.AddUser: {
|
case MSAgentProtocolMessageType.AddUser: {
|
||||||
let addUserMsg = msg as MSAgentAddUserMessage;
|
let addUserMsg = msg as MSAgentAddUserMessage;
|
||||||
let agent = await agentCreateCharacterFromUrl(this.url + '/api/agents/' + addUserMsg.data.agent);
|
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.addToDom(this.agentContainer);
|
||||||
agent.show();
|
agent.show();
|
||||||
let user = new User(addUserMsg.data.username, agent);
|
let user = new User(addUserMsg.data.username, agent);
|
||||||
|
@ -292,7 +295,7 @@ export class MSAgentClient {
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
user.agent.hide(true);
|
user.agent.hide(true);
|
||||||
if (this.playingAudio.has(user!.username)) {
|
if (this.playingAudio.has(user!.username)) {
|
||||||
this.playingAudio.get(user!.username)?.pause();
|
this.playingAudio.get(user!.username)?.pause();
|
||||||
this.playingAudio.delete(user!.username);
|
this.playingAudio.delete(user!.username);
|
||||||
}
|
}
|
||||||
this.users.splice(this.users.indexOf(user), 1);
|
this.users.splice(this.users.indexOf(user), 1);
|
||||||
|
@ -317,10 +320,10 @@ export class MSAgentClient {
|
||||||
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.playingAudio.get(user!.username) === audio) {
|
||||||
user!.agent.stopSpeaking();
|
user!.agent.stopSpeaking();
|
||||||
this.playingAudio.delete(user!.username);
|
this.playingAudio.delete(user!.username);
|
||||||
}
|
}
|
||||||
}, 1000);
|
}, 1000);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -338,7 +341,7 @@ export class MSAgentClient {
|
||||||
this.admin = true;
|
this.admin = true;
|
||||||
for (const user of this.users) this.setContextMenu(user);
|
for (const user of this.users) this.setContextMenu(user);
|
||||||
} else {
|
} else {
|
||||||
alert("Incorrect password!");
|
alert('Incorrect password!');
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -352,10 +355,10 @@ export class MSAgentClient {
|
||||||
}
|
}
|
||||||
case MSAgentProtocolMessageType.Promote: {
|
case MSAgentProtocolMessageType.Promote: {
|
||||||
let promoteMsg = msg as MSAgentPromoteMessage;
|
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;
|
if (!user) return;
|
||||||
user.admin = true;
|
user.admin = true;
|
||||||
user.agent.setUsername(user.username, "#ff0000");
|
user.agent.setUsername(user.username, '#ff0000');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case MSAgentProtocolMessageType.Error: {
|
case MSAgentProtocolMessageType.Error: {
|
||||||
|
|
|
@ -1,122 +1,121 @@
|
||||||
import { MSWindow, MSWindowStartPosition } from "./MSWindow.js";
|
import { MSWindow, MSWindowStartPosition } from './MSWindow.js';
|
||||||
import { agentInit } from "@msagent-chat/msagent.js";
|
import { agentInit } from '@msagent-chat/msagent.js';
|
||||||
import { MSAgentClient } from "./client.js";
|
import { MSAgentClient } from './client.js';
|
||||||
import { Config } from "../../config.js";
|
import { Config } from '../../config.js';
|
||||||
|
|
||||||
|
|
||||||
const elements = {
|
const elements = {
|
||||||
motdWindow: document.getElementById("motdWindow") as HTMLDivElement,
|
motdWindow: document.getElementById('motdWindow') as HTMLDivElement,
|
||||||
motdContainer: document.getElementById("motdContainer") as HTMLDivElement,
|
motdContainer: document.getElementById('motdContainer') as HTMLDivElement,
|
||||||
rulesLink: document.getElementById("rulesLink") as HTMLAnchorElement,
|
rulesLink: document.getElementById('rulesLink') as HTMLAnchorElement,
|
||||||
|
|
||||||
logonView: document.getElementById("logonView") as HTMLDivElement,
|
logonView: document.getElementById('logonView') as HTMLDivElement,
|
||||||
logonWindow: document.getElementById("logonWindow") as HTMLDivElement,
|
logonWindow: document.getElementById('logonWindow') as HTMLDivElement,
|
||||||
logonForm: document.getElementById("logonForm") as HTMLFormElement,
|
logonForm: document.getElementById('logonForm') as HTMLFormElement,
|
||||||
logonUsername: document.getElementById("logonUsername") as HTMLInputElement,
|
logonUsername: document.getElementById('logonUsername') as HTMLInputElement,
|
||||||
logonButton: document.getElementById("logonButton") as HTMLButtonElement,
|
logonButton: document.getElementById('logonButton') as HTMLButtonElement,
|
||||||
agentSelect: document.getElementById("agentSelect") as HTMLSelectElement,
|
agentSelect: document.getElementById('agentSelect') as HTMLSelectElement,
|
||||||
|
|
||||||
chatView: document.getElementById("chatView") as HTMLDivElement,
|
chatView: document.getElementById('chatView') as HTMLDivElement,
|
||||||
chatInput: document.getElementById("chatInput") as HTMLInputElement,
|
chatInput: document.getElementById('chatInput') as HTMLInputElement,
|
||||||
chatSendBtn: document.getElementById("chatSendBtn") as HTMLButtonElement,
|
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() {
|
function roomInit() {
|
||||||
Room = new MSAgentClient(Config.serverAddress, elements.chatView);
|
Room = new MSAgentClient(Config.serverAddress, elements.chatView);
|
||||||
Room.on('close', () => {
|
Room.on('close', () => {
|
||||||
for (let user of Room.getUsers()) {
|
for (let user of Room.getUsers()) {
|
||||||
user.agent.remove();
|
user.agent.remove();
|
||||||
}
|
}
|
||||||
roomInit();
|
roomInit();
|
||||||
loggingIn = false;
|
loggingIn = false;
|
||||||
elements.logonButton.disabled = false;
|
elements.logonButton.disabled = false;
|
||||||
logonWindow.show();
|
logonWindow.show();
|
||||||
elements.logonView.style.display = "block";
|
elements.logonView.style.display = 'block';
|
||||||
elements.chatView.style.display = "none";
|
elements.chatView.style.display = 'none';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let motdWindow = new MSWindow(elements.motdWindow, {
|
let motdWindow = new MSWindow(elements.motdWindow, {
|
||||||
minWidth: 600,
|
minWidth: 600,
|
||||||
minHeight: 300,
|
minHeight: 300,
|
||||||
maxWidth: 600,
|
maxWidth: 600,
|
||||||
startPosition: MSWindowStartPosition.Center
|
startPosition: MSWindowStartPosition.Center
|
||||||
});
|
});
|
||||||
|
|
||||||
let logonWindow = new MSWindow(elements.logonWindow, {
|
let logonWindow = new MSWindow(elements.logonWindow, {
|
||||||
minWidth: 500,
|
minWidth: 500,
|
||||||
minHeight: 275,
|
minHeight: 275,
|
||||||
startPosition: MSWindowStartPosition.Center
|
startPosition: MSWindowStartPosition.Center
|
||||||
});
|
});
|
||||||
|
|
||||||
let roomSettingsWindow = new MSWindow(elements.roomSettingsWindow, {
|
let roomSettingsWindow = new MSWindow(elements.roomSettingsWindow, {
|
||||||
minWidth: 398,
|
minWidth: 398,
|
||||||
minHeight: 442,
|
minHeight: 442,
|
||||||
startPosition: MSWindowStartPosition.Center
|
startPosition: MSWindowStartPosition.Center
|
||||||
});
|
});
|
||||||
|
|
||||||
logonWindow.show();
|
logonWindow.show();
|
||||||
// roomSettingsWindow.show();
|
// roomSettingsWindow.show();
|
||||||
|
|
||||||
let loggingIn = false;
|
let loggingIn = false;
|
||||||
elements.logonForm.addEventListener('submit', e => {
|
elements.logonForm.addEventListener('submit', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
connectToRoom();
|
connectToRoom();
|
||||||
});
|
});
|
||||||
|
|
||||||
elements.chatInput.addEventListener('keypress', e => {
|
elements.chatInput.addEventListener('keypress', (e) => {
|
||||||
// enter
|
// enter
|
||||||
if (e.key === "Enter") talk();
|
if (e.key === 'Enter') talk();
|
||||||
});
|
});
|
||||||
|
|
||||||
elements.chatSendBtn.addEventListener('click', () => {
|
elements.chatSendBtn.addEventListener('click', () => {
|
||||||
talk();
|
talk();
|
||||||
});
|
});
|
||||||
|
|
||||||
async function connectToRoom() {
|
async function connectToRoom() {
|
||||||
if (!elements.agentSelect.value) {
|
if (!elements.agentSelect.value) {
|
||||||
alert("Please select an agent.");
|
alert('Please select an agent.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (loggingIn) return;
|
if (loggingIn) return;
|
||||||
loggingIn = true;
|
loggingIn = true;
|
||||||
elements.logonButton.disabled = true;
|
elements.logonButton.disabled = true;
|
||||||
await Room.connect();
|
await Room.connect();
|
||||||
await Room.join(elements.logonUsername.value, elements.agentSelect.value);
|
await Room.join(elements.logonUsername.value, elements.agentSelect.value);
|
||||||
elements.chatInput.maxLength = Room.getCharlimit();
|
elements.chatInput.maxLength = Room.getCharlimit();
|
||||||
logonWindow.hide();
|
logonWindow.hide();
|
||||||
elements.logonView.style.display = "none";
|
elements.logonView.style.display = 'none';
|
||||||
elements.chatView.style.display = "block";
|
elements.chatView.style.display = 'block';
|
||||||
};
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', async () => {
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
await agentInit();
|
await agentInit();
|
||||||
for (const agent of await Room.getAgents()) {
|
for (const agent of await Room.getAgents()) {
|
||||||
let option = document.createElement("option");
|
let option = document.createElement('option');
|
||||||
option.innerText = agent.friendlyName;
|
option.innerText = agent.friendlyName;
|
||||||
option.value = agent.filename;
|
option.value = agent.filename;
|
||||||
elements.agentSelect.appendChild(option);
|
elements.agentSelect.appendChild(option);
|
||||||
}
|
}
|
||||||
let motd = await Room.getMotd();
|
let motd = await Room.getMotd();
|
||||||
elements.motdContainer.innerHTML = motd.html;
|
elements.motdContainer.innerHTML = motd.html;
|
||||||
let ver = localStorage.getItem("msagent-chat-motd-version");
|
let ver = localStorage.getItem('msagent-chat-motd-version');
|
||||||
if (!ver || parseInt(ver) !== motd.version) {
|
if (!ver || parseInt(ver) !== motd.version) {
|
||||||
motdWindow.show();
|
motdWindow.show();
|
||||||
localStorage.setItem("msagent-chat-motd-version", motd.version.toString());
|
localStorage.setItem('msagent-chat-motd-version', motd.version.toString());
|
||||||
}
|
}
|
||||||
elements.rulesLink.addEventListener('click', () => {
|
elements.rulesLink.addEventListener('click', () => {
|
||||||
motdWindow.show();
|
motdWindow.show();
|
||||||
})
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function talk() {
|
function talk() {
|
||||||
if (Room === null) return;
|
if (Room === null) return;
|
||||||
Room.talk(elements.chatInput.value);
|
Room.talk(elements.chatInput.value);
|
||||||
elements.chatInput.value = "";
|
elements.chatInput.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
roomInit();
|
roomInit();
|
||||||
|
|
|
@ -1,40 +1,40 @@
|
||||||
// Testbed code
|
// Testbed code
|
||||||
// This will go away when it isn't needed
|
// 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;
|
let w = window as any;
|
||||||
w.agents = [];
|
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 () => {
|
input.addEventListener('change', async () => {
|
||||||
let buffer = await input.files![0].arrayBuffer();
|
let buffer = await input.files![0].arrayBuffer();
|
||||||
|
|
||||||
console.log("Creating agent");
|
console.log('Creating agent');
|
||||||
let agent = msagent.agentCreateCharacter(new Uint8Array(buffer));
|
let agent = msagent.agentCreateCharacter(new Uint8Array(buffer));
|
||||||
|
|
||||||
w.agents.push(agent);
|
w.agents.push(agent);
|
||||||
|
|
||||||
agent.addToDom(mount);
|
agent.addToDom(mount);
|
||||||
|
|
||||||
agent.show();
|
agent.show();
|
||||||
console.log("Agent created");
|
console.log('Agent created');
|
||||||
})
|
});
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", async () => {
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
await msagent.agentInit();
|
await msagent.agentInit();
|
||||||
console.log("msagent initalized!");
|
console.log('msagent initalized!');
|
||||||
})
|
});
|
||||||
|
|
||||||
let form = document.getElementById("acsUrlForm") as HTMLFormElement;
|
let form = document.getElementById('acsUrlForm') as HTMLFormElement;
|
||||||
form.addEventListener('submit', e => {
|
form.addEventListener('submit', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
let url = (document.getElementById("acsUrl") as HTMLInputElement).value;
|
let url = (document.getElementById('acsUrl') as HTMLInputElement).value;
|
||||||
msagent.agentCreateCharacterFromUrl(url).then(agent => {
|
msagent.agentCreateCharacterFromUrl(url).then((agent) => {
|
||||||
w.agents.push(agent);
|
w.agents.push(agent);
|
||||||
agent.addToDom(document.body);
|
agent.addToDom(document.body);
|
||||||
agent.show();
|
agent.show();
|
||||||
console.log(`Loaded agent from ${url}`);
|
console.log(`Loaded agent from ${url}`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
import { Agent } from "@msagent-chat/msagent.js";
|
import { Agent } from '@msagent-chat/msagent.js';
|
||||||
|
|
||||||
export class User {
|
export class User {
|
||||||
username: string;
|
username: string;
|
||||||
agent: Agent;
|
agent: Agent;
|
||||||
muted: boolean;
|
muted: boolean;
|
||||||
admin: boolean;
|
admin: boolean;
|
||||||
|
|
||||||
constructor(username: string, agent: Agent) {
|
constructor(username: string, agent: Agent) {
|
||||||
this.username = username;
|
this.username = username;
|
||||||
this.agent = agent;
|
this.agent = agent;
|
||||||
this.muted = false;
|
this.muted = false;
|
||||||
this.admin = false;
|
this.admin = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
10
yarn.lock
10
yarn.lock
|
@ -3259,6 +3259,7 @@ __metadata:
|
||||||
"@parcel/packager-ts": "npm:2.12.0"
|
"@parcel/packager-ts": "npm:2.12.0"
|
||||||
"@parcel/transformer-sass": "npm:2.12.0"
|
"@parcel/transformer-sass": "npm:2.12.0"
|
||||||
"@parcel/transformer-typescript-types": "npm:2.12.0"
|
"@parcel/transformer-typescript-types": "npm:2.12.0"
|
||||||
|
prettier: "npm:^3.3.3"
|
||||||
typescript: "npm:>=3.0.0"
|
typescript: "npm:>=3.0.0"
|
||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
@ -3694,6 +3695,15 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"proc-log@npm:^4.1.0, proc-log@npm:^4.2.0":
|
||||||
version: 4.2.0
|
version: 4.2.0
|
||||||
resolution: "proc-log@npm:4.2.0"
|
resolution: "proc-log@npm:4.2.0"
|
||||||
|
|
Loading…
Reference in a new issue