Add admin features (get ip, kick, ban), context menu, ban database, clean disconnect and reconnect, and errors.
This commit is contained in:
parent
78629b9cb7
commit
3525fdcff4
19 changed files with 626 additions and 25 deletions
|
@ -1,5 +1,6 @@
|
||||||
import { BufferStream, SeekDir } from './buffer.js';
|
import { BufferStream, SeekDir } from './buffer.js';
|
||||||
import { AcsData } from './character.js';
|
import { AcsData } from './character.js';
|
||||||
|
import { ContextMenu, ContextMenuItem } from './contextmenu.js';
|
||||||
import { AcsAnimation, AcsAnimationFrameInfo } from './structs/animation.js';
|
import { AcsAnimation, AcsAnimationFrameInfo } from './structs/animation.js';
|
||||||
import { AcsImageEntry } from './structs/image.js';
|
import { AcsImageEntry } from './structs/image.js';
|
||||||
import { Point, Size } from './types.js';
|
import { Point, Size } from './types.js';
|
||||||
|
@ -69,7 +70,7 @@ class AgentWordBalloonState {
|
||||||
balloonCanvas: HTMLCanvasElement;
|
balloonCanvas: HTMLCanvasElement;
|
||||||
balloonCanvasCtx: CanvasRenderingContext2D;
|
balloonCanvasCtx: CanvasRenderingContext2D;
|
||||||
|
|
||||||
constructor(char: Agent, text: string, hasTip: boolean, position: AgentWordBalloonPosition) {
|
constructor(char: Agent, text: string, hasTip: boolean, position: AgentWordBalloonPosition, textColor: string) {
|
||||||
this.char = char;
|
this.char = char;
|
||||||
this.text = text;
|
this.text = text;
|
||||||
this.hasTip = hasTip;
|
this.hasTip = hasTip;
|
||||||
|
@ -87,13 +88,13 @@ class AgentWordBalloonState {
|
||||||
// hack fix for above
|
// hack fix for above
|
||||||
this.balloonCanvas.style.pointerEvents = 'none';
|
this.balloonCanvas.style.pointerEvents = 'none';
|
||||||
|
|
||||||
let rect = wordballoonDrawText(this.balloonCanvasCtx, { x: 0, y: 0 }, this.text, 20, hasTip);
|
let rect = wordballoonDrawText(this.balloonCanvasCtx, { x: 0, y: 0 }, this.text, 20, hasTip, textColor);
|
||||||
|
|
||||||
// Second pass, actually set the element to the right width and stuffs
|
// Second pass, actually set the element to the right width and stuffs
|
||||||
this.balloonCanvas.width = rect.w;
|
this.balloonCanvas.width = rect.w;
|
||||||
this.balloonCanvas.height = rect.h;
|
this.balloonCanvas.height = rect.h;
|
||||||
|
|
||||||
wordballoonDrawText(this.balloonCanvasCtx, { x: 0, y: 0 }, this.text, 20, hasTip);
|
wordballoonDrawText(this.balloonCanvasCtx, { x: 0, y: 0 }, this.text, 20, hasTip, textColor);
|
||||||
|
|
||||||
this.char.getElement().appendChild(this.balloonCanvas);
|
this.char.getElement().appendChild(this.balloonCanvas);
|
||||||
|
|
||||||
|
@ -142,6 +143,8 @@ export class Agent {
|
||||||
private wordballoonState: AgentWordBalloonState | null = null;
|
private wordballoonState: AgentWordBalloonState | null = null;
|
||||||
private usernameBalloonState: AgentWordBalloonState | null = null;
|
private usernameBalloonState: AgentWordBalloonState | null = null;
|
||||||
|
|
||||||
|
private contextMenu: ContextMenu;
|
||||||
|
|
||||||
constructor(data: AcsData) {
|
constructor(data: AcsData) {
|
||||||
this.data = data;
|
this.data = data;
|
||||||
this.charDiv = document.createElement('div');
|
this.charDiv = document.createElement('div');
|
||||||
|
@ -155,6 +158,8 @@ export class Agent {
|
||||||
this.cnv.height = data.characterInfo.charHeight;
|
this.cnv.height = data.characterInfo.charHeight;
|
||||||
this.cnv.style.display = 'none';
|
this.cnv.style.display = 'none';
|
||||||
|
|
||||||
|
this.contextMenu = new ContextMenu(this.charDiv);
|
||||||
|
|
||||||
this.charDiv.appendChild(this.cnv);
|
this.charDiv.appendChild(this.cnv);
|
||||||
|
|
||||||
this.dragging = false;
|
this.dragging = false;
|
||||||
|
@ -173,7 +178,7 @@ export class Agent {
|
||||||
});
|
});
|
||||||
this.cnv.addEventListener('contextmenu', (e) => {
|
this.cnv.addEventListener('contextmenu', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
// TODO: Custom context menu support
|
this.contextMenu.show(e.clientX, e.clientY);
|
||||||
});
|
});
|
||||||
document.addEventListener('mousemove', (e) => {
|
document.addEventListener('mousemove', (e) => {
|
||||||
if (!this.dragging) return;
|
if (!this.dragging) return;
|
||||||
|
@ -201,6 +206,10 @@ export class Agent {
|
||||||
return this.charDiv;
|
return this.charDiv;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getContextMenu() {
|
||||||
|
return this.contextMenu;
|
||||||
|
}
|
||||||
|
|
||||||
getAt() {
|
getAt() {
|
||||||
let point: Point = {
|
let point: Point = {
|
||||||
x: this.x,
|
x: this.x,
|
||||||
|
@ -284,13 +293,13 @@ export class Agent {
|
||||||
if (index !== -1) this.playAnimation(index, finishCallback);
|
if (index !== -1) this.playAnimation(index, finishCallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
setUsername(username: string) {
|
setUsername(username: string, color: string) {
|
||||||
if (this.usernameBalloonState !== null) {
|
if (this.usernameBalloonState !== null) {
|
||||||
this.usernameBalloonState.finish();
|
this.usernameBalloonState.finish();
|
||||||
this.usernameBalloonState = null;
|
this.usernameBalloonState = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.usernameBalloonState = new AgentWordBalloonState(this, username, false, AgentWordBalloonPosition.BelowCentered);
|
this.usernameBalloonState = new AgentWordBalloonState(this, username, false, AgentWordBalloonPosition.BelowCentered, color);
|
||||||
this.usernameBalloonState.show();
|
this.usernameBalloonState.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -299,7 +308,7 @@ export class Agent {
|
||||||
this.stopSpeaking();
|
this.stopSpeaking();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.wordballoonState = new AgentWordBalloonState(this, text, true, AgentWordBalloonPosition.AboveCentered);
|
this.wordballoonState = new AgentWordBalloonState(this, text, true, AgentWordBalloonPosition.AboveCentered, "#000000");
|
||||||
this.wordballoonState.positionUpdated();
|
this.wordballoonState.positionUpdated();
|
||||||
this.wordballoonState.show();
|
this.wordballoonState.show();
|
||||||
}
|
}
|
||||||
|
|
80
msagent.js/src/contextmenu.ts
Normal file
80
msagent.js/src/contextmenu.ts
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
export class ContextMenuItem {
|
||||||
|
private element: HTMLLIElement;
|
||||||
|
|
||||||
|
name: string;
|
||||||
|
cb: Function;
|
||||||
|
|
||||||
|
constructor(name: string, cb: Function) {
|
||||||
|
this.name = name;
|
||||||
|
this.cb = cb;
|
||||||
|
this.element = document.createElement("li");
|
||||||
|
this.element.classList.add("context-menu-item");
|
||||||
|
this.element.innerText = name;
|
||||||
|
this.element.addEventListener('mousedown', () => this.cb());
|
||||||
|
}
|
||||||
|
|
||||||
|
getElement() {
|
||||||
|
return this.element;
|
||||||
|
}
|
||||||
|
|
||||||
|
setName(name: string) {
|
||||||
|
this.name = name;
|
||||||
|
this.element.innerText = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
setCb(cb: Function) {
|
||||||
|
this.cb = cb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ContextMenu {
|
||||||
|
private element: HTMLDivElement;
|
||||||
|
private list: HTMLUListElement;
|
||||||
|
|
||||||
|
private items: Array<ContextMenuItem>
|
||||||
|
|
||||||
|
constructor(parent: HTMLElement) {
|
||||||
|
this.element = document.createElement("div");
|
||||||
|
this.list = document.createElement("ul");
|
||||||
|
this.element.appendChild(this.list);
|
||||||
|
this.items = [];
|
||||||
|
this.element.classList.add("context-menu");
|
||||||
|
this.element.style.display = "none";
|
||||||
|
this.element.style.position = "fixed";
|
||||||
|
parent.appendChild(this.element);
|
||||||
|
}
|
||||||
|
|
||||||
|
show(x: number, y: number) {
|
||||||
|
this.element.style.left = x + "px";
|
||||||
|
this.element.style.top = y + "px";
|
||||||
|
document.addEventListener('mousedown', () => {
|
||||||
|
this.hide();
|
||||||
|
}, {once: true});
|
||||||
|
this.element.style.display = "block";
|
||||||
|
}
|
||||||
|
|
||||||
|
hide() {
|
||||||
|
this.element.style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
addItem(item: ContextMenuItem) {
|
||||||
|
this.items.push(item);
|
||||||
|
this.list.appendChild(item.getElement());
|
||||||
|
}
|
||||||
|
|
||||||
|
removeItem(item: ContextMenuItem) {
|
||||||
|
let i = this.items.indexOf(item);
|
||||||
|
if (i === -1) return;
|
||||||
|
this.items.splice(i, 1);
|
||||||
|
item.getElement().remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
getItem(name: string) {
|
||||||
|
return this.items.find(i => i.name === name);
|
||||||
|
}
|
||||||
|
|
||||||
|
clearItems() {
|
||||||
|
this.items.splice(0, this.items.length);
|
||||||
|
this.list.replaceChildren();
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ export * from "./character.js";
|
||||||
export * from "./decompress.js";
|
export * from "./decompress.js";
|
||||||
export * from "./sprite.js";
|
export * from "./sprite.js";
|
||||||
export * from "./wordballoon.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.
|
||||||
|
|
|
@ -129,7 +129,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): 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
|
||||||
|
@ -167,6 +167,7 @@ export function wordballoonDrawText(ctx: CanvasRenderingContext2D, at: Point, te
|
||||||
let metric = metrics[i];
|
let metric = metrics[i];
|
||||||
let height = metric.actualBoundingBoxAscent + metric.actualBoundingBoxDescent;
|
let height = metric.actualBoundingBoxAscent + metric.actualBoundingBoxDescent;
|
||||||
|
|
||||||
|
ctx.fillStyle = color;
|
||||||
ctx.fillText(lines[i], rectInner.x - 12, rectInner.y + y);
|
ctx.fillText(lines[i], rectInner.x - 12, rectInner.y + y);
|
||||||
y += height * 1.25;
|
y += height * 1.25;
|
||||||
}
|
}
|
||||||
|
|
64
protocol/src/admin.ts
Normal file
64
protocol/src/admin.ts
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
import { MSAgentProtocolMessage, MSAgentProtocolMessageType } from "./protocol";
|
||||||
|
|
||||||
|
export enum MSAgentAdminOperation {
|
||||||
|
// Client-to-server
|
||||||
|
Kick = "kick",
|
||||||
|
Ban = "ban",
|
||||||
|
// Bidirectional
|
||||||
|
Login = "login",
|
||||||
|
GetIP = "ip",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MSAgentAdminMessage extends MSAgentProtocolMessage {
|
||||||
|
op: MSAgentProtocolMessageType.Admin,
|
||||||
|
data: {
|
||||||
|
action: MSAgentAdminOperation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client-to-server
|
||||||
|
|
||||||
|
export interface MSAgentAdminLoginMessage extends MSAgentAdminMessage {
|
||||||
|
data: {
|
||||||
|
action: MSAgentAdminOperation.Login,
|
||||||
|
password: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MSAgentAdminGetIPMessage extends MSAgentAdminMessage {
|
||||||
|
data: {
|
||||||
|
action: MSAgentAdminOperation.GetIP,
|
||||||
|
username: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MSAgentAdminKickMessage extends MSAgentAdminMessage {
|
||||||
|
data: {
|
||||||
|
action: MSAgentAdminOperation.Kick,
|
||||||
|
username: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MSAgentAdminBanMessage extends MSAgentAdminMessage {
|
||||||
|
data: {
|
||||||
|
action: MSAgentAdminOperation.Ban,
|
||||||
|
username: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server-to-client
|
||||||
|
|
||||||
|
export interface MSAgentAdminLoginResponse extends MSAgentAdminMessage {
|
||||||
|
data: {
|
||||||
|
action: MSAgentAdminOperation.Login,
|
||||||
|
success: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MSAgentAdminGetIPResponse extends MSAgentAdminMessage {
|
||||||
|
data: {
|
||||||
|
action: MSAgentAdminOperation.GetIP,
|
||||||
|
username: string
|
||||||
|
ip: string
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,12 +1,17 @@
|
||||||
|
export * from './admin.js';
|
||||||
|
|
||||||
export enum MSAgentProtocolMessageType {
|
export enum MSAgentProtocolMessageType {
|
||||||
// Client-to-server
|
// Client-to-server
|
||||||
Join = "join",
|
Join = "join",
|
||||||
Talk = "talk",
|
Talk = "talk",
|
||||||
|
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",
|
||||||
|
Error = "error"
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MSAgentProtocolMessage {
|
export interface MSAgentProtocolMessage {
|
||||||
|
@ -68,3 +73,17 @@ export interface MSAgentChatMessage extends MSAgentProtocolMessage {
|
||||||
audio? : string | undefined;
|
audio? : string | undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MSAgentPromoteMessage extends MSAgentProtocolMessage {
|
||||||
|
op: MSAgentProtocolMessageType.Promote,
|
||||||
|
data: {
|
||||||
|
username: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MSAgentErrorMessage extends MSAgentProtocolMessage {
|
||||||
|
op: MSAgentProtocolMessageType.Error,
|
||||||
|
data: {
|
||||||
|
error: string;
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,10 +3,17 @@ host = "127.0.0.1"
|
||||||
port = 3000
|
port = 3000
|
||||||
proxied = true
|
proxied = true
|
||||||
|
|
||||||
|
[mysql]
|
||||||
|
host = "127.0.0.1"
|
||||||
|
username = "agentchat"
|
||||||
|
password = "hunter2"
|
||||||
|
database = "agentchat"
|
||||||
|
|
||||||
[chat]
|
[chat]
|
||||||
charlimit = 100
|
charlimit = 100
|
||||||
agentsDir = "agents/"
|
agentsDir = "agents/"
|
||||||
maxConnectionsPerIP = 2
|
maxConnectionsPerIP = 2
|
||||||
|
adminPasswordHash = "f52fbd32b2b3b86ff88ef6c490628285f482af15ddcb29541f94bcf526a3f6c7"
|
||||||
|
|
||||||
[chat.ratelimits]
|
[chat.ratelimits]
|
||||||
chat = {seconds = 10, limit = 8}
|
chat = {seconds = 10, limit = 8}
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
"@fastify/websocket": "^10.0.1",
|
"@fastify/websocket": "^10.0.1",
|
||||||
"fastify": "^4.28.1",
|
"fastify": "^4.28.1",
|
||||||
"html-entities": "^2.5.2",
|
"html-entities": "^2.5.2",
|
||||||
|
"mysql2": "^3.10.2",
|
||||||
"toml": "^3.0.0",
|
"toml": "^3.0.0",
|
||||||
"ws": "^8.17.1"
|
"ws": "^8.17.1"
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import EventEmitter from "events";
|
import EventEmitter from "events";
|
||||||
import { WebSocket } from "ws";
|
import { WebSocket } from "ws";
|
||||||
import { MSAgentJoinMessage, MSAgentProtocolMessage, MSAgentProtocolMessageType, MSAgentTalkMessage } from '@msagent-chat/protocol';
|
import { MSAgentAdminBanMessage, MSAgentAdminGetIPMessage, MSAgentAdminGetIPResponse, MSAgentAdminKickMessage, MSAgentAdminLoginMessage, MSAgentAdminLoginResponse, MSAgentAdminMessage, MSAgentAdminOperation, MSAgentErrorMessage, MSAgentJoinMessage, MSAgentProtocolMessage, MSAgentProtocolMessageType, MSAgentTalkMessage } from '@msagent-chat/protocol';
|
||||||
import { MSAgentChatRoom } from "./room.js";
|
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";
|
||||||
|
|
||||||
// Event types
|
// Event types
|
||||||
|
|
||||||
|
@ -19,6 +20,7 @@ export class Client extends EventEmitter {
|
||||||
ip: string;
|
ip: string;
|
||||||
username: string | null;
|
username: string | null;
|
||||||
agent: string | null;
|
agent: string | null;
|
||||||
|
admin: boolean;
|
||||||
|
|
||||||
room: MSAgentChatRoom;
|
room: MSAgentChatRoom;
|
||||||
socket: WebSocket;
|
socket: WebSocket;
|
||||||
|
@ -32,6 +34,7 @@ export class Client extends EventEmitter {
|
||||||
this.room = room;
|
this.room = room;
|
||||||
this.username = null;
|
this.username = null;
|
||||||
this.agent = null;
|
this.agent = null;
|
||||||
|
this.admin = false;
|
||||||
|
|
||||||
this.chatRateLimit = new RateLimiter(this.room.config.ratelimits.chat);
|
this.chatRateLimit = new RateLimiter(this.room.config.ratelimits.chat);
|
||||||
|
|
||||||
|
@ -64,7 +67,7 @@ export class Client extends EventEmitter {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseMessage(data: string) {
|
private async parseMessage(data: string) {
|
||||||
let msg: MSAgentProtocolMessage;
|
let msg: MSAgentProtocolMessage;
|
||||||
try {
|
try {
|
||||||
msg = JSON.parse(data);
|
msg = JSON.parse(data);
|
||||||
|
@ -105,6 +108,83 @@ export class Client extends EventEmitter {
|
||||||
this.emit('talk', talkMsg.data.msg);
|
this.emit('talk', talkMsg.data.msg);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case MSAgentProtocolMessageType.Admin: {
|
||||||
|
let adminMsg = msg as MSAgentAdminMessage;
|
||||||
|
if (!adminMsg.data) return;
|
||||||
|
switch (adminMsg.data.action) {
|
||||||
|
case MSAgentAdminOperation.Login: {
|
||||||
|
let loginMsg = adminMsg as MSAgentAdminLoginMessage;
|
||||||
|
if (this.admin || !loginMsg.data.password) return;
|
||||||
|
let sha256 = createHash("sha256");
|
||||||
|
sha256.update(loginMsg.data.password);
|
||||||
|
let hash = sha256.digest("hex");
|
||||||
|
sha256.destroy();
|
||||||
|
let success = false;
|
||||||
|
if (hash === this.room.config.adminPasswordHash) {
|
||||||
|
this.admin = true;
|
||||||
|
success = true;
|
||||||
|
this.emit('admin');
|
||||||
|
}
|
||||||
|
let res : MSAgentAdminLoginResponse = {
|
||||||
|
op: MSAgentProtocolMessageType.Admin,
|
||||||
|
data: {
|
||||||
|
action: MSAgentAdminOperation.Login,
|
||||||
|
success
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.send(res);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case MSAgentAdminOperation.GetIP: {
|
||||||
|
let getIPMsg = adminMsg as MSAgentAdminGetIPMessage;
|
||||||
|
if (!this.admin || !getIPMsg.data || !getIPMsg.data.username) return;
|
||||||
|
let _user = this.room.clients.find(c => c.username === getIPMsg.data.username);
|
||||||
|
if (!_user) return;
|
||||||
|
let res: MSAgentAdminGetIPResponse = {
|
||||||
|
op: MSAgentProtocolMessageType.Admin,
|
||||||
|
data: {
|
||||||
|
action: MSAgentAdminOperation.GetIP,
|
||||||
|
username: _user.username!,
|
||||||
|
ip: _user.ip
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.send(res);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case MSAgentAdminOperation.Kick: {
|
||||||
|
let kickMsg = adminMsg as MSAgentAdminKickMessage;
|
||||||
|
if (!this.admin || !kickMsg.data || !kickMsg.data.username) return;
|
||||||
|
let _user = this.room.clients.find(c => c.username === kickMsg.data.username);
|
||||||
|
if (!_user) return;
|
||||||
|
let res: MSAgentErrorMessage = {
|
||||||
|
op: MSAgentProtocolMessageType.Error,
|
||||||
|
data: {
|
||||||
|
error: "You have been kicked."
|
||||||
|
}
|
||||||
|
};
|
||||||
|
await _user.send(res);
|
||||||
|
_user.socket.close();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case MSAgentAdminOperation.Ban: {
|
||||||
|
let banMsg = adminMsg as MSAgentAdminBanMessage;
|
||||||
|
if (!this.admin || !banMsg.data || !banMsg.data.username) return;
|
||||||
|
let _user = this.room.clients.find(c => c.username === banMsg.data.username);
|
||||||
|
if (!_user) return;
|
||||||
|
let res: MSAgentErrorMessage = {
|
||||||
|
op: MSAgentProtocolMessageType.Error,
|
||||||
|
data: {
|
||||||
|
error: "You have been banned."
|
||||||
|
}
|
||||||
|
};
|
||||||
|
await this.room.db.banUser(_user.ip, _user.username!);
|
||||||
|
await _user.send(res);
|
||||||
|
_user.socket.close();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -4,6 +4,7 @@ export interface IConfig {
|
||||||
port: number;
|
port: number;
|
||||||
proxied: boolean;
|
proxied: boolean;
|
||||||
}
|
}
|
||||||
|
mysql: MySQLConfig;
|
||||||
chat: ChatConfig;
|
chat: ChatConfig;
|
||||||
tts: TTSConfig;
|
tts: TTSConfig;
|
||||||
agents: AgentConfig[];
|
agents: AgentConfig[];
|
||||||
|
@ -21,6 +22,7 @@ export interface ChatConfig {
|
||||||
charlimit: number;
|
charlimit: number;
|
||||||
agentsDir: string;
|
agentsDir: string;
|
||||||
maxConnectionsPerIP: number;
|
maxConnectionsPerIP: number;
|
||||||
|
adminPasswordHash: string;
|
||||||
ratelimits: {
|
ratelimits: {
|
||||||
chat: RateLimitConfig;
|
chat: RateLimitConfig;
|
||||||
}
|
}
|
||||||
|
@ -32,8 +34,14 @@ export interface AgentConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export interface RateLimitConfig {
|
export interface RateLimitConfig {
|
||||||
seconds: number;
|
seconds: number;
|
||||||
limit: number;
|
limit: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MySQLConfig {
|
||||||
|
host: string;
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
database: string;
|
||||||
|
}
|
38
server/src/database.ts
Normal file
38
server/src/database.ts
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import { MySQLConfig } from "./config.js";
|
||||||
|
import * as mysql from 'mysql2/promise';
|
||||||
|
|
||||||
|
export class Database {
|
||||||
|
private config: MySQLConfig;
|
||||||
|
private db: mysql.Pool;
|
||||||
|
|
||||||
|
constructor(config: MySQLConfig) {
|
||||||
|
this.config = config;
|
||||||
|
this.db = mysql.createPool({
|
||||||
|
host: this.config.host,
|
||||||
|
user: this.config.username,
|
||||||
|
password: this.config.password,
|
||||||
|
database: this.config.database,
|
||||||
|
connectionLimit: 10,
|
||||||
|
multipleStatements: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
let conn = await this.db.getConnection();
|
||||||
|
await conn.execute("CREATE TABLE IF NOT EXISTS bans (ip VARCHAR(45) NOT NULL PRIMARY KEY, username TEXT NOT NULL, time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP());");
|
||||||
|
conn.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
async banUser(ip: string, username: string) {
|
||||||
|
let conn = await this.db.getConnection();
|
||||||
|
await conn.execute("INSERT INTO bans (ip, username) VALUES (?, ?)", [ip, username]);
|
||||||
|
conn.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
async isUserBanned(ip: string): Promise<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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,6 +10,8 @@ import { TTSClient } from './tts.js';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import { isIP } from 'net';
|
import { isIP } from 'net';
|
||||||
|
import { Database } from './database.js';
|
||||||
|
import { MSAgentErrorMessage, MSAgentProtocolMessageType } from '@msagent-chat/protocol';
|
||||||
|
|
||||||
let config: IConfig;
|
let config: IConfig;
|
||||||
let configPath: string;
|
let configPath: string;
|
||||||
|
@ -31,6 +33,9 @@ try {
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let db = new Database(config.mysql);
|
||||||
|
await db.init();
|
||||||
|
|
||||||
const app = Fastify({
|
const app = Fastify({
|
||||||
logger: true,
|
logger: true,
|
||||||
});
|
});
|
||||||
|
@ -77,10 +82,10 @@ app.get("/api/agents", (req, res) => {
|
||||||
return config.agents;
|
return config.agents;
|
||||||
});
|
});
|
||||||
|
|
||||||
let room = new MSAgentChatRoom(config.chat, config.agents, tts);
|
let room = new MSAgentChatRoom(config.chat, config.agents, db, tts);
|
||||||
|
|
||||||
app.register(async app => {
|
app.register(async app => {
|
||||||
app.get("/api/socket", {websocket: true}, (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) {
|
||||||
|
@ -102,6 +107,18 @@ app.register(async app => {
|
||||||
} else {
|
} else {
|
||||||
ip = req.ip;
|
ip = req.ip;
|
||||||
}
|
}
|
||||||
|
if (await db.isUserBanned(ip)) {
|
||||||
|
let msg: MSAgentErrorMessage = {
|
||||||
|
op: MSAgentProtocolMessageType.Error,
|
||||||
|
data: {
|
||||||
|
error: "You have been banned."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
socket.send(JSON.stringify(msg), () => {
|
||||||
|
socket.close();
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
let o = room.clients.filter(c => c.ip === ip);
|
let o = room.clients.filter(c => c.ip === ip);
|
||||||
if (o.length >= config.chat.maxConnectionsPerIP) {
|
if (o.length >= config.chat.maxConnectionsPerIP) {
|
||||||
o[0].socket.close();
|
o[0].socket.close();
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import { MSAgentAddUserMessage, MSAgentChatMessage, MSAgentInitMessage, MSAgentProtocolMessage, MSAgentProtocolMessageType, MSAgentRemoveUserMessage } from "@msagent-chat/protocol";
|
import { MSAgentAddUserMessage, MSAgentChatMessage, MSAgentInitMessage, MSAgentPromoteMessage, MSAgentProtocolMessage, MSAgentProtocolMessageType, MSAgentRemoveUserMessage } from "@msagent-chat/protocol";
|
||||||
import { Client } from "./client.js";
|
import { Client } from "./client.js";
|
||||||
import { TTSClient } from "./tts.js";
|
import { TTSClient } from "./tts.js";
|
||||||
import { AgentConfig, ChatConfig } from "./config.js";
|
import { AgentConfig, ChatConfig } from "./config.js";
|
||||||
import * as htmlentities from 'html-entities';
|
import * as htmlentities from 'html-entities';
|
||||||
|
import { Database } from "./database.js";
|
||||||
|
|
||||||
export class MSAgentChatRoom {
|
export class MSAgentChatRoom {
|
||||||
agents: AgentConfig[];
|
agents: AgentConfig[];
|
||||||
|
@ -10,12 +11,14 @@ export class MSAgentChatRoom {
|
||||||
tts: TTSClient | null;
|
tts: TTSClient | null;
|
||||||
msgId : number = 0;
|
msgId : number = 0;
|
||||||
config: ChatConfig;
|
config: ChatConfig;
|
||||||
|
db: Database;
|
||||||
|
|
||||||
constructor(config: ChatConfig, agents: AgentConfig[], tts: TTSClient | null) {
|
constructor(config: ChatConfig, agents: AgentConfig[], db: Database, tts: TTSClient | 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
addClient(client: Client) {
|
addClient(client: Client) {
|
||||||
|
@ -76,6 +79,17 @@ export class MSAgentChatRoom {
|
||||||
_client.send(msg);
|
_client.send(msg);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
client.on('admin', () => {
|
||||||
|
let msg: MSAgentPromoteMessage = {
|
||||||
|
op: MSAgentProtocolMessageType.Promote,
|
||||||
|
data: {
|
||||||
|
username: client.username!
|
||||||
|
}
|
||||||
|
};
|
||||||
|
for (const _client of this.getActiveClients()) {
|
||||||
|
_client.send(msg);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private getActiveClients() {
|
private getActiveClients() {
|
||||||
|
|
|
@ -81,3 +81,32 @@ body {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.context-menu {
|
||||||
|
background-color: silver;
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu-item {
|
||||||
|
padding-right: 0.2rem;
|
||||||
|
padding-left: 0.2rem;
|
||||||
|
font-size: 11px;
|
||||||
|
|
||||||
|
padding-bottom: 0.2rem;
|
||||||
|
padding-top: 0.2rem;
|
||||||
|
|
||||||
|
margin-top: 0.3rem;
|
||||||
|
margin-bottom: 0.3rem;
|
||||||
|
font-family: "Pixelated MS Sans Serif", Arial;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu-item:hover {
|
||||||
|
background-color: navy;
|
||||||
|
color: #fff;
|
||||||
|
}
|
|
@ -29,7 +29,7 @@
|
||||||
</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="Optional"/>
|
<input type="text" id="logonRoom" placeholder="Coming Soon" disabled/>
|
||||||
</div>
|
</div>
|
||||||
<div id="logonButtonsContainer">
|
<div id="logonButtonsContainer">
|
||||||
<select id="agentSelect">
|
<select id="agentSelect">
|
||||||
|
|
|
@ -1,16 +1,26 @@
|
||||||
import { createNanoEvents, Emitter, Unsubscribe } from 'nanoevents';
|
import { createNanoEvents, Emitter, Unsubscribe } from 'nanoevents';
|
||||||
import {
|
import {
|
||||||
MSAgentAddUserMessage,
|
MSAgentAddUserMessage,
|
||||||
|
MSAgentAdminBanMessage,
|
||||||
|
MSAgentAdminGetIPMessage,
|
||||||
|
MSAgentAdminGetIPResponse,
|
||||||
|
MSAgentAdminKickMessage,
|
||||||
|
MSAgentAdminLoginMessage,
|
||||||
|
MSAgentAdminLoginResponse,
|
||||||
|
MSAgentAdminMessage,
|
||||||
|
MSAgentAdminOperation,
|
||||||
MSAgentChatMessage,
|
MSAgentChatMessage,
|
||||||
|
MSAgentErrorMessage,
|
||||||
MSAgentInitMessage,
|
MSAgentInitMessage,
|
||||||
MSAgentJoinMessage,
|
MSAgentJoinMessage,
|
||||||
|
MSAgentPromoteMessage,
|
||||||
MSAgentProtocolMessage,
|
MSAgentProtocolMessage,
|
||||||
MSAgentProtocolMessageType,
|
MSAgentProtocolMessageType,
|
||||||
MSAgentRemoveUserMessage,
|
MSAgentRemoveUserMessage,
|
||||||
MSAgentTalkMessage
|
MSAgentTalkMessage
|
||||||
} from '@msagent-chat/protocol';
|
} from '@msagent-chat/protocol';
|
||||||
import { User } from './user';
|
import { User } from './user';
|
||||||
import { agentCreateCharacterFromUrl } from '@msagent-chat/msagent.js';
|
import { agentCreateCharacterFromUrl, ContextMenuItem } from '@msagent-chat/msagent.js';
|
||||||
|
|
||||||
export interface MSAgentClientEvents {
|
export interface MSAgentClientEvents {
|
||||||
close: () => void;
|
close: () => void;
|
||||||
|
@ -32,6 +42,7 @@ export class MSAgentClient {
|
||||||
private users: User[];
|
private users: User[];
|
||||||
private playingAudio: Map<string, HTMLAudioElement> = new Map();
|
private playingAudio: Map<string, HTMLAudioElement> = new Map();
|
||||||
private charlimit: number = 0;
|
private charlimit: number = 0;
|
||||||
|
private admin: boolean;
|
||||||
|
|
||||||
private username: string | null = null;
|
private username: string | null = null;
|
||||||
private agentContainer: HTMLElement;
|
private agentContainer: HTMLElement;
|
||||||
|
@ -43,6 +54,7 @@ export class MSAgentClient {
|
||||||
this.socket = null;
|
this.socket = null;
|
||||||
this.events = createNanoEvents();
|
this.events = createNanoEvents();
|
||||||
this.users = [];
|
this.users = [];
|
||||||
|
this.admin = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
on<E extends keyof MSAgentClientEvents>(event: E, callback: MSAgentClientEvents[E]): Unsubscribe {
|
on<E extends keyof MSAgentClientEvents>(event: E, callback: MSAgentClientEvents[E]): Unsubscribe {
|
||||||
|
@ -54,6 +66,10 @@ export class MSAgentClient {
|
||||||
return (await res.json()) as APIAgentInfo[];
|
return (await res.json()) as APIAgentInfo[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getUsers() {
|
||||||
|
return this.users;
|
||||||
|
}
|
||||||
|
|
||||||
connect(): Promise<void> {
|
connect(): Promise<void> {
|
||||||
return new Promise((res) => {
|
return new Promise((res) => {
|
||||||
let url = new URL(this.url);
|
let url = new URL(this.url);
|
||||||
|
@ -79,8 +95,6 @@ export class MSAgentClient {
|
||||||
});
|
});
|
||||||
this.socket.addEventListener('close', () => {
|
this.socket.addEventListener('close', () => {
|
||||||
this.events.emit('close');
|
this.events.emit('close');
|
||||||
// TODO: Make this clean
|
|
||||||
window.location.reload();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -128,6 +142,73 @@ export class MSAgentClient {
|
||||||
return this.charlimit;
|
return this.charlimit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setContextMenu(user: User) {
|
||||||
|
let ctx = user.agent.getContextMenu();
|
||||||
|
ctx.clearItems();
|
||||||
|
// Mute
|
||||||
|
let _user = user;
|
||||||
|
let mute = new ContextMenuItem("Mute", () => {
|
||||||
|
if (_user.muted) {
|
||||||
|
mute.setName("Mute");
|
||||||
|
_user.muted = false;
|
||||||
|
} else {
|
||||||
|
mute.setName("Unmute");
|
||||||
|
_user.muted = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ctx.addItem(mute);
|
||||||
|
// Admin
|
||||||
|
if (this.admin) {
|
||||||
|
// Get IP
|
||||||
|
let getip = new ContextMenuItem("Get IP", () => {
|
||||||
|
let msg: MSAgentAdminGetIPMessage = {
|
||||||
|
op: MSAgentProtocolMessageType.Admin,
|
||||||
|
data: {
|
||||||
|
action: MSAgentAdminOperation.GetIP,
|
||||||
|
username: _user.username
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.send(msg);
|
||||||
|
});
|
||||||
|
ctx.addItem(getip);
|
||||||
|
// Kick
|
||||||
|
let kick = new ContextMenuItem("Kick", () => {
|
||||||
|
let msg: MSAgentAdminKickMessage = {
|
||||||
|
op: MSAgentProtocolMessageType.Admin,
|
||||||
|
data: {
|
||||||
|
action: MSAgentAdminOperation.Kick,
|
||||||
|
username: _user.username
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.send(msg);
|
||||||
|
});
|
||||||
|
ctx.addItem(kick);
|
||||||
|
// Ban
|
||||||
|
let ban = new ContextMenuItem("Ban", () => {
|
||||||
|
let msg: MSAgentAdminBanMessage = {
|
||||||
|
op: MSAgentProtocolMessageType.Admin,
|
||||||
|
data: {
|
||||||
|
action: MSAgentAdminOperation.Ban,
|
||||||
|
username: _user.username
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.send(msg);
|
||||||
|
});
|
||||||
|
ctx.addItem(ban);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async login(password: string) {
|
||||||
|
let msg: MSAgentAdminLoginMessage = {
|
||||||
|
op: MSAgentProtocolMessageType.Admin,
|
||||||
|
data: {
|
||||||
|
action: MSAgentAdminOperation.Login,
|
||||||
|
password
|
||||||
|
}
|
||||||
|
};
|
||||||
|
await this.send(msg);
|
||||||
|
}
|
||||||
|
|
||||||
private async handleMessage(data: string) {
|
private async handleMessage(data: string) {
|
||||||
let msg: MSAgentProtocolMessage;
|
let msg: MSAgentProtocolMessage;
|
||||||
try {
|
try {
|
||||||
|
@ -144,22 +225,32 @@ 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);
|
agent.setUsername(_user.username, "#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);
|
||||||
|
this.setContextMenu(user);
|
||||||
this.users.push(user);
|
this.users.push(user);
|
||||||
}
|
}
|
||||||
|
document.addEventListener('keydown', e => {
|
||||||
|
if (e.key === "l" && e.ctrlKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
let password = window.prompt("Papers, please");
|
||||||
|
if (!password) return;
|
||||||
|
this.login(password);
|
||||||
|
}
|
||||||
|
});
|
||||||
this.events.emit('join');
|
this.events.emit('join');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
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);
|
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);
|
||||||
|
this.setContextMenu(user);
|
||||||
this.users.push(user);
|
this.users.push(user);
|
||||||
this.events.emit('adduser', user);
|
this.events.emit('adduser', user);
|
||||||
break;
|
break;
|
||||||
|
@ -180,6 +271,8 @@ export class MSAgentClient {
|
||||||
case MSAgentProtocolMessageType.Chat: {
|
case MSAgentProtocolMessageType.Chat: {
|
||||||
let chatMsg = msg as MSAgentChatMessage;
|
let chatMsg = msg as MSAgentChatMessage;
|
||||||
let user = this.users.find((u) => u.username === chatMsg.data.username);
|
let user = this.users.find((u) => u.username === chatMsg.data.username);
|
||||||
|
if (user?.muted) return;
|
||||||
|
|
||||||
this.events.emit('chat', user, chatMsg.data.message);
|
this.events.emit('chat', user, chatMsg.data.message);
|
||||||
if (chatMsg.data.audio !== undefined) {
|
if (chatMsg.data.audio !== undefined) {
|
||||||
let audio = new Audio(this.url + chatMsg.data.audio);
|
let audio = new Audio(this.url + chatMsg.data.audio);
|
||||||
|
@ -205,6 +298,41 @@ export class MSAgentClient {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case MSAgentProtocolMessageType.Admin: {
|
||||||
|
let adminMsg = msg as MSAgentAdminMessage;
|
||||||
|
switch (adminMsg.data.action) {
|
||||||
|
case MSAgentAdminOperation.Login: {
|
||||||
|
let loginMsg = adminMsg as MSAgentAdminLoginResponse;
|
||||||
|
if (loginMsg.data.success) {
|
||||||
|
this.admin = true;
|
||||||
|
for (const user of this.users) this.setContextMenu(user);
|
||||||
|
} else {
|
||||||
|
alert("Incorrect password!");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case MSAgentAdminOperation.GetIP: {
|
||||||
|
let ipMsg = adminMsg as MSAgentAdminGetIPResponse;
|
||||||
|
alert(`${ipMsg.data.username} - ${ipMsg.data.ip}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case MSAgentProtocolMessageType.Promote: {
|
||||||
|
let promoteMsg = msg as MSAgentPromoteMessage;
|
||||||
|
let user = this.users.find(u => u.username === promoteMsg.data.username);
|
||||||
|
if (!user) return;
|
||||||
|
user.admin = true;
|
||||||
|
user.agent.setUsername(user.username, "#ff0000");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case MSAgentProtocolMessageType.Error: {
|
||||||
|
let errorMsg = msg as MSAgentErrorMessage;
|
||||||
|
// TODO: This should be shown as part of the logon window
|
||||||
|
window.alert(errorMsg.data.error);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,22 @@ const elements = {
|
||||||
chatSendBtn: document.getElementById("chatSendBtn") as HTMLButtonElement
|
chatSendBtn: document.getElementById("chatSendBtn") as HTMLButtonElement
|
||||||
}
|
}
|
||||||
|
|
||||||
let Room : MSAgentClient = new MSAgentClient(`${window.location.protocol}//${window.location.host}`, elements.chatView);
|
let Room : MSAgentClient;
|
||||||
|
|
||||||
|
function roomInit() {
|
||||||
|
Room = new MSAgentClient(`${window.location.protocol}//${window.location.host}`, elements.chatView);
|
||||||
|
Room.on('close', () => {
|
||||||
|
for (let user of Room.getUsers()) {
|
||||||
|
user.agent.remove();
|
||||||
|
}
|
||||||
|
roomInit();
|
||||||
|
loggingIn = false;
|
||||||
|
elements.logonButton.disabled = false;
|
||||||
|
logonWindow.show();
|
||||||
|
elements.logonView.style.display = "block";
|
||||||
|
elements.chatView.style.display = "none";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let logonWindow = new MSWindow(elements.logonWindow, {
|
let logonWindow = new MSWindow(elements.logonWindow, {
|
||||||
width: 500,
|
width: 500,
|
||||||
|
@ -73,3 +88,5 @@ function talk() {
|
||||||
Room.talk(elements.chatInput.value);
|
Room.talk(elements.chatInput.value);
|
||||||
elements.chatInput.value = "";
|
elements.chatInput.value = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
roomInit();
|
|
@ -2,10 +2,14 @@ import { Agent } from "@msagent-chat/msagent.js";
|
||||||
|
|
||||||
export class User {
|
export class User {
|
||||||
username: string;
|
username: string;
|
||||||
agent: Agent
|
agent: Agent;
|
||||||
|
muted: 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.admin = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
86
yarn.lock
86
yarn.lock
|
@ -233,6 +233,7 @@ __metadata:
|
||||||
"@types/ws": "npm:^8.5.10"
|
"@types/ws": "npm:^8.5.10"
|
||||||
fastify: "npm:^4.28.1"
|
fastify: "npm:^4.28.1"
|
||||||
html-entities: "npm:^2.5.2"
|
html-entities: "npm:^2.5.2"
|
||||||
|
mysql2: "npm:^3.10.2"
|
||||||
toml: "npm:^3.0.0"
|
toml: "npm:^3.0.0"
|
||||||
typescript: "npm:^5.5.3"
|
typescript: "npm:^5.5.3"
|
||||||
ws: "npm:^8.17.1"
|
ws: "npm:^8.17.1"
|
||||||
|
@ -1865,6 +1866,13 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"denque@npm:^2.1.0":
|
||||||
|
version: 2.1.0
|
||||||
|
resolution: "denque@npm:2.1.0"
|
||||||
|
checksum: 10c0/f9ef81aa0af9c6c614a727cb3bd13c5d7db2af1abf9e6352045b86e85873e629690f6222f4edd49d10e4ccf8f078bbeec0794fafaf61b659c0589d0c511ec363
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"depd@npm:2.0.0":
|
"depd@npm:2.0.0":
|
||||||
version: 2.0.0
|
version: 2.0.0
|
||||||
resolution: "depd@npm:2.0.0"
|
resolution: "depd@npm:2.0.0"
|
||||||
|
@ -2245,6 +2253,15 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"generate-function@npm:^2.3.1":
|
||||||
|
version: 2.3.1
|
||||||
|
resolution: "generate-function@npm:2.3.1"
|
||||||
|
dependencies:
|
||||||
|
is-property: "npm:^1.0.2"
|
||||||
|
checksum: 10c0/4645cf1da90375e46a6f1dc51abc9933e5eafa4cd1a44c2f7e3909a30a4e9a1a08c14cd7d5b32da039da2dba2a085e1ed4597b580c196c3245b2d35d8bc0de5d
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"get-port@npm:^4.2.0":
|
"get-port@npm:^4.2.0":
|
||||||
version: 4.2.0
|
version: 4.2.0
|
||||||
resolution: "get-port@npm:4.2.0"
|
resolution: "get-port@npm:4.2.0"
|
||||||
|
@ -2401,7 +2418,7 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"iconv-lite@npm:^0.6.2":
|
"iconv-lite@npm:^0.6.2, iconv-lite@npm:^0.6.3":
|
||||||
version: 0.6.3
|
version: 0.6.3
|
||||||
resolution: "iconv-lite@npm:0.6.3"
|
resolution: "iconv-lite@npm:0.6.3"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -2530,6 +2547,13 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"is-property@npm:^1.0.2":
|
||||||
|
version: 1.0.2
|
||||||
|
resolution: "is-property@npm:1.0.2"
|
||||||
|
checksum: 10c0/33ab65a136e4ba3f74d4f7d9d2a013f1bd207082e11cedb160698e8d5394644e873c39668d112a402175ccbc58a087cef87198ed46829dbddb479115a0257283
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"isexe@npm:^2.0.0":
|
"isexe@npm:^2.0.0":
|
||||||
version: 2.0.0
|
version: 2.0.0
|
||||||
resolution: "isexe@npm:2.0.0"
|
resolution: "isexe@npm:2.0.0"
|
||||||
|
@ -2767,6 +2791,13 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"long@npm:^5.2.1":
|
||||||
|
version: 5.2.3
|
||||||
|
resolution: "long@npm:5.2.3"
|
||||||
|
checksum: 10c0/6a0da658f5ef683b90330b1af76f06790c623e148222da9d75b60e266bbf88f803232dd21464575681638894a84091616e7f89557aa087fd14116c0f4e0e43d9
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"lru-cache@npm:^10.0.1, lru-cache@npm:^10.2.0":
|
"lru-cache@npm:^10.0.1, lru-cache@npm:^10.2.0":
|
||||||
version: 10.3.0
|
version: 10.3.0
|
||||||
resolution: "lru-cache@npm:10.3.0"
|
resolution: "lru-cache@npm:10.3.0"
|
||||||
|
@ -2774,6 +2805,20 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"lru-cache@npm:^7.14.1":
|
||||||
|
version: 7.18.3
|
||||||
|
resolution: "lru-cache@npm:7.18.3"
|
||||||
|
checksum: 10c0/b3a452b491433db885beed95041eb104c157ef7794b9c9b4d647be503be91769d11206bb573849a16b4cc0d03cbd15ffd22df7960997788b74c1d399ac7a4fed
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"lru-cache@npm:^8.0.0":
|
||||||
|
version: 8.0.5
|
||||||
|
resolution: "lru-cache@npm:8.0.5"
|
||||||
|
checksum: 10c0/cd95a9c38497611c5a6453de39a881f6eb5865851a2a01b5f14104ff3fee515362a7b1e7de28606028f423802910ba05bdb8ae1aa7b0d54eae70c92f0cec10b2
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"make-fetch-happen@npm:^13.0.0":
|
"make-fetch-happen@npm:^13.0.0":
|
||||||
version: 13.0.1
|
version: 13.0.1
|
||||||
resolution: "make-fetch-happen@npm:13.0.1"
|
resolution: "make-fetch-happen@npm:13.0.1"
|
||||||
|
@ -3003,6 +3048,31 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"mysql2@npm:^3.10.2":
|
||||||
|
version: 3.10.2
|
||||||
|
resolution: "mysql2@npm:3.10.2"
|
||||||
|
dependencies:
|
||||||
|
denque: "npm:^2.1.0"
|
||||||
|
generate-function: "npm:^2.3.1"
|
||||||
|
iconv-lite: "npm:^0.6.3"
|
||||||
|
long: "npm:^5.2.1"
|
||||||
|
lru-cache: "npm:^8.0.0"
|
||||||
|
named-placeholders: "npm:^1.1.3"
|
||||||
|
seq-queue: "npm:^0.0.5"
|
||||||
|
sqlstring: "npm:^2.3.2"
|
||||||
|
checksum: 10c0/7d7a0a703748fc7872b282893ebdda4ba34a4d888b5094e008fb7fd1d22c37afbba8b67d31faf9c43ef5a9ca3d242f866b54030aa2e419f6a3ef491268fc77ca
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"named-placeholders@npm:^1.1.3":
|
||||||
|
version: 1.1.3
|
||||||
|
resolution: "named-placeholders@npm:1.1.3"
|
||||||
|
dependencies:
|
||||||
|
lru-cache: "npm:^7.14.1"
|
||||||
|
checksum: 10c0/cd83b4bbdf358b2285e3c51260fac2039c9d0546632b8a856b3eeabd3bfb3d5b597507ab319b97c281a4a70d748f38bc66fa218a61cb44f55ad997ad5d9c9935
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"nanoevents@npm:^9.0.0":
|
"nanoevents@npm:^9.0.0":
|
||||||
version: 9.0.0
|
version: 9.0.0
|
||||||
resolution: "nanoevents@npm:9.0.0"
|
resolution: "nanoevents@npm:9.0.0"
|
||||||
|
@ -3595,6 +3665,13 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"seq-queue@npm:^0.0.5":
|
||||||
|
version: 0.0.5
|
||||||
|
resolution: "seq-queue@npm:0.0.5"
|
||||||
|
checksum: 10c0/ec870fc392f0e6e99ec0e551c3041c1a66144d1580efabae7358e572de127b0ad2f844c95a4861d2e6203f836adea4c8196345b37bed55331ead8f22d99ac84c
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"set-cookie-parser@npm:^2.4.1":
|
"set-cookie-parser@npm:^2.4.1":
|
||||||
version: 2.6.0
|
version: 2.6.0
|
||||||
resolution: "set-cookie-parser@npm:2.6.0"
|
resolution: "set-cookie-parser@npm:2.6.0"
|
||||||
|
@ -3734,6 +3811,13 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"sqlstring@npm:^2.3.2":
|
||||||
|
version: 2.3.3
|
||||||
|
resolution: "sqlstring@npm:2.3.3"
|
||||||
|
checksum: 10c0/3b5dd7badb3d6312f494cfa6c9a381ee630fbe3dbd571c4c9eb8ecdb99a7bf5a1f7a5043191d768797f6b3c04eed5958ac6a5f948b998f0a138294c6d3125fbd
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"srcset@npm:4":
|
"srcset@npm:4":
|
||||||
version: 4.0.0
|
version: 4.0.0
|
||||||
resolution: "srcset@npm:4.0.0"
|
resolution: "srcset@npm:4.0.0"
|
||||||
|
|
Loading…
Reference in a new issue