585 lines
14 KiB
TypeScript
585 lines
14 KiB
TypeScript
import { EventEmitter } from "events";
|
|
import { NotificationInput } from "../Interface/Notification";
|
|
|
|
export interface Participant {
|
|
x?: number;
|
|
y?: number;
|
|
color?: string;
|
|
name?: string;
|
|
_id?: string;
|
|
id?: string;
|
|
nameDiv?: HTMLElement;
|
|
cursorDiv?: HTMLElement;
|
|
displayX?: number;
|
|
displayY?: number;
|
|
}
|
|
|
|
interface Channel {
|
|
_id: string;
|
|
count?: number;
|
|
crown?: {
|
|
participantId: string;
|
|
time: number;
|
|
endPos: {x: number; y: number};
|
|
startPos: {x: number; y: number};
|
|
};
|
|
settings: ChannelSettings
|
|
}
|
|
|
|
export interface ChannelSettings {
|
|
chat?: boolean;
|
|
color?: string;
|
|
color2?: string;
|
|
crownsolo?: boolean;
|
|
lobby?: boolean;
|
|
visible?: boolean;
|
|
'no cussing'?: boolean;
|
|
}
|
|
|
|
interface Note {
|
|
d?: number;
|
|
s?: number;
|
|
n: string;
|
|
v?: number;
|
|
}
|
|
|
|
export interface ChatMessage {
|
|
a: string;
|
|
p: Participant;
|
|
}
|
|
|
|
export interface InMessageHi {
|
|
m: "hi";
|
|
motd?: string;
|
|
t: number;
|
|
u?: Participant;
|
|
}
|
|
export interface InMessageT {
|
|
m: "t";
|
|
t: number;
|
|
e?: number;
|
|
}
|
|
export interface InMessageCh {
|
|
m: "ch";
|
|
p: string;
|
|
ppl: {
|
|
color: string;
|
|
id: string;
|
|
_id: string;
|
|
name: string;
|
|
}[];
|
|
ch: Channel;
|
|
}
|
|
export interface InMessageP {
|
|
m: "p";
|
|
id: string;
|
|
_id: string;
|
|
name: string;
|
|
color?: string;
|
|
x?: number;
|
|
y?: number;
|
|
}
|
|
export interface InMessageM {
|
|
m: "m";
|
|
id: string;
|
|
x: number;
|
|
y: number;
|
|
}
|
|
interface InMessageBye {
|
|
m: "bye";
|
|
p: string;
|
|
}
|
|
export interface InMessageN {
|
|
m: "n";
|
|
t: number;
|
|
p: string;
|
|
n: Note[];
|
|
}
|
|
export interface InMessageNq {
|
|
m: "nq";
|
|
allowance: number;
|
|
max: number;
|
|
}
|
|
export interface InMessageLs {
|
|
m: "ls";
|
|
u: (Channel & {banned: boolean})[];
|
|
}
|
|
export interface InMessageC {
|
|
m: "c";
|
|
c: ChatMessage[];
|
|
}
|
|
export interface InMessageA extends ChatMessage {
|
|
m: "a";
|
|
}
|
|
|
|
interface InMessages {
|
|
"hi": InMessageHi;
|
|
"t": InMessageT;
|
|
"ch": InMessageCh;
|
|
"p": InMessageP;
|
|
"m": InMessageM;
|
|
"bye": InMessageBye;
|
|
"n": InMessageN;
|
|
"nq": InMessageNq;
|
|
"ls": InMessageLs;
|
|
"c": InMessageC;
|
|
"a": InMessageA;
|
|
}
|
|
|
|
interface OutMessageHi {
|
|
m: "hi";
|
|
"🐈"?: number;
|
|
}
|
|
interface OutMessageT {
|
|
m: "t";
|
|
e: number;
|
|
}
|
|
interface OutMessageCh {
|
|
m: "ch";
|
|
_id: string;
|
|
set?: ChannelSettings;
|
|
}
|
|
interface OutMessageUserset {
|
|
m: "userset";
|
|
set: {name: string};
|
|
}
|
|
interface OutMessageKickban {
|
|
m: "kickban";
|
|
_id: string;
|
|
ms: number;
|
|
}
|
|
interface OutMessageChown {
|
|
m: "chown";
|
|
id?: string;
|
|
}
|
|
interface OutMessageN {
|
|
m: "n";
|
|
t: number;
|
|
n: Note[];
|
|
}
|
|
interface OutMessageM {
|
|
m: "m";
|
|
x: number;
|
|
y: number;
|
|
}
|
|
interface OutMessageChset {
|
|
m: "chset";
|
|
set: ChannelSettings;
|
|
}
|
|
interface OutMessageMinusLs {
|
|
m: "-ls";
|
|
}
|
|
interface OutMessagePlusLs {
|
|
m: "+ls";
|
|
}
|
|
interface OutMessageA {
|
|
m: "a";
|
|
message: string;
|
|
}
|
|
interface OutMessageDevices {
|
|
m: "devices";
|
|
list: any;
|
|
}
|
|
type OutMessage =
|
|
OutMessageHi |
|
|
OutMessageT |
|
|
OutMessageCh |
|
|
OutMessageUserset |
|
|
OutMessageKickban |
|
|
OutMessageChown |
|
|
OutMessageN |
|
|
OutMessageM |
|
|
OutMessageChset |
|
|
OutMessageMinusLs |
|
|
OutMessagePlusLs |
|
|
OutMessageA |
|
|
OutMessageDevices;
|
|
|
|
export declare interface Client {
|
|
on<U extends keyof InMessages>(event: U, listener: (msg: InMessages[U]) => void): this;
|
|
emit<U extends keyof InMessages>(event: U, msg: InMessages[U]): boolean;
|
|
|
|
on(event: "status", listener: (status: string) => void): void;
|
|
emit(event: "status", status: string): void;
|
|
on(event: "disconnect", listener: (evt: CloseEvent) => void): void;
|
|
emit(event: "disconnect", evt: CloseEvent): void;
|
|
on(event: "wserror", listener: (err: Event) => void): void;
|
|
emit(event: "wserror", err: Event): void;
|
|
on(event: "connect", listener: () => void): void;
|
|
emit(event: "connect"): void;
|
|
|
|
on(event: "participant added", listener: (p: Participant) => void): void;
|
|
emit(event: "participant added", p: Participant): void;
|
|
on(event: "participant update", listener: (p: Participant) => void): void;
|
|
emit(event: "participant update", p: Participant): void;
|
|
on(event: "participant removed", listener: (p: Participant) => void): void;
|
|
emit(event: "participant removed", p: Participant): void;
|
|
|
|
on(event: "count", listener: (count: number) => void): void;
|
|
emit(event: "count", count: number): void;
|
|
on(event: "notification", listener: (input: NotificationInput) => void): void;
|
|
emit(event: "notification", input: NotificationInput): void;
|
|
}
|
|
|
|
export class Client extends EventEmitter {
|
|
ws?: WebSocket;
|
|
uri: string;
|
|
serverTimeOffset: number;
|
|
user?: Participant;
|
|
participantId?: string;
|
|
channel?: Channel;
|
|
ppl: Record<string, Participant>;
|
|
connectionTime?: number;
|
|
connectionAttempts: number;
|
|
desiredChannelId?: string;
|
|
desiredChannelSettings?: ChannelSettings;
|
|
pingInterval?: number;
|
|
canConnect: boolean;
|
|
noteBuffer: Note[];
|
|
noteBufferTime: number;
|
|
noteFlushInterval?: number;
|
|
['🐈']: number;
|
|
offlineChannelSettings: ChannelSettings = {color:"#ecfaed"};
|
|
offlineParticipant: Participant = {_id: "", name: "", color: "#777"};
|
|
|
|
constructor(uri: string) {
|
|
super();
|
|
this.uri = uri;
|
|
this.ws = undefined;
|
|
this.serverTimeOffset = 0;
|
|
this.user = undefined;
|
|
this.participantId = undefined;
|
|
this.channel = undefined;
|
|
this.ppl = {};
|
|
this.connectionTime = undefined;
|
|
this.connectionAttempts = 0;
|
|
this.desiredChannelId = undefined;
|
|
this.desiredChannelSettings = undefined;
|
|
this.pingInterval = undefined;
|
|
this.canConnect = false;
|
|
this.noteBuffer = [];
|
|
this.noteBufferTime = 0;
|
|
this.noteFlushInterval = undefined;
|
|
this['🐈'] = 0;
|
|
|
|
this.offlineParticipant = {
|
|
_id: "",
|
|
name: "",
|
|
color: "#777",
|
|
id: ""
|
|
};
|
|
|
|
|
|
this.bindEventListeners();
|
|
}
|
|
|
|
isSupported(): boolean {
|
|
return typeof WebSocket === "function";
|
|
}
|
|
|
|
isConnected(): boolean {
|
|
return this.isSupported() && this.ws !== undefined && this.ws.readyState === WebSocket.OPEN;
|
|
}
|
|
|
|
isConnecting(): boolean {
|
|
return this.isSupported() && this.ws !== undefined && this.ws.readyState === WebSocket.CONNECTING;
|
|
}
|
|
|
|
start() {
|
|
this.canConnect = true;
|
|
this.connect();
|
|
}
|
|
|
|
stop() {
|
|
this.canConnect = false;
|
|
this.ws!.close();
|
|
}
|
|
|
|
connect() {
|
|
if (!this.canConnect || !this.isSupported() || this.isConnected() || this.isConnecting())
|
|
return;
|
|
this.emit("status", "Connecting...");
|
|
this.ws = new WebSocket(this.uri);
|
|
let self = this;
|
|
this.ws.addEventListener("close", function(evt: CloseEvent) {
|
|
self.user = undefined;
|
|
self.participantId = undefined;
|
|
self.channel = undefined;
|
|
self.setParticipants([]);
|
|
clearInterval(self.pingInterval);
|
|
clearInterval(self.noteFlushInterval);
|
|
|
|
self.emit("disconnect", evt);
|
|
self.emit("status", "Offline mode");
|
|
|
|
// reconnect!
|
|
if (self.connectionTime) {
|
|
self.connectionTime = undefined;
|
|
self.connectionAttempts = 0;
|
|
} else {
|
|
++self.connectionAttempts;
|
|
}
|
|
let ms_lut = [50, 2950, 7000, 10000];
|
|
let idx = self.connectionAttempts;
|
|
if (idx >= ms_lut.length) idx = ms_lut.length - 1;
|
|
let ms = ms_lut[idx];
|
|
setTimeout(self.connect.bind(self), ms);
|
|
});
|
|
this.ws.addEventListener("error", function(err: Event) {
|
|
self.emit("wserror", err);
|
|
self.ws!.close(); // self.ws.emit("close");
|
|
});
|
|
this.ws.addEventListener("open", function(evt: Event) {
|
|
self.connectionTime = Date.now();
|
|
self.sendArray([{"m": "hi", "🐈": self['🐈']++ || undefined}]);
|
|
self.pingInterval = window.setInterval(function() {
|
|
self.sendArray([{m: "t", e: Date.now()}]);
|
|
}, 20000);
|
|
//self.sendArray([{m: "t", e: Date.now()}]);
|
|
self.noteBuffer = [];
|
|
self.noteBufferTime = 0;
|
|
self.noteFlushInterval = window.setInterval(function() {
|
|
if(self.noteBufferTime && self.noteBuffer.length > 0) {
|
|
self.sendArray([{m: "n", t: self.noteBufferTime + self.serverTimeOffset, n: self.noteBuffer}]);
|
|
self.noteBufferTime = 0;
|
|
self.noteBuffer = [];
|
|
}
|
|
}, 200);
|
|
|
|
self.emit("connect");
|
|
self.emit("status", "Joining channel...");
|
|
});
|
|
this.ws.addEventListener("message", function(evt: any) {
|
|
let transmission = JSON.parse(evt.data);
|
|
for (let i = 0; i < transmission.length; i++) {
|
|
let msg = transmission[i];
|
|
self.emit(msg.m, msg);
|
|
}
|
|
});
|
|
}
|
|
|
|
bindEventListeners() {
|
|
this.on("hi", msg => {
|
|
this.user = msg.u;
|
|
this.receiveServerTime(msg.t, undefined);
|
|
if (this.desiredChannelId) {
|
|
this.setChannel();
|
|
}
|
|
});
|
|
|
|
this.on("t", msg => {
|
|
this.receiveServerTime(msg.t, msg.e || undefined);
|
|
});
|
|
|
|
this.on("ch", msg => {
|
|
this.desiredChannelId = msg.ch._id;
|
|
this.desiredChannelSettings = msg.ch.settings;
|
|
this.channel = msg.ch;
|
|
if (msg.p) this.participantId = msg.p;
|
|
this.setParticipants(msg.ppl);
|
|
});
|
|
|
|
this.on("p", msg => {
|
|
this.participantUpdate(msg);
|
|
this.emit("participant update", this.findParticipantById(msg.id));
|
|
});
|
|
|
|
this.on("m", msg => {
|
|
if (this.ppl.hasOwnProperty(msg.id)) {
|
|
this.participantUpdate(msg);
|
|
}
|
|
});
|
|
|
|
this.on("bye", msg => {
|
|
this.removeParticipant(msg.p);
|
|
});
|
|
}
|
|
|
|
send(raw: string) {
|
|
if (this.isConnected()) this.ws!.send(raw);
|
|
}
|
|
|
|
sendArray(arr: OutMessage[]) {
|
|
this.send(JSON.stringify(arr));
|
|
}
|
|
|
|
setChannel(id?: string, set?: ChannelSettings) {
|
|
this.desiredChannelId = id || this.desiredChannelId || "lobby";
|
|
this.desiredChannelSettings = set || this.desiredChannelSettings || undefined;
|
|
this.sendArray([{m: "ch", _id: this.desiredChannelId, set: this.desiredChannelSettings}]);
|
|
}
|
|
|
|
getChannelSetting<Key extends keyof ChannelSettings>(key: Key): ChannelSettings[Key] {
|
|
if (!this.isConnected() || !this.channel || !this.channel.settings) {
|
|
return this.offlineChannelSettings[key];
|
|
}
|
|
return this.channel.settings[key];
|
|
}
|
|
|
|
setChannelSettings(settings: ChannelSettings) {
|
|
if (!this.isConnected() || !this.channel || !this.channel.settings) {
|
|
return;
|
|
}
|
|
if (this.desiredChannelSettings) {
|
|
for (let key in settings) {
|
|
(this.desiredChannelSettings as any)[key] = (settings as any)[key];
|
|
}
|
|
this.sendArray([{m: "chset", set: this.desiredChannelSettings}]);
|
|
}
|
|
}
|
|
|
|
getOwnParticipant() {
|
|
return this.findParticipantById(this.participantId!);
|
|
}
|
|
|
|
setParticipants(ppl: Participant[]) {
|
|
for (let id in this.ppl) {
|
|
if (!this.ppl.hasOwnProperty(id)) continue;
|
|
let found = false;
|
|
for (let j = 0; j < ppl.length; j++) {
|
|
if (ppl[j].id === id) {
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!found) {
|
|
this.removeParticipant(id);
|
|
}
|
|
}
|
|
// update all
|
|
for (let i = 0; i < ppl.length; i++) {
|
|
this.participantUpdate(ppl[i]);
|
|
}
|
|
}
|
|
|
|
countParticipants(): number {
|
|
let count = 0;
|
|
for (let i in this.ppl) {
|
|
if (this.ppl.hasOwnProperty(i)) ++count;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
participantUpdate(update: Participant) {
|
|
let part = this.ppl[update.id!] || null;
|
|
if (part === null) {
|
|
part = update;
|
|
this.ppl[part.id!] = part;
|
|
this.emit("participant added", part);
|
|
this.emit("count", this.countParticipants());
|
|
} else {
|
|
if (update.x) part.x = update.x;
|
|
if (update.y) part.y = update.y;
|
|
if (update.color) part.color = update.color;
|
|
if (update.name) part.name = update.name;
|
|
}
|
|
}
|
|
|
|
removeParticipant(id: string) {
|
|
if (this.ppl.hasOwnProperty(id)) {
|
|
let part = this.ppl[id];
|
|
delete this.ppl[id];
|
|
this.emit("participant removed", part);
|
|
this.emit("count", this.countParticipants());
|
|
}
|
|
}
|
|
|
|
findParticipantById(id: string): Participant {
|
|
return this.ppl[id] || this.offlineParticipant;
|
|
}
|
|
|
|
isOwner() {
|
|
return this.channel && this.channel.crown && this.channel.crown.participantId === this.participantId;
|
|
}
|
|
|
|
preventsPlaying(): boolean {
|
|
return this.isConnected() && !this.isOwner() && this.getChannelSetting("crownsolo") === true;
|
|
}
|
|
|
|
receiveServerTime(time: number, echo: number | undefined) {
|
|
let now = Date.now();
|
|
let target = time - now;
|
|
//console.log("Target serverTimeOffset: " + target);
|
|
let duration = 1000;
|
|
let step = 0;
|
|
let steps = 50;
|
|
let step_ms = duration / steps;
|
|
let difference = target - this.serverTimeOffset;
|
|
let inc = difference / steps;
|
|
let iv = setInterval(() => {
|
|
this.serverTimeOffset += inc;
|
|
if (++step >= steps) {
|
|
clearInterval(iv);
|
|
//console.log("serverTimeOffset reached: " + self.serverTimeOffset);
|
|
this.serverTimeOffset = target;
|
|
}
|
|
}, step_ms);
|
|
// smoothen
|
|
|
|
//this.serverTimeOffset = time - now; // mostly time zone offset ... also the lags so todo smoothen this
|
|
// not smooth:
|
|
//if(echo) this.serverTimeOffset += echo - now; // mostly round trip time offset
|
|
}
|
|
|
|
startNote(note: string, vel?: number) {
|
|
if (this.isConnected()) {
|
|
vel = typeof vel === "undefined" ? undefined : +vel.toFixed(3);
|
|
if (!this.noteBufferTime) {
|
|
this.noteBufferTime = Date.now();
|
|
this.noteBuffer.push({n: note, v: vel});
|
|
} else {
|
|
this.noteBuffer.push({d: Date.now() - this.noteBufferTime, n: note, v: vel});
|
|
}
|
|
}
|
|
};
|
|
|
|
stopNote(note: string) {
|
|
if (this.isConnected()) {
|
|
if (!this.noteBufferTime) {
|
|
this.noteBufferTime = Date.now();
|
|
this.noteBuffer.push({n: note, s: 1});
|
|
} else {
|
|
this.noteBuffer.push({d: Date.now() - this.noteBufferTime, n: note, s: 1});
|
|
}
|
|
}
|
|
};
|
|
|
|
// Where do these methods come from???
|
|
// keeping extra methods for now, might change later.
|
|
|
|
/// START EXTRA METHODS BY HRI ///
|
|
setName(str: string) {
|
|
if (str.length > 40) return;
|
|
this.sendArray([{m:'userset', set:{name:str}}]);
|
|
}
|
|
|
|
kickban(_id: string, ms: number) {
|
|
if (ms > 60*60*1000) ms = 60*60*1000;
|
|
if (ms < 0) ms = 0;
|
|
this.sendArray([{m:'kickban', _id: _id, ms: ms}]);
|
|
}
|
|
|
|
chown(id: string) {
|
|
if (!this.isOwner()) return;
|
|
this.sendArray([{m:'chown', id: id}]);
|
|
}
|
|
|
|
pickupCrown() {
|
|
this.sendArray([{m:'chown', id: this.getOwnParticipant().id!}]);
|
|
}
|
|
|
|
getParticipant(str: string) {
|
|
let ret;
|
|
for (let id in this.ppl) {
|
|
let part = this.ppl[id];
|
|
if (part.name!.toLowerCase().includes(str.toLowerCase()) || part._id!.toLowerCase().includes(str.toLowerCase()) || part.id!.toLowerCase().includes(str.toLowerCase())) {
|
|
ret = part;
|
|
}
|
|
}
|
|
if (typeof ret !== "undefined") {
|
|
return ret;
|
|
}
|
|
}
|
|
/// END EXTRA METHODS BY HRI ///
|
|
}
|