Skip to content

Instantly share code, notes, and snippets.

@fucksophie
Created January 17, 2025 20:23
Show Gist options
  • Save fucksophie/5cf609f7b1231c68274a93a35ad05f16 to your computer and use it in GitHub Desktop.
Save fucksophie/5cf609f7b1231c68274a93a35ad05f16 to your computer and use it in GitHub Desktop.
this is a typescript client for multiplayerpiano. it has fully compatiablity with every single message that MPP sends out. from all of the "custom" related stuff, to a notebuffer system. EVERYTHING.
import chalk from "chalk";
import EventEmitter from "wolfy87-eventemitter";
import type {
ChannelSettings,
ChatMessage,
ChChannelSettings,
Crown,
LSMessage,
Messages,
MessageTypes,
ParticipantInfo,
ServerMessage,
Targets,
} from "./types";
import type TypedEmitter from "typed-emitter";
export const numbertomidi: { [key: number]: string } = {
1: "a-1",
2: "as-1",
3: "b-1",
4: "c0",
5: "cs0",
6: "d0",
7: "ds0",
8: "e0",
9: "f0",
10: "fs0",
11: "g0",
12: "gs0",
13: "a0",
14: "as0",
15: "b0",
16: "c1",
17: "cs1",
18: "d1",
19: "ds1",
20: "e1",
21: "f1",
22: "fs1",
23: "g1",
24: "gs1",
25: "a1",
26: "as1",
27: "b1",
28: "c2",
29: "cs2",
30: "d2",
31: "ds2",
32: "e2",
33: "f2",
34: "fs2",
35: "g2",
36: "gs2",
37: "a2",
38: "as2",
39: "b2",
40: "c3",
41: "cs3",
42: "d3",
43: "ds3",
44: "e3",
45: "f3",
46: "fs3",
47: "g3",
48: "gs3",
49: "a3",
50: "as3",
51: "b3",
52: "c4",
53: "cs4",
54: "d4",
55: "ds4",
56: "e4",
57: "f4",
58: "fs4",
59: "g4",
60: "gs4",
61: "a4",
62: "as4",
63: "b4",
64: "c5",
65: "cs5",
66: "d5",
67: "ds5",
68: "e5",
69: "f5",
70: "fs5",
71: "g5",
72: "gs5",
73: "a5",
74: "as5",
75: "b5",
76: "c6",
77: "cs6",
78: "d6",
79: "ds6",
80: "e6",
81: "f6",
82: "fs6",
83: "g6",
84: "gs6",
85: "a6",
86: "as6",
87: "b6",
88: "c7",
};
export const miditonumber: { [key: string]: number } = {
"a-1": 1,
"as-1": 2,
"b-1": 3,
c0: 4,
cs0: 5,
d0: 6,
ds0: 7,
e0: 8,
f0: 9,
fs0: 10,
g0: 11,
gs0: 12,
a0: 13,
as0: 14,
b0: 15,
c1: 16,
cs1: 17,
d1: 18,
ds1: 19,
e1: 20,
f1: 21,
fs1: 22,
g1: 23,
gs1: 24,
a1: 25,
as1: 26,
b1: 27,
c2: 28,
cs2: 29,
d2: 30,
ds2: 31,
e2: 32,
f2: 33,
fs2: 34,
g2: 35,
gs2: 36,
a2: 37,
as2: 38,
b2: 39,
c3: 40,
cs3: 41,
d3: 42,
ds3: 43,
e3: 44,
f3: 45,
fs3: 46,
g3: 47,
gs3: 48,
a3: 49,
as3: 50,
b3: 51,
c4: 52,
cs4: 53,
d4: 54,
ds4: 55,
e4: 56,
f4: 57,
fs4: 58,
g4: 59,
gs4: 60,
a4: 61,
as4: 62,
b4: 63,
c5: 64,
cs5: 65,
d5: 66,
ds5: 67,
e5: 68,
f5: 69,
fs5: 70,
g5: 71,
gs5: 72,
a5: 73,
as5: 74,
b5: 75,
c6: 76,
cs6: 77,
d6: 78,
ds6: 79,
e6: 80,
f6: 81,
fs6: 82,
g6: 83,
gs6: 84,
a6: 85,
as6: 86,
b6: 87,
c7: 88,
};
class Room {
players: Map<string, ParticipantInfo>;
settings: ChannelSettings;
_id: string;
crown?: Crown;
chatHistory: ChatMessage[];
constructor(
playerArray: ParticipantInfo[],
settings: ChannelSettings,
_id: string,
crown: Crown
) {
this.players = new Map(
playerArray.map((ParticipantInfo) => [
ParticipantInfo._id,
ParticipantInfo,
])
);
this.settings = settings;
this._id = _id;
this.crown = crown;
this.chatHistory = [];
}
addToChatHistory(msgOrDm: ChatMessage) {
this.chatHistory = this.chatHistory.slice(0, 30);
this.chatHistory.push(msgOrDm);
}
addPlayer(ParticipantInfo: ParticipantInfo) {
this.players.set(ParticipantInfo._id, ParticipantInfo);
}
removeId(_id: string) {
this.players.delete(_id);
}
}
const clamp = (num: number, min: number, max: number): number =>
Math.min(Math.max(num, min), max);
type MessageEvents = {
[key in Messages["m"]]: (msg: Extract<Messages, { m: key }>) => void;
} & {
entered_room: (id: string) => void;
connected: () => void;
disconnect: () => void;
authenticated: () => void;
join: (player: ParticipantInfo) => void;
message: (msg: ChatMessage) => void;
ls: (msg: LSMessage) => void;
leave: (id: string) => void;
motd: (motd: string) => void;
};
export class Client extends (EventEmitter as unknown as new () => TypedEmitter<MessageEvents>) {
private token: string;
private ws?: WebSocket;
private tInterval?: Timer;
private noteInterval?: Timer;
private noteBuffer: { n: string; s?: 1; v?: number; addedTime: number }[] =
[];
private noteBufferFlushTimeout = 5;
private noteFillingBegun?: Date;
user: {
id?: string;
_id: string
name: string
color: string
afk?: boolean
tag?: {
text: string
color: string
}
vanished?: boolean
permissions?: {
playNotesAnywhere?: boolean
clearChat?: boolean
vanish?: boolean
chsetAnywhere?: boolean
chownAnywhere?: boolean
siteBan?: boolean
siteBanAnyReason?: boolean
siteBanAnyDuration?: boolean
usersetOthers?: boolean
}
accountInfo?: {
type?: string
username?: string
discriminator?: string
avatar?: string
}
};
ping: number;
url: string;
readyToAcceptMessages: boolean;
room?: Room;
constructor(url: string, token: string, proxy?: string) {
super();
this.user = {} as any;
this.ping = 0;
this.url = url;
this.readyToAcceptMessages = false;
this.token = token;
}
subscribeCustom() {
this.sendArray({
m: "+custom"
})
}
unsubscribeCustom() {
this.sendArray({
m: "-custom"
})
}
custom(data: any, target: Targets) {
this.sendArray({
m: "custom",
data,
target
})
}
move(x: string, y: string) {
this.sendArray({ m: "m", x, y });
}
setRoom(_id: string, set: ChChannelSettings = {}): Promise<void> {
return new Promise((resolve) => {
this.sendArray({ m: "ch", _id, set });
const listener = (_id: string) => {
if (_id === _id) {
resolve();
this.removeListener("entered_room", listener);
}
};
this.addListener("entered_room", listener);
});
}
chset(settings: Partial<ChannelSettings> = {}) {
this.sendArray({
m: "chset",
set: {
...this.room?.settings,
...settings,
},
});
}
chown(id: string) {
this.sendArray({
m: "chown",
id
});
}
userSet(name: string, color: string) {
this.sendArray({
m: "userset",
set: { name, color },
});
}
//#region these are MPP.net/MPPclone exclusives
setname(name: string, _id: string) {
this.sendArray({
m: "setname",
name,
_id
});
}
setcolor(color: string, _id: string) {
this.sendArray({
m: "setcolor",
color,
_id
});
}
//#endregion
connect() {
this.ws = new WebSocket(this.url);
this.bindEvents();
}
private sendArray(data: any) {
if (this.readyToAcceptMessages && this.ws) {
this.ws.send(JSON.stringify([data]));
}
}
reply(message: string, id: string) {
this.sendArray({
m: "a",
message,
reply_to: id,
});
}
message(message: string) {
this.sendArray({
m: "a",
message,
});
}
dm(_id: string, message: string) {
this.sendArray({
_id,
message,
m: "dm",
});
}
vanish(state: boolean) {
this.sendArray({
vanish: state,
m: "v",
});
}
roomList(): Promise<any> {
return new Promise((resolve) => {
this.sendArray({ m: "+ls" });
const listener = (data: LSMessage) => {
const { u, c } = data;
if (!c) return;
resolve(u);
this.removeListener("ls", listener as (msg: ServerMessage) => void);
this.sendArray({ m: "-ls" });
};
this.removeListener("ls", listener as (msg: ServerMessage) => void);
});
}
subscribeToLs() {
this.sendArray({ m: "+ls" });
}
unsubscribeFromLs() {
this.sendArray({ m: "-ls" });
}
kickban(id: string, time: number) {
const max = 1.8e7;
this.sendArray({
m: "kickban",
_id: id,
ms: time === -1 ? max : clamp(time, 1, max),
});
}
siteban(id: string, reason: string, note?: string, time?: number) {
let msg: Record<string,any> = {
m: "siteban",
id,
_id: id,
permanent: time == null,
duration: time,
reason: reason || "Evading site-wide punishments"
};
if(note) {
msg.note = note;
}
this.sendArray(msg);
}
unban(id: string) {
this.sendArray({
m: "unban",
_id: id,
});
}
clearChat() {
this.sendArray({
m: "clearchat"
})
}
private bindEvents() {
if (!this.ws) return;
this.ws.addEventListener("open", () => {
this.readyToAcceptMessages = true;
this.tInterval = setInterval(() => {
this.sendArray({
m: "t",
e: Date.now(),
});
}, 15000);
this.noteInterval = setInterval(() => {
if (this.noteBuffer.length != 0 && this.noteFillingBegun) {
this.sendArray({
m: "n",
t: this.noteFillingBegun.getTime(),
n: this.noteBuffer.map((z) => {
return {
n: z.n,
s: z.s,
v: z.v,
d: z.addedTime - this.noteFillingBegun!.getTime(),
};
}),
});
this.noteBuffer = [];
this.noteFillingBegun = undefined;
}
}, this.noteBufferFlushTimeout);
this.sendArray({
m: "hi",
token: this.token,
});
this.emit("connected");
console.log(chalk.green(`Connected to WS (${this.url})!`));
});
this.ws.addEventListener("message", (e) => {
let json;
try {
json = JSON.parse(e.data);
} catch {
return;
}
if (!Array.isArray(json)) return;
json.forEach((msg: Messages) => {
this.handleMessage(msg);
this.emit(msg.m, msg as any);
});
});
this.ws.addEventListener("close", () => {
if (!this.readyToAcceptMessages) return;
this.readyToAcceptMessages = false;
this.emit("disconnect");
if (this.tInterval) clearInterval(this.tInterval);
if (this.noteInterval) clearInterval(this.noteInterval);
setTimeout(() => {
this.connect();
}, 10000 + Math.floor(Math.random() * 5000));
});
this.ws.addEventListener("error", () => {});
}
releaseNote(note: string) {
if (!this.noteFillingBegun) {
this.noteFillingBegun = new Date();
}
this.noteBuffer.push({ n: note, s: 1, addedTime: Date.now() });
}
pressNote(note: string, velocity: number) {
if (!this.noteFillingBegun) {
this.noteFillingBegun = new Date();
}
this.noteBuffer.push({ n: note, v: velocity, addedTime: Date.now() });
}
private handleMessage(msg: Messages) {
switch (msg.m) {
case "hi":
this.emit("authenticated");
this.user = {
...msg.u,
permissions: msg.permissions,
accountInfo: msg.accountInfo
}
console.log(
chalk.greenBright(
`Authenticated to ${this.url} as ${chalk.hex(this.user.color)(
this.user.name
)}`
)
);
this.emit("motd", msg.motd);
break;
case "t":
this.ping = Date.now() - msg.t;
break;
case "ch":
if (this.room?._id !== msg.ch._id) {
console.log(chalk.blue(`Joined channel ${msg.ch._id}!`));
}
this.room = new Room(
msg.ppl,
msg.ch.settings,
msg.ch._id,
msg.ch.crown!
);
this.user.id = msg.p;
this.emit("entered_room", msg.ch._id);
[...this.room.players.values()].forEach((player) => {
this.emit("join", player);
});
break;
case "c":
if (this.room) this.room.chatHistory = msg.c;
break;
case "a":
if (this.room) this.room.addToChatHistory(msg);
this.emit("message", msg);
break;
case "dm":
if (this.room) this.room.addToChatHistory(msg);
break;
case "ls":
this.emit("ls", { m: msg.m, u: msg.u, c: msg.c });
break;
case "p":
if (this.room && !this.room.players.has(msg._id)) {
this.emit("join", msg);
}
if (this.room) this.room.addPlayer(msg);
if (msg._id === this.user._id) {
let a = {
...msg,
permissions: this.user.permissions,
accountInfo: this.user.accountInfo,
}
this.user = a;
}
break;
case "bye":
if (this.room) {
this.emit("leave", msg.p);
this.room.removeId(msg.p);
}
break;
}
}
//#region MPP client compatiablity
isOwner() {
return this.room?.crown?.userId === this.user._id;
}
preventsPlaying() {
return this.ws?.readyState !== this.ws?.OPEN
}
getOwnParticipant() {
return this.user;
}
get channel() {
return this.room;
}
//#endregion
}
export interface ParticipantInfo {
id: string;
_id: string;
name: string;
color: string;
x: string | number;
y: string | number;
afk: boolean;
tag?: Tag;
vanished?: boolean;
}
export interface ChannelInfo {
banned?: boolean;
count: number;
id: string;
_id: string;
crown?: Crown;
settings: ChannelSettings;
ppl?: ParticipantInfo[];
}
export interface ChannelSettings {
visible: boolean;
color: string;
color2?: string;
chat: boolean;
crownsolo: boolean;
noindex?: boolean;
"no cussing"?: boolean;
limit: number;
minOnlineTime?: number;
lobby?: boolean;
}
export interface ChChannelSettings {
visible?: boolean;
color?: string;
color2?: string;
chat?: boolean;
crownsolo?: boolean;
noindex?: boolean;
"no cussing"?: boolean;
limit?: number;
minOnlineTime?: number;
lobby?: boolean;
}
export interface Crown {
userId: string;
participantId?: string;
time: number;
startPos: { x: number; y: number };
endPos: { x: number; y: number };
}
export interface Tag {
text: string;
color: string;
}
export interface Note {
n: string;
d?: number;
v?: number;
s?: 1;
}
export interface Login {
type: "discord";
code: string;
}
export interface AccountInfo {
type: "discord";
username: string;
discriminator: string;
avatar: string;
}
export interface ServerMessage {
m: MessageTypes;
}
export interface AMessage extends ServerMessage {
m: "a";
id: string;
t: number;
a: string;
p: ParticipantInfo;
r?: string;
}
export interface BMessage extends ServerMessage {
m: "b";
code: string;
}
export interface ByeMessage extends ServerMessage {
m: "bye";
p: string;
}
export interface CMessage extends ServerMessage {
m: "c";
c: Array<ChatMessage>;
}
export interface ChMessage extends ServerMessage {
m: "ch";
p: string;
ppl: ParticipantInfo[];
ch: ChannelInfo;
}
export interface CustomMessage extends ServerMessage {
m: "custom";
data: any;
p: string;
}
export interface DMMessage extends ServerMessage {
m: "dm";
id: string;
t: number;
a: string;
sender: ParticipantInfo;
recipient: ParticipantInfo;
r?: string;
}
export interface HiMessage extends ServerMessage {
m: "hi";
t: number;
u: Omit<ParticipantInfo, "id">;
permissions?: {
playNotesAnywhere?: boolean
clearChat?: boolean
vanish?: boolean
chsetAnywhere?: boolean
chownAnywhere?: boolean
siteBan?: boolean
siteBanAnyReason?: boolean
siteBanAnyDuration?: boolean
usersetOthers?: boolean
};
token?: string;
motd: string;
accountInfo: AccountInfo;
}
export interface LSMessage extends ServerMessage {
m: "ls";
c: boolean;
u: ChannelInfo[];
}
export interface MMessage extends ServerMessage {
m: "m";
id: string;
x: string;
y: string;
}
export interface NMessage extends ServerMessage {
m: "n";
t: number;
p: string;
n: Note[];
}
export interface NotificationMessage extends ServerMessage {
m: "notification";
duration?: number;
class?: string;
id?: string;
title?: string;
text?: string;
html?: string;
target?: string;
}
export interface NQMessage extends ServerMessage {
m: "nq";
allowance: number;
max: number;
maxHistLen: number;
}
export interface PMessage extends ServerMessage, ParticipantInfo {
m: "p";
}
export interface TMessage extends ServerMessage {
m: "t";
t: number;
e?: number;
}
export type ChatMessage = AMessage | DMMessage;
export type MessageTypes =
| "a"
| "b"
| "bye"
| "c"
| "ch"
| "custom"
| "dm"
| "hi"
| "ls"
| "m"
| "n"
| "notification"
| "nq"
| "p"
| "t";
export type Messages =
| AMessage
| BMessage
| ByeMessage
| CMessage
| ChMessage
| CustomMessage
| DMMessage
| HiMessage
| LSMessage
| MMessage
| NMessage
| NotificationMessage
| NQMessage
| PMessage
| TMessage;
export enum TargetType {
SUBSCRIBED = "subscribed",
ID = "id",
IDS = "ids"
}
export interface Target {
mode: TargetType
global: boolean
}
export interface IDTarget extends Target {
mode: TargetType.ID
id: string
}
export interface IDSTarget extends Target {
mode: TargetType.IDS
ids: string[]
}
export interface SubscribedTarget extends Target {
mode: TargetType.SUBSCRIBED
}
export type Targets = IDSTarget | IDTarget | SubscribedTarget;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment