format code with prettier

This commit is contained in:
Elijah R 2024-07-14 15:26:05 -04:00
parent cc2a9db92e
commit 1ad9ee14fe
30 changed files with 1224 additions and 1178 deletions

View file

@ -115,14 +115,14 @@ class AgentWordBalloonState {
positionUpdated() {
let size = this.char.getSize();
this.balloonCanvas.style.left = -((this.balloonCanvas.width / 2) - (size.w / 2)) + 'px';
this.balloonCanvas.style.left = -(this.balloonCanvas.width / 2 - size.w / 2) + 'px';
switch (this.position) {
case AgentWordBalloonPosition.AboveCentered: {
this.balloonCanvas.style.top = -(this.balloonCanvas.height) + 'px';
this.balloonCanvas.style.top = -this.balloonCanvas.height + 'px';
break;
}
case AgentWordBalloonPosition.BelowCentered: {
this.balloonCanvas.style.bottom = -(this.balloonCanvas.height) + 'px';
this.balloonCanvas.style.bottom = -this.balloonCanvas.height + 'px';
break;
}
}
@ -308,7 +308,7 @@ export class Agent {
this.stopSpeaking();
}
this.wordballoonState = new AgentWordBalloonState(this, text, true, AgentWordBalloonPosition.AboveCentered, "#000000");
this.wordballoonState = new AgentWordBalloonState(this, text, true, AgentWordBalloonPosition.AboveCentered, '#000000');
this.wordballoonState.positionUpdated();
this.wordballoonState.show();
}

View file

@ -5,7 +5,7 @@ export enum SeekDir {
BEG = 0,
CUR = 1,
END = 2
};
}
// A helper over DataView to make it more ergonomic for parsing file data.
export class BufferStream {
@ -19,7 +19,7 @@ export class BufferStream {
}
seek(where: number, whence: SeekDir) {
switch(whence) {
switch (whence) {
case SeekDir.BEG:
this.readPointer = where;
break;
@ -29,8 +29,7 @@ export class BufferStream {
break;
case SeekDir.END:
if(where > 0)
throw new Error("Cannot use SeekDir.END with where greater than 0");
if (where > 0) throw new Error('Cannot use SeekDir.END with where greater than 0');
this.readPointer = this.bufferImpl.length + whence;
break;
@ -39,10 +38,12 @@ export class BufferStream {
return this.readPointer;
}
tell() { return this.seek(0, SeekDir.CUR); }
tell() {
return this.seek(0, SeekDir.CUR);
}
// common impl function for read*()
private readImpl<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);
this.readPointer += size;
return res;
@ -57,16 +58,36 @@ export class BufferStream {
return new BufferStream(buffer, oldReadPointer);
}
readS8() { return this.readImpl(DataView.prototype.getInt8, 1); }
readU8() { return this.readImpl(DataView.prototype.getUint8, 1); }
readS16LE() { return this.readImpl(DataView.prototype.getInt16, 2, true); }
readS16BE() { return this.readImpl(DataView.prototype.getInt16, 2, false); }
readU16LE() { return this.readImpl(DataView.prototype.getUint16, 2, true); }
readU16BE() { return this.readImpl(DataView.prototype.getUint16, 2, false); }
readS32LE() { return this.readImpl(DataView.prototype.getInt32, 4, true); }
readS32BE() { return this.readImpl(DataView.prototype.getInt32, 4, false); }
readU32LE() { return this.readImpl(DataView.prototype.getUint32, 4, true); }
readU32BE() { return this.readImpl(DataView.prototype.getUint32, 4, false); }
readS8() {
return this.readImpl(DataView.prototype.getInt8, 1);
}
readU8() {
return this.readImpl(DataView.prototype.getUint8, 1);
}
readS16LE() {
return this.readImpl(DataView.prototype.getInt16, 2, true);
}
readS16BE() {
return this.readImpl(DataView.prototype.getInt16, 2, false);
}
readU16LE() {
return this.readImpl(DataView.prototype.getUint16, 2, true);
}
readU16BE() {
return this.readImpl(DataView.prototype.getUint16, 2, false);
}
readS32LE() {
return this.readImpl(DataView.prototype.getInt32, 4, true);
}
readS32BE() {
return this.readImpl(DataView.prototype.getInt32, 4, false);
}
readU32LE() {
return this.readImpl(DataView.prototype.getUint32, 4, true);
}
readU32BE() {
return this.readImpl(DataView.prototype.getUint32, 4, false);
}
// Use this for temporary offset modification, e.g: when reading
// a structure *pointed to* inside another structure.
@ -77,16 +98,15 @@ export class BufferStream {
this.seek(last, SeekDir.BEG);
}
readBool() : boolean {
readBool(): boolean {
let res = this.readU8();
return res != 0;
}
readString<TChar extends number>(len: number, charReader: (this: BufferStream) => TChar): string {
let str = "";
let str = '';
for(let i = 0; i < len; ++i)
str += String.fromCharCode(charReader.call(this));
for (let i = 0; i < len; ++i) str += String.fromCharCode(charReader.call(this));
// dispose of a nul terminator. We don't support other bare Agent formats,
// so we shouldn't need to add the "support" for that.
@ -96,8 +116,7 @@ export class BufferStream {
readPascalString(lengthReader: (this: BufferStream) => number = BufferStream.prototype.readU32LE, charReader: (this: BufferStream) => number = BufferStream.prototype.readU16LE) {
let len = lengthReader.call(this);
if(len == 0)
return "";
if (len == 0) return '';
return this.readString(len, charReader);
}
@ -116,16 +135,13 @@ export class BufferStream {
readCountedList<TObject>(objReader: (stream: BufferStream) => TObject, lengthReader: (this: BufferStream) => number = BufferStream.prototype.readU32LE): TObject[] {
let len = lengthReader.call(this);
let arr: TObject[] = [];
if(len == 0)
return arr;
if (len == 0) return arr;
for(let i = 0; i < len; ++i)
arr.push(objReader(this));
for (let i = 0; i < len; ++i) arr.push(objReader(this));
return arr;
}
raw() {
return this.bufferImpl;
}

View file

@ -35,7 +35,6 @@ export function agentCharacterParseACS(buffer: BufferStream): AcsData {
let imageInfoLocation = LOCATION.read(buffer);
let audioInfoLocation = LOCATION.read(buffer);
buffer.withOffset(characterInfoLocation.offset, () => {
acsData.characterInfo = AcsCharacterInfo.read(buffer);
});
@ -59,9 +58,9 @@ export function agentCreateCharacter(data: AcsData): Agent {
return new Agent(data);
}
export async function agentCreateCharacterFromUrl(url: string) : Promise<Agent> {
export async function agentCreateCharacterFromUrl(url: string): Promise<Agent> {
// just return the cache object
if(acsDataCache.has(url)) {
if (acsDataCache.has(url)) {
return agentCreateCharacter(acsDataCache.get(url)!);
} else {
let res = await fetch(url);

View file

@ -7,8 +7,8 @@ export class ContextMenuItem {
constructor(name: string, cb: Function) {
this.name = name;
this.cb = cb;
this.element = document.createElement("li");
this.element.classList.add("context-menu-item");
this.element = document.createElement('li');
this.element.classList.add('context-menu-item');
this.element.innerText = name;
this.element.addEventListener('mousedown', () => this.cb());
}
@ -31,30 +31,34 @@ export class ContextMenu {
private element: HTMLDivElement;
private list: HTMLUListElement;
private items: Array<ContextMenuItem>
private items: Array<ContextMenuItem>;
constructor(parent: HTMLElement) {
this.element = document.createElement("div");
this.list = document.createElement("ul");
this.element = document.createElement('div');
this.list = document.createElement('ul');
this.element.appendChild(this.list);
this.items = [];
this.element.classList.add("context-menu");
this.element.style.display = "none";
this.element.style.position = "fixed";
this.element.classList.add('context-menu');
this.element.style.display = 'none';
this.element.style.position = 'fixed';
parent.appendChild(this.element);
}
show(x: number, y: number) {
this.element.style.left = x + "px";
this.element.style.top = y + "px";
document.addEventListener('mousedown', () => {
this.element.style.left = x + 'px';
this.element.style.top = y + 'px';
document.addEventListener(
'mousedown',
() => {
this.hide();
}, {once: true});
this.element.style.display = "block";
},
{ once: true }
);
this.element.style.display = 'block';
}
hide() {
this.element.style.display = "none";
this.element.style.display = 'none';
}
addItem(item: ContextMenuItem) {
@ -70,7 +74,7 @@ export class ContextMenu {
}
getItem(name: string) {
return this.items.find(i => i.name === name);
return this.items.find((i) => i.name === name);
}
clearItems() {

View file

@ -14,16 +14,14 @@ export async function compressInit() {
compressWasm = await WebAssembly.instantiateStreaming(fetch(url));
}
function compressWasmGetExports() {
return (compressWasm.instance.exports as any) as CompressWasmExports;
return compressWasm.instance.exports as any as CompressWasmExports;
}
function compressWASMGetMemory() : WebAssembly.Memory {
function compressWASMGetMemory(): WebAssembly.Memory {
return compressWasmGetExports().memory;
}
// debugging
//(window as any).DEBUGcompressGetWASM = () => {
// return compressWasm;
@ -35,10 +33,10 @@ export function compressDecompress(src: Uint8Array, dest: Uint8Array) {
// Grow the WASM heap if needed. Funnily enough, this code is never hit in most
// ACSes, so IDK if it's even needed
let memory = compressWASMGetMemory();
if(memory.buffer.byteLength < src.length + dest.length) {
if (memory.buffer.byteLength < src.length + dest.length) {
// A WebAssembly page is 64kb, so we need to grow at least that much
let npages = Math.floor((src.length + dest.length) / 65535) + 1;
console.log("Need to grow WASM heap", npages, "pages", "(current byteLength is", memory.buffer.byteLength, ", we need", src.length + dest.length, ")");
console.log('Need to grow WASM heap', npages, 'pages', '(current byteLength is', memory.buffer.byteLength, ', we need', src.length + dest.length, ')');
memory.grow(npages);
}
@ -50,8 +48,7 @@ export function compressDecompress(src: Uint8Array, dest: Uint8Array) {
// Call the WASM compression routine
let nrBytesDecompressed = compressWasmGetExports().agentDecompressWASM(0, src.length, src.length, dest.length);
if(nrBytesDecompressed != dest.length)
throw new Error(`decompression failed: ${nrBytesDecompressed} != ${dest.length}`);
if (nrBytesDecompressed != dest.length) throw new Error(`decompression failed: ${nrBytesDecompressed} != ${dest.length}`);
// Dest will be memory[src.length..dest.length]
dest.set(copyBuffer.slice(src.length, src.length + dest.length), 0);

View file

@ -1,13 +1,12 @@
import { compressInit } from "./decompress.js";
import { wordballoonInit } from "./wordballoon.js";
export * from "./types.js";
export * from "./character.js";
export * from "./decompress.js";
export * from "./sprite.js";
export * from "./wordballoon.js";
export * from "./contextmenu.js";
import { compressInit } from './decompress.js';
import { wordballoonInit } from './wordballoon.js';
export * from './types.js';
export * from './character.js';
export * from './decompress.js';
export * from './sprite.js';
export * from './wordballoon.js';
export * from './contextmenu.js';
// Convinence function which initalizes all of msagent.js.
export async function agentInit() {

View file

@ -108,7 +108,7 @@ export class RGBAColor {
//quad.g = (val & 0x00ff0000) >> 16;
//quad.b = (val & 0x0000ff00) >> 8;
quad.r = (val & 0x000000ff);
quad.r = val & 0x000000ff;
quad.g = (val & 0x0000ff00) >> 8;
quad.b = (val & 0x00ff0000) >> 16;

View file

@ -1,16 +1,16 @@
export type Rect = {
x: number,
y: number,
w: number,
h: number
x: number;
y: number;
w: number;
h: number;
};
export type Size = {
w: number,
h: number
w: number;
h: number;
};
export type Point = {
x: number,
y: number
x: number;
y: number;
};

View file

@ -5,7 +5,6 @@ let corner_sprite: HTMLImageElement;
let straight_sprite: HTMLImageElement;
let tip_sprite: HTMLImageElement;
// Call *once* to initalize the wordballoon drawing system.
// Do not call other wordballoon* functions WITHOUT doing so.
export async function wordballoonInit() {
@ -93,8 +92,7 @@ export function wordballoonDraw(ctx: CanvasRenderingContext2D, at: Point, size:
// For now, we always simply use the center of the bottom..
// Draw the tip.
if (hasTip)
spriteDraw(ctx, tip_sprite, at.x + size.w / 2, at.y + 12 * (j + 1) - 1);
if (hasTip) spriteDraw(ctx, tip_sprite, at.x + size.w / 2, at.y + 12 * (j + 1) - 1);
ctx.restore();
@ -129,7 +127,7 @@ function wordWrapToStringList(text: string, maxLength: number) {
}
// This draws a wordballoon with text. This function respects the current context's font settings and does *not* modify them.
export function wordballoonDrawText(ctx: CanvasRenderingContext2D, at: Point, text: string, maxLen: number = 20, hasTip: boolean = true, color: string = "#000000"): Rect {
export function wordballoonDrawText(ctx: CanvasRenderingContext2D, at: Point, text: string, maxLen: number = 20, hasTip: boolean = true, color: string = '#000000'): Rect {
let lines = wordWrapToStringList(text, maxLen);
// Create metrics for each line
@ -175,7 +173,7 @@ export function wordballoonDrawText(ctx: CanvasRenderingContext2D, at: Point, te
return {
x: at.x,
y: at.y,
w: rectInner.w + (12*3) + 12,
h: rectInner.h + (13*3) + 18
}
w: rectInner.w + 12 * 3 + 12,
h: rectInner.h + 13 * 3 + 18
};
}

View file

@ -6,11 +6,15 @@
"protocol",
"msagent.js"
],
"scripts": {
"format": "prettier --write **/*.{ts,html,scss}"
},
"packageManager": "yarn@4.2.2",
"devDependencies": {
"@parcel/packager-ts": "2.12.0",
"@parcel/transformer-sass": "2.12.0",
"@parcel/transformer-typescript-types": "2.12.0",
"prettier": "^3.3.3",
"typescript": ">=3.0.0"
}
}

View file

@ -1,64 +1,64 @@
import { MSAgentProtocolMessage, MSAgentProtocolMessageType } from "./protocol";
import { MSAgentProtocolMessage, MSAgentProtocolMessageType } from './protocol';
export enum MSAgentAdminOperation {
// Client-to-server
Kick = "kick",
Ban = "ban",
Kick = 'kick',
Ban = 'ban',
// Bidirectional
Login = "login",
GetIP = "ip",
Login = 'login',
GetIP = 'ip'
}
export interface MSAgentAdminMessage extends MSAgentProtocolMessage {
op: MSAgentProtocolMessageType.Admin,
op: MSAgentProtocolMessageType.Admin;
data: {
action: MSAgentAdminOperation
}
action: MSAgentAdminOperation;
};
}
// Client-to-server
export interface MSAgentAdminLoginMessage extends MSAgentAdminMessage {
data: {
action: MSAgentAdminOperation.Login,
password: string
}
action: MSAgentAdminOperation.Login;
password: string;
};
}
export interface MSAgentAdminGetIPMessage extends MSAgentAdminMessage {
data: {
action: MSAgentAdminOperation.GetIP,
username: string
}
action: MSAgentAdminOperation.GetIP;
username: string;
};
}
export interface MSAgentAdminKickMessage extends MSAgentAdminMessage {
data: {
action: MSAgentAdminOperation.Kick,
username: string
}
action: MSAgentAdminOperation.Kick;
username: string;
};
}
export interface MSAgentAdminBanMessage extends MSAgentAdminMessage {
data: {
action: MSAgentAdminOperation.Ban,
username: string
}
action: MSAgentAdminOperation.Ban;
username: string;
};
}
// Server-to-client
export interface MSAgentAdminLoginResponse extends MSAgentAdminMessage {
data: {
action: MSAgentAdminOperation.Login,
success: boolean
}
action: MSAgentAdminOperation.Login;
success: boolean;
};
}
export interface MSAgentAdminGetIPResponse extends MSAgentAdminMessage {
data: {
action: MSAgentAdminOperation.GetIP,
username: string
ip: string
}
action: MSAgentAdminOperation.GetIP;
username: string;
ip: string;
};
}

View file

@ -2,90 +2,90 @@ export * from './admin.js';
export enum MSAgentProtocolMessageType {
// Client-to-server
KeepAlive = "nop",
Join = "join",
Talk = "talk",
Admin = "admin",
KeepAlive = 'nop',
Join = 'join',
Talk = 'talk',
Admin = 'admin',
// Server-to-client
Init = "init",
AddUser = "adduser",
RemoveUser = "remuser",
Chat = "chat",
Promote = "promote",
Error = "error"
Init = 'init',
AddUser = 'adduser',
RemoveUser = 'remuser',
Chat = 'chat',
Promote = 'promote',
Error = 'error'
}
export interface MSAgentProtocolMessage {
op: MSAgentProtocolMessageType
op: MSAgentProtocolMessageType;
}
// Client-to-server
export interface MSAgentJoinMessage extends MSAgentProtocolMessage {
op: MSAgentProtocolMessageType.Join,
op: MSAgentProtocolMessageType.Join;
data: {
username: string;
agent: string;
}
};
}
export interface MSAgentTalkMessage extends MSAgentProtocolMessage {
op: MSAgentProtocolMessageType.Talk,
op: MSAgentProtocolMessageType.Talk;
data: {
msg: string;
}
};
}
// Server-to-client
export interface MSAgentInitMessage extends MSAgentProtocolMessage {
op: MSAgentProtocolMessageType.Init,
data: {
username: string
agent: string
charlimit: number
users: {
username: string,
agent: string,
admin: boolean
}[]
}
}
export interface MSAgentAddUserMessage extends MSAgentProtocolMessage {
op: MSAgentProtocolMessageType.AddUser,
op: MSAgentProtocolMessageType.Init;
data: {
username: string;
agent: string;
}
charlimit: number;
users: {
username: string;
agent: string;
admin: boolean;
}[];
};
}
export interface MSAgentAddUserMessage extends MSAgentProtocolMessage {
op: MSAgentProtocolMessageType.AddUser;
data: {
username: string;
agent: string;
};
}
export interface MSAgentRemoveUserMessage extends MSAgentProtocolMessage {
op: MSAgentProtocolMessageType.RemoveUser,
op: MSAgentProtocolMessageType.RemoveUser;
data: {
username: string;
}
};
}
export interface MSAgentChatMessage extends MSAgentProtocolMessage {
op: MSAgentProtocolMessageType.Chat,
op: MSAgentProtocolMessageType.Chat;
data: {
username: string;
message: string;
audio? : string | undefined;
}
audio?: string | undefined;
};
}
export interface MSAgentPromoteMessage extends MSAgentProtocolMessage {
op: MSAgentProtocolMessageType.Promote,
op: MSAgentProtocolMessageType.Promote;
data: {
username: string;
}
};
}
export interface MSAgentErrorMessage extends MSAgentProtocolMessage {
op: MSAgentProtocolMessageType.Error,
op: MSAgentProtocolMessageType.Error;
data: {
error: string;
}
};
}

View file

@ -1,10 +1,24 @@
import EventEmitter from "events";
import { WebSocket } from "ws";
import { MSAgentAdminBanMessage, MSAgentAdminGetIPMessage, MSAgentAdminGetIPResponse, MSAgentAdminKickMessage, MSAgentAdminLoginMessage, MSAgentAdminLoginResponse, MSAgentAdminMessage, MSAgentAdminOperation, MSAgentErrorMessage, MSAgentJoinMessage, MSAgentProtocolMessage, MSAgentProtocolMessageType, MSAgentTalkMessage } from '@msagent-chat/protocol';
import { MSAgentChatRoom } from "./room.js";
import EventEmitter from 'events';
import { WebSocket } from 'ws';
import {
MSAgentAdminBanMessage,
MSAgentAdminGetIPMessage,
MSAgentAdminGetIPResponse,
MSAgentAdminKickMessage,
MSAgentAdminLoginMessage,
MSAgentAdminLoginResponse,
MSAgentAdminMessage,
MSAgentAdminOperation,
MSAgentErrorMessage,
MSAgentJoinMessage,
MSAgentProtocolMessage,
MSAgentProtocolMessageType,
MSAgentTalkMessage
} from '@msagent-chat/protocol';
import { MSAgentChatRoom } from './room.js';
import * as htmlentities from 'html-entities';
import RateLimiter from "./ratelimiter.js";
import { createHash } from "crypto";
import RateLimiter from './ratelimiter.js';
import { createHash } from 'crypto';
// Event types
@ -28,7 +42,7 @@ export class Client extends EventEmitter {
nopTimer: NodeJS.Timeout | undefined;
nopLevel: number;
chatRateLimit: RateLimiter
chatRateLimit: RateLimiter;
constructor(socket: WebSocket, room: MSAgentChatRoom, ip: string) {
super();
@ -48,7 +62,7 @@ export class Client extends EventEmitter {
this.socket.close();
return;
}
this.parseMessage(msg.toString("utf-8"));
this.parseMessage(msg.toString('utf-8'));
});
this.socket.on('error', () => {});
this.socket.on('close', () => {
@ -62,7 +76,7 @@ export class Client extends EventEmitter {
res();
return;
}
this.socket.send(JSON.stringify(msg), err => {
this.socket.send(JSON.stringify(msg), (err) => {
if (err) {
rej(err);
return;
@ -83,7 +97,7 @@ export class Client extends EventEmitter {
op: MSAgentProtocolMessageType.KeepAlive
});
}
}, 10000)
}, 10000);
}
private async parseMessage(data: string) {
@ -107,25 +121,25 @@ export class Client extends EventEmitter {
let msg: MSAgentErrorMessage = {
op: MSAgentProtocolMessageType.Error,
data: {
error: "Usernames can contain only numbers, letters, spaces, dashes, underscores, and dots, and it must be between 3 and 20 characters."
error: 'Usernames can contain only numbers, letters, spaces, dashes, underscores, and dots, and it must be between 3 and 20 characters.'
}
};
await this.send(msg);
this.socket.close();
return;
}
if (this.room.config.bannedWords.some(w => username.indexOf(w) !== -1)) {
if (this.room.config.bannedWords.some((w) => username.indexOf(w) !== -1)) {
this.socket.close();
return;
}
if (this.room.clients.some(u => u.username === username)) {
if (this.room.clients.some((u) => u.username === username)) {
let i = 1;
let uo = username;
do {
username = uo + i++;
} while (this.room.clients.some(u => u.username === username))
} while (this.room.clients.some((u) => u.username === username));
}
if (!this.room.agents.some(a => a.filename === joinMsg.data.agent)) {
if (!this.room.agents.some((a) => a.filename === joinMsg.data.agent)) {
this.socket.close();
return;
}
@ -140,7 +154,7 @@ export class Client extends EventEmitter {
return;
}
if (talkMsg.data.msg.length > this.room.config.charlimit) return;
if (this.room.config.bannedWords.some(w => talkMsg.data.msg.indexOf(w) !== -1)) {
if (this.room.config.bannedWords.some((w) => talkMsg.data.msg.indexOf(w) !== -1)) {
return;
}
this.emit('talk', talkMsg.data.msg);
@ -153,9 +167,9 @@ export class Client extends EventEmitter {
case MSAgentAdminOperation.Login: {
let loginMsg = adminMsg as MSAgentAdminLoginMessage;
if (this.admin || !loginMsg.data.password) return;
let sha256 = createHash("sha256");
let sha256 = createHash('sha256');
sha256.update(loginMsg.data.password);
let hash = sha256.digest("hex");
let hash = sha256.digest('hex');
sha256.destroy();
let success = false;
if (hash === this.room.config.adminPasswordHash) {
@ -163,20 +177,20 @@ export class Client extends EventEmitter {
success = true;
this.emit('admin');
}
let res : MSAgentAdminLoginResponse = {
let res: MSAgentAdminLoginResponse = {
op: MSAgentProtocolMessageType.Admin,
data: {
action: MSAgentAdminOperation.Login,
success
}
}
};
this.send(res);
break;
}
case MSAgentAdminOperation.GetIP: {
let getIPMsg = adminMsg as MSAgentAdminGetIPMessage;
if (!this.admin || !getIPMsg.data || !getIPMsg.data.username) return;
let _user = this.room.clients.find(c => c.username === getIPMsg.data.username);
let _user = this.room.clients.find((c) => c.username === getIPMsg.data.username);
if (!_user) return;
let res: MSAgentAdminGetIPResponse = {
op: MSAgentProtocolMessageType.Admin,
@ -192,12 +206,12 @@ export class Client extends EventEmitter {
case MSAgentAdminOperation.Kick: {
let kickMsg = adminMsg as MSAgentAdminKickMessage;
if (!this.admin || !kickMsg.data || !kickMsg.data.username) return;
let _user = this.room.clients.find(c => c.username === kickMsg.data.username);
let _user = this.room.clients.find((c) => c.username === kickMsg.data.username);
if (!_user) return;
let res: MSAgentErrorMessage = {
op: MSAgentProtocolMessageType.Error,
data: {
error: "You have been kicked."
error: 'You have been kicked.'
}
};
await _user.send(res);
@ -207,12 +221,12 @@ export class Client extends EventEmitter {
case MSAgentAdminOperation.Ban: {
let banMsg = adminMsg as MSAgentAdminBanMessage;
if (!this.admin || !banMsg.data || !banMsg.data.username) return;
let _user = this.room.clients.find(c => c.username === banMsg.data.username);
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."
error: 'You have been banned.'
}
};
await this.room.db.banUser(_user.ip, _user.username!);
@ -228,9 +242,5 @@ export class Client extends EventEmitter {
}
function validateUsername(username: string) {
return (
username.length >= 3 &&
username.length <= 20 &&
/^[a-zA-Z0-9\ \-\_\.]+$/.test(username)
);
return username.length >= 3 && username.length <= 20 && /^[a-zA-Z0-9\ \-\_\.]+$/.test(username);
}

View file

@ -3,7 +3,7 @@ export interface IConfig {
host: string;
port: number;
proxied: boolean;
}
};
mysql: MySQLConfig;
chat: ChatConfig;
motd: motdConfig;
@ -28,7 +28,7 @@ export interface ChatConfig {
bannedWords: string[];
ratelimits: {
chat: RateLimitConfig;
}
};
}
export interface motdConfig {
@ -41,7 +41,6 @@ export interface AgentConfig {
filename: string;
}
export interface RateLimitConfig {
seconds: number;
limit: number;

View file

@ -1,4 +1,4 @@
import { MySQLConfig } from "./config.js";
import { MySQLConfig } from './config.js';
import * as mysql from 'mysql2/promise';
export class Database {
@ -19,20 +19,20 @@ export class Database {
async init() {
let conn = await this.db.getConnection();
await conn.execute("CREATE TABLE IF NOT EXISTS bans (ip VARCHAR(45) NOT NULL PRIMARY KEY, username TEXT NOT NULL, time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP());");
await conn.execute('CREATE TABLE IF NOT EXISTS bans (ip VARCHAR(45) NOT NULL PRIMARY KEY, username TEXT NOT NULL, time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP());');
conn.release();
}
async banUser(ip: string, username: string) {
let conn = await this.db.getConnection();
await conn.execute("INSERT INTO bans (ip, username) VALUES (?, ?)", [ip, username]);
await conn.execute('INSERT INTO bans (ip, username) VALUES (?, ?)', [ip, username]);
conn.release();
}
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;
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;
return res[0][0]['cnt'] !== 0;
}
}

View file

@ -1,18 +1,18 @@
import { WebhookClient } from "discord.js";
import { DiscordConfig } from "./config.js";
import { WebhookClient } from 'discord.js';
import { DiscordConfig } from './config.js';
export class DiscordLogger {
private webhook: WebhookClient;
constructor(config: DiscordConfig) {
this.webhook = new WebhookClient({url: config.webhookURL});
this.webhook = new WebhookClient({ url: config.webhookURL });
}
logMsg(username: string, msg: string) {
this.webhook.send({
username,
allowedMentions: {},
content: msg,
content: msg
});
}
}

View file

@ -15,10 +15,8 @@ import { DiscordLogger } from './discord.js';
let config: IConfig;
let configPath: string;
if (process.argv.length < 3)
configPath = "./config.toml";
else
configPath = process.argv[2];
if (process.argv.length < 3) configPath = './config.toml';
else configPath = process.argv[2];
if (!fs.existsSync(configPath)) {
console.error(`${configPath} not found. Please copy config.example.toml and fill out fields.`);
@ -26,7 +24,7 @@ if (!fs.existsSync(configPath)) {
}
try {
let configRaw = fs.readFileSync(configPath, "utf-8");
let configRaw = fs.readFileSync(configPath, 'utf-8');
config = toml.parse(configRaw);
} catch (e) {
console.error(`Failed to read or parse ${configPath}: ${(e as Error).message}`);
@ -37,7 +35,7 @@ let db = new Database(config.mysql);
await db.init();
const app = Fastify({
logger: true,
logger: true
});
app.register(FastifyWS);
@ -48,12 +46,12 @@ if (config.tts.enabled) {
tts = new TTSClient(config.tts);
app.register(FastifyStatic, {
root: config.tts.tempDir,
prefix: "/api/tts/",
prefix: '/api/tts/',
decorateReply: false
});
}
if (!config.chat.agentsDir.endsWith("/")) config.chat.agentsDir += "/";
if (!config.chat.agentsDir.endsWith('/')) config.chat.agentsDir += '/';
if (!fs.existsSync(config.chat.agentsDir)) {
console.error(`Directory ${config.chat.agentsDir} does not exist.`);
process.exit(1);
@ -68,23 +66,23 @@ for (let agent of config.agents) {
app.register(FastifyStatic, {
root: path.resolve(config.chat.agentsDir),
prefix: "/api/agents/",
decorateReply: true,
prefix: '/api/agents/',
decorateReply: true
});
app.get("/api/agents", (req, res) => {
app.get('/api/agents', (req, res) => {
return config.agents;
});
// MOTD
app.get("/api/motd/version", (req, res) => {
res.header("Content-Type", "text/plain");
app.get('/api/motd/version', (req, res) => {
res.header('Content-Type', 'text/plain');
return config.motd.version.toString();
});
app.get("/api/motd/html", (req, res) => {
res.header("Content-Type", "text/html");
app.get('/api/motd/html', (req, res) => {
res.header('Content-Type', 'text/html');
return config.motd.html;
});
@ -96,21 +94,19 @@ if (config.discord.enabled) {
let room = new MSAgentChatRoom(config.chat, config.agents, db, tts, discord);
app.register(async app => {
app.get("/api/socket", {websocket: true}, async (socket, req) => {
app.register(async (app) => {
app.get('/api/socket', { websocket: true }, async (socket, req) => {
// TODO: Do this pre-upgrade and return the appropriate status codes
let ip: string;
if (config.http.proxied) {
if (req.headers["x-forwarded-for"] === undefined) {
if (req.headers['x-forwarded-for'] === undefined) {
console.error(`Warning: X-Forwarded-For not set! This is likely a misconfiguration of your reverse proxy.`);
socket.close();
return;
}
let xff = req.headers["x-forwarded-for"];
if (xff instanceof Array)
ip = xff[0];
else
ip = xff;
let xff = req.headers['x-forwarded-for'];
if (xff instanceof Array) ip = xff[0];
else ip = xff;
if (!isIP(ip)) {
console.error(`Warning: X-Forwarded-For malformed! This is likely a misconfiguration of your reverse proxy.`);
socket.close();
@ -123,15 +119,15 @@ app.register(async app => {
let msg: MSAgentErrorMessage = {
op: MSAgentProtocolMessageType.Error,
data: {
error: "You have been banned."
}
error: 'You have been banned.'
}
};
socket.send(JSON.stringify(msg), () => {
socket.close();
});
return;
}
let o = room.clients.filter(c => c.ip === ip);
let o = room.clients.filter((c) => c.ip === ip);
if (o.length >= config.chat.maxConnectionsPerIP) {
o[0].socket.close();
}
@ -140,5 +136,4 @@ app.register(async app => {
});
});
app.listen({host: config.http.host, port: config.http.port});
app.listen({ host: config.http.host, port: config.http.port });

View file

@ -1,16 +1,24 @@
import { MSAgentAddUserMessage, MSAgentChatMessage, MSAgentInitMessage, MSAgentPromoteMessage, MSAgentProtocolMessage, MSAgentProtocolMessageType, MSAgentRemoveUserMessage } from "@msagent-chat/protocol";
import { Client } from "./client.js";
import { TTSClient } from "./tts.js";
import { AgentConfig, ChatConfig } from "./config.js";
import {
MSAgentAddUserMessage,
MSAgentChatMessage,
MSAgentInitMessage,
MSAgentPromoteMessage,
MSAgentProtocolMessage,
MSAgentProtocolMessageType,
MSAgentRemoveUserMessage
} from '@msagent-chat/protocol';
import { Client } from './client.js';
import { TTSClient } from './tts.js';
import { AgentConfig, ChatConfig } from './config.js';
import * as htmlentities from 'html-entities';
import { Database } from "./database.js";
import { DiscordLogger } from "./discord.js";
import { Database } from './database.js';
import { DiscordLogger } from './discord.js';
export class MSAgentChatRoom {
agents: AgentConfig[];
clients: Client[];
tts: TTSClient | null;
msgId : number = 0;
msgId: number = 0;
config: ChatConfig;
db: Database;
discord: DiscordLogger | null;
@ -40,18 +48,20 @@ export class MSAgentChatRoom {
}
});
client.on('join', () => {
let initmsg : MSAgentInitMessage = {
let initmsg: MSAgentInitMessage = {
op: MSAgentProtocolMessageType.Init,
data: {
username: client.username!,
agent: client.agent!,
charlimit: this.config.charlimit,
users: this.clients.filter(c => c.username !== null).map(c => {
users: this.clients
.filter((c) => c.username !== null)
.map((c) => {
return {
username: c.username!,
agent: c.agent!,
admin: c.admin
}
};
})
}
};
@ -62,12 +72,12 @@ export class MSAgentChatRoom {
username: client.username!,
agent: client.agent!
}
}
for (const _client of this.getActiveClients().filter(c => c !== client)) {
};
for (const _client of this.getActiveClients().filter((c) => c !== client)) {
_client.send(msg);
}
});
client.on('talk', async message => {
client.on('talk', async (message) => {
let msg: MSAgentChatMessage = {
op: MSAgentProtocolMessageType.Chat,
data: {
@ -77,7 +87,7 @@ export class MSAgentChatRoom {
};
if (this.tts !== null) {
let filename = await this.tts.synthesizeToFile(message, (++this.msgId).toString(10));
msg.data.audio = "/api/tts/" + filename;
msg.data.audio = '/api/tts/' + filename;
}
for (const _client of this.getActiveClients()) {
_client.send(msg);
@ -98,6 +108,6 @@ export class MSAgentChatRoom {
}
private getActiveClients() {
return this.clients.filter(c => c.username !== null);
return this.clients.filter((c) => c.username !== null);
}
}

View file

@ -1,13 +1,13 @@
import path from "path";
import path from 'path';
import * as fs from 'fs/promises';
import { TTSConfig } from "./config.js";
import { Readable } from "node:stream";
import { TTSConfig } from './config.js';
import { Readable } from 'node:stream';
import { ReadableStream } from 'node:stream/web';
import { finished } from "node:stream/promises";
import { finished } from 'node:stream/promises';
export class TTSClient {
private config: TTSConfig;
private deleteOps: Map<string, NodeJS.Timeout>
private deleteOps: Map<string, NodeJS.Timeout>;
constructor(config: TTSConfig) {
this.config = config;
@ -22,13 +22,13 @@ export class TTSClient {
} catch (e) {
let error = e as NodeJS.ErrnoException;
switch (error.code) {
case "ENOTDIR": {
console.warn("File exists at TTS temp directory path. Unlinking...");
case 'ENOTDIR': {
console.warn('File exists at TTS temp directory path. Unlinking...');
await fs.unlink(this.config.tempDir.substring(0, this.config.tempDir.length - 1));
// intentional fall-through
}
case "ENOENT": {
await fs.mkdir(this.config.tempDir, {recursive: true});
case 'ENOENT': {
await fs.mkdir(this.config.tempDir, { recursive: true });
break;
}
default: {
@ -40,19 +40,19 @@ export class TTSClient {
}
}
async synthesizeToFile(text: string, id: string) : Promise<string> {
async synthesizeToFile(text: string, id: string): Promise<string> {
this.ensureDirectoryExists();
let wavFilename = id + ".wav"
let wavFilename = id + '.wav';
let wavPath = path.join(this.config.tempDir, wavFilename);
try {
await fs.unlink(wavPath);
} catch {}
let file = await fs.open(wavPath, fs.constants.O_CREAT | fs.constants.O_TRUNC | fs.constants.O_WRONLY);
let stream = file.createWriteStream();
let res = await fetch(this.config.server + "/api/synthesize", {
method: "POST",
let res = await fetch(this.config.server + '/api/synthesize', {
method: 'POST',
headers: {
"Content-Type": "application/json"
'Content-Type': 'application/json'
},
body: JSON.stringify({
text,
@ -61,10 +61,13 @@ export class TTSClient {
});
await finished(Readable.fromWeb(res.body as ReadableStream<any>).pipe(stream));
await file.close();
this.deleteOps.set(wavPath, setTimeout(async () => {
this.deleteOps.set(
wavPath,
setTimeout(async () => {
await fs.unlink(wavPath);
this.deleteOps.delete(wavPath);
}, this.config.wavExpirySeconds * 1000));
}, this.config.wavExpirySeconds * 1000)
);
return wavFilename;
}
}

View file

@ -1,4 +1,4 @@
export const Config = {
// The server address for the webapp to connect to. The below default is the same address the webapp is hosted at.
serverAddress: `${window.location.protocol}//${window.location.host}`
}
};

View file

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en" prefix="og: https://ogp.me/ns#">
<head>
<title>MSAgent Chat</title>
@ -18,16 +18,16 @@
</div>
<div class="window-body">
<div id="logonWindowLogo">
<img src="../../assets/agentchat.png?width=361&height=82"/>
<img src="../../assets/agentchat.png?width=361&height=82" />
</div>
<form id="logonForm">
<div id="logonUsernameContainer">
<label for="logonUsername">User name:</label>
<input type="text" id="logonUsername" required/>
<input type="text" id="logonUsername" required />
</div>
<div id="logonRoomContainer">
<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 id="logonButtonsContainer">
<select id="agentSelect">
@ -41,9 +41,7 @@
<div class="window" id="motdWindow">
<div class="title-bar">
<div class="title-bar-text">
Welcome to Agent Chat
</div>
<div class="title-bar-text">Welcome to Agent Chat</div>
<div class="title-bar-controls">
<button aria-label="Close"></button>
</div>
@ -54,7 +52,9 @@
</div>
<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>
<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>
</ul>
</div>
<div id="chatView">
@ -75,7 +75,7 @@
<div class="wallpaperMonitor">
<div>
<div class="wallpaperMonitorFg" id="roomWallpaperPreview"></div>
<img src="../../assets/monitor.png"/>
<img src="../../assets/monitor.png" />
</div>
</div>
<div>
@ -92,7 +92,7 @@
<li>Fall Memories</li>
<li>FeatherTexture</li>
</ul>
<input type="text" placeholder="Wallpaper URL" id="roomSettingsWallpaperURL"/>
<input type="text" placeholder="Wallpaper URL" id="roomSettingsWallpaperURL" />
</div>
<div>
<div class="field-row-stacked">
@ -121,7 +121,7 @@
</div>
<div id="chatBar">
<input type="text" id="chatInput" placeholder="Send a message"/>
<input type="text" id="chatInput" placeholder="Send a message" />
<button id="chatSendBtn">Send</button>
</div>
</div>

View file

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en" prefix="og: https://ogp.me/ns#">
<head>
<title>MSAgent Chat - testbed</title>
@ -19,13 +19,12 @@
</div>
<div class="field-row-stacked" style="width: 400px">
<form id="acsUrlForm">
<label for="acsUrl">Load ACS from URL</label><br>
<input type="url" id="acsUrl" required/><br>
<input type="submit" value="Load"/>
<label for="acsUrl">Load ACS from URL</label><br />
<input type="url" id="acsUrl" required /><br />
<input type="submit" value="Load" />
</form>
</div>
<!-- This is where all Agents go -->
<div id="agent-mount">
</div>
<div id="agent-mount"></div>
</body>
</html>

View file

@ -1,9 +1,9 @@
export interface MSWindowConfig {
minWidth: number,
minHeight: number,
maxWidth?: number | undefined,
maxHeight?: number | undefined,
startPosition: MSWindowStartPosition // TODO: Should be a union with the enum and a "Point" (containing X and Y)
minWidth: number;
minHeight: number;
maxWidth?: number | undefined;
maxHeight?: number | undefined;
startPosition: MSWindowStartPosition; // TODO: Should be a union with the enum and a "Point" (containing X and Y)
}
export enum MSWindowStartPosition {
@ -25,20 +25,17 @@ export class MSWindow {
this.wnd = wnd;
this.shown = false;
this.config = config;
this.wnd.style.minWidth = config.minWidth + "px";
this.wnd.style.minHeight = config.minHeight + "px";
this.wnd.style.minWidth = config.minWidth + 'px';
this.wnd.style.minHeight = config.minHeight + 'px';
if (config.maxWidth)
this.wnd.style.maxWidth = config.maxWidth + "px";
if (config.maxHeight)
this.wnd.style.maxHeight = config.maxHeight + "px";
if (config.maxWidth) this.wnd.style.maxWidth = config.maxWidth + 'px';
if (config.maxHeight) this.wnd.style.maxHeight = config.maxHeight + 'px';
this.wnd.classList.add("d-none");
this.wnd.classList.add('d-none');
let titlebar = this.wnd.querySelector("div.title-bar");
let body = this.wnd.querySelector("div.window-body");
if (!titlebar || !body)
throw new Error("MSWindow is missing titlebar or body element.");
let titlebar = this.wnd.querySelector('div.title-bar');
let body = this.wnd.querySelector('div.window-body');
if (!titlebar || !body) throw new Error('MSWindow is missing titlebar or body element.');
this.titlebar = titlebar as HTMLDivElement;
this.body = body as HTMLDivElement;
@ -60,24 +57,28 @@ export class MSWindow {
break;
}
case MSWindowStartPosition.Center: {
this.x = (document.documentElement.clientWidth / 2) - (this.config.minWidth / 2);
this.y = (document.documentElement.clientHeight / 2) - (this.config.minHeight / 2);
this.x = document.documentElement.clientWidth / 2 - this.config.minWidth / 2;
this.y = document.documentElement.clientHeight / 2 - this.config.minHeight / 2;
break;
}
default: {
throw new Error("Invalid start position");
throw new Error('Invalid start position');
}
}
this.setLoc();
this.titlebar.addEventListener('mousedown', () => {
this.dragging = true;
document.addEventListener('mouseup', () => {
document.addEventListener(
'mouseup',
() => {
this.dragging = false;
}, {once: true});
},
{ once: true }
);
});
document.addEventListener('mousemove', e => {
document.addEventListener('mousemove', (e) => {
if (!this.dragging) return;
this.x += e.movementX;
this.y += e.movementY;
@ -90,12 +91,12 @@ export class MSWindow {
}
show() {
this.wnd.classList.remove("d-none");
this.wnd.classList.remove('d-none');
this.shown = true;
}
hide() {
this.wnd.classList.add("d-none");
this.wnd.classList.add('d-none');
this.shown = false;
}
@ -104,7 +105,7 @@ export class MSWindow {
if (this.y < 0) this.y = 0;
if (this.x > document.documentElement.clientWidth - this.config.minWidth) this.x = document.documentElement.clientWidth - this.config.minWidth;
if (this.y > document.documentElement.clientHeight - this.config.minHeight) this.y = document.documentElement.clientHeight - this.config.minHeight;
this.wnd.style.top = this.y + "px";
this.wnd.style.left = this.x + "px";
this.wnd.style.top = this.y + 'px';
this.wnd.style.left = this.x + 'px';
}
}

View file

@ -62,14 +62,17 @@ export class MSAgentClient {
this.users = [];
this.admin = false;
document.addEventListener('keydown', this.loginCb = (e: KeyboardEvent) => {
if (e.key === "l" && e.ctrlKey) {
document.addEventListener(
'keydown',
(this.loginCb = (e: KeyboardEvent) => {
if (e.key === 'l' && e.ctrlKey) {
e.preventDefault();
let password = window.prompt("Papers, please");
let password = window.prompt('Papers, please');
if (!password) return;
this.login(password);
}
});
})
);
}
on<E extends keyof MSAgentClientEvents>(event: E, callback: MSAgentClientEvents[E]): Unsubscribe {
@ -82,13 +85,13 @@ export class MSAgentClient {
}
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 version = parseInt(vs);
if (isNaN(version)) {
throw new Error("Version was NaN");
throw new Error('Version was NaN');
}
res = await fetch(this.url + "/api/motd/html");
res = await fetch(this.url + '/api/motd/html');
let html = await res.text();
return {
version,
@ -178,12 +181,12 @@ export class MSAgentClient {
ctx.clearItems();
// Mute
let _user = user;
let mute = new ContextMenuItem("Mute", () => {
let mute = new ContextMenuItem('Mute', () => {
if (_user.muted) {
mute.setName("Mute");
mute.setName('Mute');
_user.muted = false;
} else {
mute.setName("Unmute");
mute.setName('Unmute');
_user.muted = true;
_user.agent.stopSpeaking();
this.playingAudio.get(_user.username)?.pause();
@ -193,7 +196,7 @@ export class MSAgentClient {
// Admin
if (this.admin) {
// Get IP
let getip = new ContextMenuItem("Get IP", () => {
let getip = new ContextMenuItem('Get IP', () => {
let msg: MSAgentAdminGetIPMessage = {
op: MSAgentProtocolMessageType.Admin,
data: {
@ -205,7 +208,7 @@ export class MSAgentClient {
});
ctx.addItem(getip);
// Kick
let kick = new ContextMenuItem("Kick", () => {
let kick = new ContextMenuItem('Kick', () => {
let msg: MSAgentAdminKickMessage = {
op: MSAgentProtocolMessageType.Admin,
data: {
@ -217,7 +220,7 @@ export class MSAgentClient {
});
ctx.addItem(kick);
// Ban
let ban = new ContextMenuItem("Ban", () => {
let ban = new ContextMenuItem('Ban', () => {
let msg: MSAgentAdminBanMessage = {
op: MSAgentProtocolMessageType.Admin,
data: {
@ -264,7 +267,7 @@ export class MSAgentClient {
this.charlimit = initMsg.data.charlimit;
for (let _user of initMsg.data.users) {
let agent = await agentCreateCharacterFromUrl(this.url + '/api/agents/' + _user.agent);
agent.setUsername(_user.username, _user.admin ? "#FF0000" : "#000000");
agent.setUsername(_user.username, _user.admin ? '#FF0000' : '#000000');
agent.addToDom(this.agentContainer);
agent.show();
let user = new User(_user.username, agent);
@ -277,7 +280,7 @@ export class MSAgentClient {
case MSAgentProtocolMessageType.AddUser: {
let addUserMsg = msg as MSAgentAddUserMessage;
let agent = await agentCreateCharacterFromUrl(this.url + '/api/agents/' + addUserMsg.data.agent);
agent.setUsername(addUserMsg.data.username, "#000000");
agent.setUsername(addUserMsg.data.username, '#000000');
agent.addToDom(this.agentContainer);
agent.show();
let user = new User(addUserMsg.data.username, agent);
@ -338,7 +341,7 @@ export class MSAgentClient {
this.admin = true;
for (const user of this.users) this.setContextMenu(user);
} else {
alert("Incorrect password!");
alert('Incorrect password!');
}
break;
}
@ -352,10 +355,10 @@ export class MSAgentClient {
}
case MSAgentProtocolMessageType.Promote: {
let promoteMsg = msg as MSAgentPromoteMessage;
let user = this.users.find(u => u.username === promoteMsg.data.username);
let user = this.users.find((u) => u.username === promoteMsg.data.username);
if (!user) return;
user.admin = true;
user.agent.setUsername(user.username, "#ff0000");
user.agent.setUsername(user.username, '#ff0000');
break;
}
case MSAgentProtocolMessageType.Error: {

View file

@ -1,29 +1,28 @@
import { MSWindow, MSWindowStartPosition } from "./MSWindow.js";
import { agentInit } from "@msagent-chat/msagent.js";
import { MSAgentClient } from "./client.js";
import { Config } from "../../config.js";
import { MSWindow, MSWindowStartPosition } from './MSWindow.js';
import { agentInit } from '@msagent-chat/msagent.js';
import { MSAgentClient } from './client.js';
import { Config } from '../../config.js';
const elements = {
motdWindow: document.getElementById("motdWindow") as HTMLDivElement,
motdContainer: document.getElementById("motdContainer") as HTMLDivElement,
rulesLink: document.getElementById("rulesLink") as HTMLAnchorElement,
motdWindow: document.getElementById('motdWindow') as HTMLDivElement,
motdContainer: document.getElementById('motdContainer') as HTMLDivElement,
rulesLink: document.getElementById('rulesLink') as HTMLAnchorElement,
logonView: document.getElementById("logonView") as HTMLDivElement,
logonWindow: document.getElementById("logonWindow") as HTMLDivElement,
logonForm: document.getElementById("logonForm") as HTMLFormElement,
logonUsername: document.getElementById("logonUsername") as HTMLInputElement,
logonButton: document.getElementById("logonButton") as HTMLButtonElement,
agentSelect: document.getElementById("agentSelect") as HTMLSelectElement,
logonView: document.getElementById('logonView') as HTMLDivElement,
logonWindow: document.getElementById('logonWindow') as HTMLDivElement,
logonForm: document.getElementById('logonForm') as HTMLFormElement,
logonUsername: document.getElementById('logonUsername') as HTMLInputElement,
logonButton: document.getElementById('logonButton') as HTMLButtonElement,
agentSelect: document.getElementById('agentSelect') as HTMLSelectElement,
chatView: document.getElementById("chatView") as HTMLDivElement,
chatInput: document.getElementById("chatInput") as HTMLInputElement,
chatSendBtn: document.getElementById("chatSendBtn") as HTMLButtonElement,
chatView: document.getElementById('chatView') as HTMLDivElement,
chatInput: document.getElementById('chatInput') as HTMLInputElement,
chatSendBtn: document.getElementById('chatSendBtn') as HTMLButtonElement,
roomSettingsWindow: document.getElementById("roomSettingsWindow") as HTMLDivElement
}
roomSettingsWindow: document.getElementById('roomSettingsWindow') as HTMLDivElement
};
let Room : MSAgentClient;
let Room: MSAgentClient;
function roomInit() {
Room = new MSAgentClient(Config.serverAddress, elements.chatView);
@ -35,8 +34,8 @@ function roomInit() {
loggingIn = false;
elements.logonButton.disabled = false;
logonWindow.show();
elements.logonView.style.display = "block";
elements.chatView.style.display = "none";
elements.logonView.style.display = 'block';
elements.chatView.style.display = 'none';
});
}
@ -63,14 +62,14 @@ logonWindow.show();
// roomSettingsWindow.show();
let loggingIn = false;
elements.logonForm.addEventListener('submit', e => {
elements.logonForm.addEventListener('submit', (e) => {
e.preventDefault();
connectToRoom();
});
elements.chatInput.addEventListener('keypress', e => {
elements.chatInput.addEventListener('keypress', (e) => {
// enter
if (e.key === "Enter") talk();
if (e.key === 'Enter') talk();
});
elements.chatSendBtn.addEventListener('click', () => {
@ -79,7 +78,7 @@ elements.chatSendBtn.addEventListener('click', () => {
async function connectToRoom() {
if (!elements.agentSelect.value) {
alert("Please select an agent.");
alert('Please select an agent.');
return;
}
if (loggingIn) return;
@ -89,34 +88,34 @@ async function connectToRoom() {
await Room.join(elements.logonUsername.value, elements.agentSelect.value);
elements.chatInput.maxLength = Room.getCharlimit();
logonWindow.hide();
elements.logonView.style.display = "none";
elements.chatView.style.display = "block";
};
elements.logonView.style.display = 'none';
elements.chatView.style.display = 'block';
}
document.addEventListener('DOMContentLoaded', async () => {
await agentInit();
for (const agent of await Room.getAgents()) {
let option = document.createElement("option");
let option = document.createElement('option');
option.innerText = agent.friendlyName;
option.value = agent.filename;
elements.agentSelect.appendChild(option);
}
let motd = await Room.getMotd();
elements.motdContainer.innerHTML = motd.html;
let ver = localStorage.getItem("msagent-chat-motd-version");
let ver = localStorage.getItem('msagent-chat-motd-version');
if (!ver || parseInt(ver) !== motd.version) {
motdWindow.show();
localStorage.setItem("msagent-chat-motd-version", motd.version.toString());
localStorage.setItem('msagent-chat-motd-version', motd.version.toString());
}
elements.rulesLink.addEventListener('click', () => {
motdWindow.show();
})
});
});
function talk() {
if (Room === null) return;
Room.talk(elements.chatInput.value);
elements.chatInput.value = "";
elements.chatInput.value = '';
}
roomInit();

View file

@ -1,17 +1,17 @@
// Testbed code
// This will go away when it isn't needed
import * as msagent from "@msagent-chat/msagent.js";
import * as msagent from '@msagent-chat/msagent.js';
let w = window as any;
w.agents = [];
let input = document.getElementById("testbed-input") as HTMLInputElement;
let input = document.getElementById('testbed-input') as HTMLInputElement;
let mount = document.getElementById("agent-mount") as HTMLDivElement;
let mount = document.getElementById('agent-mount') as HTMLDivElement;
input.addEventListener("change", async () => {
input.addEventListener('change', async () => {
let buffer = await input.files![0].arrayBuffer();
console.log("Creating agent");
console.log('Creating agent');
let agent = msagent.agentCreateCharacter(new Uint8Array(buffer));
w.agents.push(agent);
@ -19,19 +19,19 @@ input.addEventListener("change", async () => {
agent.addToDom(mount);
agent.show();
console.log("Agent created");
})
console.log('Agent created');
});
document.addEventListener("DOMContentLoaded", async () => {
document.addEventListener('DOMContentLoaded', async () => {
await msagent.agentInit();
console.log("msagent initalized!");
})
console.log('msagent initalized!');
});
let form = document.getElementById("acsUrlForm") as HTMLFormElement;
form.addEventListener('submit', e => {
let form = document.getElementById('acsUrlForm') as HTMLFormElement;
form.addEventListener('submit', (e) => {
e.preventDefault();
let url = (document.getElementById("acsUrl") as HTMLInputElement).value;
msagent.agentCreateCharacterFromUrl(url).then(agent => {
let url = (document.getElementById('acsUrl') as HTMLInputElement).value;
msagent.agentCreateCharacterFromUrl(url).then((agent) => {
w.agents.push(agent);
agent.addToDom(document.body);
agent.show();

View file

@ -1,4 +1,4 @@
import { Agent } from "@msagent-chat/msagent.js";
import { Agent } from '@msagent-chat/msagent.js';
export class User {
username: string;

View file

@ -3259,6 +3259,7 @@ __metadata:
"@parcel/packager-ts": "npm:2.12.0"
"@parcel/transformer-sass": "npm:2.12.0"
"@parcel/transformer-typescript-types": "npm:2.12.0"
prettier: "npm:^3.3.3"
typescript: "npm:>=3.0.0"
languageName: unknown
linkType: soft
@ -3694,6 +3695,15 @@ __metadata:
languageName: node
linkType: hard
"prettier@npm:^3.3.3":
version: 3.3.3
resolution: "prettier@npm:3.3.3"
bin:
prettier: bin/prettier.cjs
checksum: 10c0/b85828b08e7505716324e4245549b9205c0cacb25342a030ba8885aba2039a115dbcf75a0b7ca3b37bc9d101ee61fab8113fc69ca3359f2a226f1ecc07ad2e26
languageName: node
linkType: hard
"proc-log@npm:^4.1.0, proc-log@npm:^4.2.0":
version: 4.2.0
resolution: "proc-log@npm:4.2.0"