mpp-client-net 1.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,33 @@
1
+ # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
2
+ # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages
3
+
4
+ name: Node.js Package
5
+
6
+ on:
7
+ release:
8
+ types: [created]
9
+
10
+ jobs:
11
+ build:
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v3
15
+ - uses: actions/setup-node@v3
16
+ with:
17
+ node-version: 16
18
+ - run: npm ci
19
+ - run: npm test
20
+
21
+ publish-npm:
22
+ needs: build
23
+ runs-on: ubuntu-latest
24
+ steps:
25
+ - uses: actions/checkout@v3
26
+ - uses: actions/setup-node@v3
27
+ with:
28
+ node-version: 16
29
+ registry-url: https://registry.npmjs.org/
30
+ - run: npm ci
31
+ - run: npm publish
32
+ env:
33
+ NODE_AUTH_TOKEN: ${{secrets.npm_token}}
package/.prettierrc ADDED
@@ -0,0 +1,5 @@
1
+ {
2
+ "arrowParens": "avoid",
3
+ "tabWidth": 4,
4
+ "trailingComma": "none"
5
+ }
package/Client.d.ts ADDED
@@ -0,0 +1,242 @@
1
+ import type { EventEmitter } from "events";
2
+
3
+ declare interface Tag {
4
+ text: string;
5
+ color: string;
6
+ }
7
+
8
+ declare interface User {
9
+ _id: string; // user id
10
+ name: string;
11
+ color: string;
12
+ tag?: Tag;
13
+ }
14
+
15
+ declare interface Participant extends User {
16
+ id: string; // participant id (same as user id on mppclone)
17
+ }
18
+
19
+ declare type ChannelSettings = {
20
+ color: string;
21
+ crownsolo: boolean;
22
+ chat: boolean;
23
+ visible: boolean;
24
+ limit: number;
25
+ } & Partial<{
26
+ color2: string;
27
+ lobby: boolean;
28
+ owner_id: string;
29
+ "lyrical notes": boolean;
30
+ "no cussing": boolean;
31
+ noindex: boolean;
32
+ }>;
33
+
34
+ declare type ChannelSettingValue = Partial<string | number | boolean>;
35
+
36
+ declare type NoteLetter = `a` | `b` | `c` | `d` | `e` | `f` | `g`;
37
+ declare type NoteOctave = -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;
38
+
39
+ declare interface Note {
40
+ n: `${NoteLetter}${NoteOctave}`;
41
+ d: number;
42
+ v: number;
43
+ s?: 1;
44
+ }
45
+
46
+ declare type Notification = Partial<{
47
+ duration: number;
48
+ class: string;
49
+ id: string;
50
+ title: string;
51
+ text: string;
52
+ html: string;
53
+ target: string;
54
+ }>;
55
+
56
+ declare type CustomTarget = {
57
+ global?: boolean;
58
+ } & (
59
+ | {
60
+ mode: "subscribed";
61
+ }
62
+ | {
63
+ mode: "ids";
64
+ ids: string[];
65
+ }
66
+ | {
67
+ mode: "id";
68
+ id: string;
69
+ }
70
+ );
71
+
72
+ declare interface Crown {
73
+ userId: string;
74
+ partcipantId?: string;
75
+ time: number;
76
+ startPos: {
77
+ x: number;
78
+ y: number;
79
+ };
80
+ endPos: {
81
+ x: number;
82
+ y: number;
83
+ };
84
+ }
85
+
86
+ declare interface ChannelInfo {
87
+ banned?: boolean;
88
+ count: number;
89
+ id: string;
90
+ _id: string;
91
+ crown?: Crown;
92
+ settings: ChannelSettings;
93
+ }
94
+
95
+ declare class Client extends EventEmitter {
96
+ public uri: string;
97
+ public ws: WebSocket | undefined;
98
+ public serverTimeOffset: number;
99
+ public user: User | undefined;
100
+ public participantId: string | undefined;
101
+ public ppl: Record<string, Participant>;
102
+ public connectionTime: number;
103
+ public connectionAttempts: number;
104
+ public desiredChannelId: string | undefined;
105
+ public desiredChannelSettings: ChannelSettings | undefined;
106
+ public pingInterval: number | undefined;
107
+ public canConnect: boolean;
108
+ public noteBuffer: OutgoingMPPEvents["n"][];
109
+ public noteBufferTime: number;
110
+ public noteFlushInterval: number | undefined;
111
+ public permissions: Record<any, unknown>;
112
+ public ["🐈"]: number;
113
+ public loginInfo: unknown | undefined;
114
+ public token: string;
115
+
116
+ constructor(uri: string, token: string);
117
+
118
+ public isSupported(): boolean;
119
+ public isConnected(): boolean;
120
+ public isConnecting(): boolean;
121
+
122
+ public start(): void;
123
+ public stop(): void;
124
+ protected connect(): void;
125
+
126
+ protected bindEventListeners(): void;
127
+
128
+ public send(raw: string): void;
129
+ public sendArray<Event extends keyof OutgoingMPPEvents>(
130
+ arr: OutgoingMPPEvents[Event][]
131
+ ): void;
132
+ public setChannel(id: string, set?: Partial<ChannelSettings>): void;
133
+
134
+ public offlineChannelSettings: ChannelSettings;
135
+
136
+ public getChannelSetting(key: string): ChannelSettings[];
137
+ public setChannelSettings(settings: ChannelSettings): void;
138
+
139
+ public offlineParticipant: Participant;
140
+
141
+ public getOwnParticipant(): Participant;
142
+ public setParticipants(ppl: Participant[]): void;
143
+ public countParticipants(): number;
144
+ public participantUpdate(update: Participant): void;
145
+ public participantMoveMouse(update: Participant): void;
146
+ public removeParticipant(id: string): void;
147
+ public findParticipantById(id: string): void;
148
+
149
+ public isOwner(): boolean;
150
+ public preventsPlaying(): boolean;
151
+ public receiveServerTime(time: number, echo: number): void;
152
+ public startNote(note: string, vel: number): void;
153
+ public stopNote(note: string): void;
154
+ public sendPing(): void;
155
+ public setLoginInfo(loginInfo: any): void;
156
+
157
+ public on<Event extends keyof IncomingMPPEvents>(
158
+ event: Event,
159
+ listener: (msg: IncomingMPPEvents[Event]) => void
160
+ ): this;
161
+
162
+ public emit<Event extends keyof IncomingMPPEvents>(
163
+ event: Event,
164
+ ...args: Parameters<(msg: IncomingMPPEvents[Event]) => void>
165
+ ): boolean;
166
+ }
167
+
168
+ export default Client;
169
+
170
+ declare interface IncomingMPPEvents {
171
+ a: { m: "a"; a: string; p: Participant; t: number };
172
+ b: { m: "b"; code: string };
173
+ c: { m: "c"; c: IncomingMPPEvents["a"][] };
174
+ ch: { m: "ch"; p: string; ch: ChannelInfo };
175
+ custom: { m: "custom"; data: any; p: string };
176
+ hi: {
177
+ m: "hi";
178
+ t: number;
179
+ u: User;
180
+ permissions: any;
181
+ token?: any;
182
+ accountInfo: any;
183
+ };
184
+ ls: { m: "ls"; c: boolean; u: ChannelInfo[] };
185
+ m: { m: "m"; x: number; y: number; id: string };
186
+ n: { m: "n"; t: number; n: Note[]; p: string };
187
+ notification: {
188
+ duration?: number;
189
+ class?: string;
190
+ id?: string;
191
+ title?: string;
192
+ text?: string;
193
+ html?: string;
194
+ target?: string;
195
+ };
196
+ nq: {
197
+ m: "nq";
198
+ allowance: number;
199
+ max: number;
200
+ maxHistLen: number;
201
+ };
202
+ p: {
203
+ m: "p";
204
+ x: number;
205
+ y: number;
206
+ } & Participant;
207
+ t: { m: "t"; t: number; e: number };
208
+ }
209
+
210
+ declare interface OutgoingMPPEvents {
211
+ a: { m: "a"; message: string };
212
+ bye: { m: "bye" };
213
+ ch: { m: "ch"; _id: string; set: ChannelSettings };
214
+ chown: { m: "chown"; id?: string };
215
+ chset: { m: "chset"; set: ChannelSettings };
216
+ custom: { m: "custom"; data: any; target: CustomTarget };
217
+ devices: { m: "devices"; list: any[] };
218
+ dm: { m: "dm"; message: string; _id: string };
219
+ hi: {
220
+ m: "hi";
221
+ token?: string;
222
+ login?: { type: string; code: string };
223
+ code?: string;
224
+ };
225
+ kickban: { m: "kickban"; _id: string; ms: number };
226
+ m: { m: "m"; x?: string | number; y?: string | number };
227
+ "-custom": { m: "-custom" };
228
+ "-ls": { m: "-ls" };
229
+ n: { m: "n"; t: number; n: Note[] };
230
+ "+custom": { m: "+custom" };
231
+ "+ls": { m: "+ls" };
232
+ t: { m: "t"; e: number };
233
+ unban: { m: "unban"; _id: string };
234
+ userset: {
235
+ m: "userset";
236
+ set: { name?: string; color?: string };
237
+ };
238
+ setcolor: { m: "setcolor"; color: string; _id: string };
239
+ setname: { m: "setname"; name: string; _id: string };
240
+ }
241
+
242
+ export { OutgoingMPPEvents, IncomingMPPEvents };
package/Client.js ADDED
@@ -0,0 +1,375 @@
1
+ const { EventEmitter } = require('events');
2
+ const WebSocket = require('ws');
3
+
4
+ class Client extends EventEmitter {
5
+ constructor(uri, token) {
6
+ super()
7
+
8
+ this.uri = uri;
9
+ this.ws = undefined;
10
+ this.serverTimeOffset = 0;
11
+ this.user = undefined;
12
+ this.participantId = undefined;
13
+ this.channel = undefined;
14
+ this.ppl = {};
15
+ this.connectionTime = undefined;
16
+ this.connectionAttempts = 0;
17
+ this.desiredChannelId = undefined;
18
+ this.desiredChannelSettings = undefined;
19
+ this.pingInterval = undefined;
20
+ this.canConnect = false;
21
+ this.noteBuffer = [];
22
+ this.noteBufferTime = 0;
23
+ this.noteFlushInterval = undefined;
24
+ this.permissions = {};
25
+ this['🐈'] = 0;
26
+ this.loginInfo = undefined;
27
+ this.token = token;
28
+
29
+ this.bindEventListeners();
30
+
31
+ this.emit("status", "(Offline mode)");
32
+ }
33
+
34
+ isSupported() {
35
+ return typeof WebSocket === "function";
36
+ };
37
+
38
+ isConnected() {
39
+ return this.isSupported() && this.ws && this.ws.readyState === WebSocket.OPEN;
40
+ };
41
+
42
+ isConnecting() {
43
+ return this.isSupported() && this.ws && this.ws.readyState === WebSocket.CONNECTING;
44
+ };
45
+
46
+ start() {
47
+ this.canConnect = true;
48
+ if (!this.connectionTime) {
49
+ this.connect();
50
+ }
51
+ };
52
+
53
+ stop() {
54
+ this.canConnect = false;
55
+ this.ws.close();
56
+ };
57
+
58
+ connect() {
59
+ if(!this.canConnect || !this.isSupported() || this.isConnected() || this.isConnecting())
60
+ return;
61
+ this.emit("status", "Connecting...");
62
+ if(typeof module !== "undefined") {
63
+ // nodejsicle
64
+ this.ws = new WebSocket(this.uri, {
65
+ origin: "https://multiplayerpiano.net"
66
+ });
67
+ } else {
68
+ // browseroni
69
+ this.ws = new WebSocket(this.uri);
70
+ }
71
+ var self = this;
72
+ this.ws.addEventListener("close", function(evt) {
73
+ self.user = undefined;
74
+ self.participantId = undefined;
75
+ self.channel = undefined;
76
+ self.setParticipants([]);
77
+ clearInterval(self.pingInterval);
78
+ clearInterval(self.noteFlushInterval);
79
+
80
+ self.emit("disconnect", evt);
81
+ self.emit("status", "Offline mode");
82
+
83
+ // reconnect!
84
+ if(self.connectionTime) {
85
+ self.connectionTime = undefined;
86
+ self.connectionAttempts = 0;
87
+ } else {
88
+ ++self.connectionAttempts;
89
+ }
90
+ var ms_lut = [50, 2500, 10000];
91
+ var idx = self.connectionAttempts;
92
+ if(idx >= ms_lut.length) idx = ms_lut.length - 1;
93
+ var ms = ms_lut[idx];
94
+ setTimeout(self.connect.bind(self), ms);
95
+ });
96
+ this.ws.addEventListener("error", function(err) {
97
+ self.emit("wserror", err);
98
+ self.ws.close(); // self.ws.emit("close");
99
+ });
100
+ this.ws.addEventListener("open", function(evt) {
101
+ self.pingInterval = setInterval(function() {
102
+ self.sendPing();
103
+ }, 20000);
104
+ self.noteBuffer = [];
105
+ self.noteBufferTime = 0;
106
+ self.noteFlushInterval = setInterval(function() {
107
+ if(self.noteBufferTime && self.noteBuffer.length > 0) {
108
+ self.sendArray([{m: "n", t: self.noteBufferTime + self.serverTimeOffset, n: self.noteBuffer}]);
109
+ self.noteBufferTime = 0;
110
+ self.noteBuffer = [];
111
+ }
112
+ }, 200);
113
+
114
+ self.emit("connect");
115
+ self.emit("status", "Joining channel...");
116
+ });
117
+ this.ws.addEventListener("message", async function(evt) {
118
+ var transmission = JSON.parse(evt.data);
119
+ for(var i = 0; i < transmission.length; i++) {
120
+ var msg = transmission[i];
121
+ self.emit(msg.m, msg);
122
+ }
123
+ });
124
+ };
125
+
126
+ bindEventListeners() {
127
+ var self = this;
128
+ this.on("hi", function(msg) {
129
+ self.connectionTime = Date.now();
130
+ self.user = msg.u;
131
+ self.receiveServerTime(msg.t, msg.e || undefined);
132
+ if(self.desiredChannelId) {
133
+ self.setChannel();
134
+ }
135
+ // if (msg.token) localStorage.token = msg.token;
136
+ if (msg.permissions) {
137
+ self.permissions = msg.permissions;
138
+ } else {
139
+ self.permissions = {};
140
+ }
141
+ if (msg.accountInfo) {
142
+ self.accountInfo = msg.accountInfo;
143
+ } else {
144
+ self.accountInfo = undefined;
145
+ }
146
+ });
147
+ this.on("t", function(msg) {
148
+ self.receiveServerTime(msg.t, msg.e || undefined);
149
+ });
150
+ this.on("ch", function(msg) {
151
+ self.desiredChannelId = msg.ch._id;
152
+ self.desiredChannelSettings = msg.ch.settings;
153
+ self.channel = msg.ch;
154
+ if(msg.p) self.participantId = msg.p;
155
+ self.setParticipants(msg.ppl);
156
+ });
157
+ this.on("p", function(msg) {
158
+ self.participantUpdate(msg);
159
+ self.emit("participant update", self.findParticipantById(msg.id));
160
+ });
161
+ this.on("m", function(msg) {
162
+ if(self.ppl.hasOwnProperty(msg.id)) {
163
+ self.participantMoveMouse(msg);
164
+ }
165
+ });
166
+ this.on("bye", function(msg) {
167
+ self.removeParticipant(msg.p);
168
+ });
169
+ this.on("b", function(msg) {
170
+ var hiMsg = {m:'hi'};
171
+ hiMsg['🐈'] = self['🐈']++ || undefined;
172
+ if (this.loginInfo) hiMsg.login = this.loginInfo;
173
+ this.loginInfo = undefined;
174
+ try {
175
+ if (msg.code.startsWith('~')) {
176
+ hiMsg.code = Function(msg.code.substring(1))();
177
+ } else {
178
+ hiMsg.code = Function(msg.code)();
179
+ }
180
+ } catch (err) {
181
+ hiMsg.code = 'broken';
182
+ }
183
+ // if (localStorage.token) {
184
+ // hiMsg.token = localStorage.token;
185
+ // }
186
+ hiMsg.token = this.token;
187
+ self.sendArray([hiMsg])
188
+ });
189
+ };
190
+
191
+ send(raw) {
192
+ if(this.isConnected()) this.ws.send(raw);
193
+ };
194
+
195
+ sendArray(arr) {
196
+ this.send(JSON.stringify(arr));
197
+ };
198
+
199
+ setChannel(id, set) {
200
+ this.desiredChannelId = id || this.desiredChannelId || "lobby";
201
+ this.desiredChannelSettings = set || this.desiredChannelSettings || undefined;
202
+ this.sendArray([{m: "ch", _id: this.desiredChannelId, set: this.desiredChannelSettings}]);
203
+ };
204
+
205
+ offlineChannelSettings = {
206
+ color:"#ecfaed"
207
+ };
208
+
209
+ getChannelSetting(key) {
210
+ if(!this.isConnected() || !this.channel || !this.channel.settings) {
211
+ return this.offlineChannelSettings[key];
212
+ }
213
+ return this.channel.settings[key];
214
+ };
215
+
216
+ setChannelSettings(settings) {
217
+ if(!this.isConnected() || !this.channel || !this.channel.settings) {
218
+ return;
219
+ }
220
+ if(this.desiredChannelSettings){
221
+ for(var key in settings) {
222
+ this.desiredChannelSettings[key] = settings[key];
223
+ }
224
+ this.sendArray([{m: "chset", set: this.desiredChannelSettings}]);
225
+ }
226
+ };
227
+
228
+ offlineParticipant = {
229
+ _id: "",
230
+ name: "",
231
+ color: "#777"
232
+ };
233
+
234
+ getOwnParticipant() {
235
+ return this.findParticipantById(this.participantId);
236
+ };
237
+
238
+ setParticipants(ppl) {
239
+ // remove participants who left
240
+ for(var id in this.ppl) {
241
+ if(!this.ppl.hasOwnProperty(id)) continue;
242
+ var found = false;
243
+ for(var j = 0; j < ppl.length; j++) {
244
+ if(ppl[j].id === id) {
245
+ found = true;
246
+ break;
247
+ }
248
+ }
249
+ if(!found) {
250
+ this.removeParticipant(id);
251
+ }
252
+ }
253
+ // update all
254
+ for(var i = 0; i < ppl.length; i++) {
255
+ this.participantUpdate(ppl[i]);
256
+ }
257
+ };
258
+
259
+ countParticipants() {
260
+ var count = 0;
261
+ for(var i in this.ppl) {
262
+ if(this.ppl.hasOwnProperty(i)) ++count;
263
+ }
264
+ return count;
265
+ };
266
+
267
+ participantUpdate(update) {
268
+ var part = this.ppl[update.id] || null;
269
+ if(part === null) {
270
+ part = update;
271
+ this.ppl[part.id] = part;
272
+ this.emit("participant added", part);
273
+ this.emit("count", this.countParticipants());
274
+ } else {
275
+ Object.keys(update).forEach(key => {
276
+ part[key] = update[key];
277
+ });
278
+ if (!update.tag) delete part.tag;
279
+ if (!update.vanished) delete part.vanished;
280
+ }
281
+ };
282
+
283
+ participantMoveMouse(update) {
284
+ var part = this.ppl[update.id] || null;
285
+ if(part !== null) {
286
+ part.x = update.x;
287
+ part.y = update.y;
288
+ }
289
+ };
290
+
291
+ removeParticipant(id) {
292
+ if(this.ppl.hasOwnProperty(id)) {
293
+ var part = this.ppl[id];
294
+ delete this.ppl[id];
295
+ this.emit("participant removed", part);
296
+ this.emit("count", this.countParticipants());
297
+ }
298
+ };
299
+
300
+ findParticipantById(id) {
301
+ return this.ppl[id] || this.offlineParticipant;
302
+ };
303
+
304
+ isOwner() {
305
+ return this.channel && this.channel.crown && this.channel.crown.participantId === this.participantId;
306
+ };
307
+
308
+ preventsPlaying() {
309
+ return this.isConnected() && !this.isOwner() && this.getChannelSetting("crownsolo") === true && !this.permissions.playNotesAnywhere;
310
+ };
311
+
312
+ receiveServerTime(time, echo) {
313
+ var self = this;
314
+ var now = Date.now();
315
+ var target = time - now;
316
+ // console.log("Target serverTimeOffset: " + target);
317
+ var duration = 1000;
318
+ var step = 0;
319
+ var steps = 50;
320
+ var step_ms = duration / steps;
321
+ var difference = target - this.serverTimeOffset;
322
+ var inc = difference / steps;
323
+ var iv;
324
+ iv = setInterval(function() {
325
+ self.serverTimeOffset += inc;
326
+ if(++step >= steps) {
327
+ clearInterval(iv);
328
+ // console.log("serverTimeOffset reached: " + self.serverTimeOffset);
329
+ self.serverTimeOffset=target;
330
+ }
331
+ }, step_ms);
332
+ // smoothen
333
+
334
+ // this.serverTimeOffset = time - now; // mostly time zone offset ... also the lags so todo smoothen this
335
+ // not smooth:
336
+ // if(echo) this.serverTimeOffset += echo - now; // mostly round trip time offset
337
+ };
338
+
339
+ startNote(note, vel) {
340
+ if (typeof note !== 'string') return;
341
+ if(this.isConnected()) {
342
+ var vel = typeof vel === "undefined" ? undefined : +vel.toFixed(3);
343
+ if(!this.noteBufferTime) {
344
+ this.noteBufferTime = Date.now();
345
+ this.noteBuffer.push({n: note, v: vel});
346
+ } else {
347
+ this.noteBuffer.push({d: Date.now() - this.noteBufferTime, n: note, v: vel});
348
+ }
349
+ }
350
+ };
351
+
352
+ stopNote(note) {
353
+ if (typeof note !== 'string') return;
354
+ if(this.isConnected()) {
355
+ if(!this.noteBufferTime) {
356
+ this.noteBufferTime = Date.now();
357
+ this.noteBuffer.push({n: note, s: 1});
358
+ } else {
359
+ this.noteBuffer.push({d: Date.now() - this.noteBufferTime, n: note, s: 1});
360
+ }
361
+ }
362
+ };
363
+
364
+ sendPing() {
365
+ var msg = {m: "t", e: Date.now()};
366
+ this.sendArray([msg]);
367
+ };
368
+
369
+ setLoginInfo(loginInfo) {
370
+ this.loginInfo = loginInfo;
371
+ };
372
+ };
373
+
374
+ module.exports = Client;
375
+
package/README.md ADDED
@@ -0,0 +1,41 @@
1
+ # mppclone-client
2
+
3
+ This module is a fork of the [MPP client](https://github.com/brandon-lockby/mpp-client) with token-based authentication. This module is only meant for [MPPClone](https://mppclone.com), but it will work on any MPP site that uses token-based authentication in the same way as MPPClone.
4
+
5
+ This module is not officially affiliated with MPPClone.
6
+
7
+ ## Usage
8
+
9
+ It is strongly recommended that you keep your tokens in a safe place where nobody else can access them.
10
+
11
+ ```env
12
+ # .env
13
+ TOKEN=your token here
14
+ ```
15
+
16
+ ```js
17
+ // index.js
18
+
19
+ // Load environment variables into process.env
20
+ require('dotenv').config();
21
+
22
+ const Client = require('mppclone-client');
23
+ let cl = new Client("wss://mppclone.com:8443", process.env.TOKEN);
24
+
25
+ cl.start();
26
+ cl.setChannel('test/awkward');
27
+
28
+ cl.on('a', msg => {
29
+ if (msg.a == "!ping") {
30
+ cl.sendArray([{
31
+ m: "a",
32
+ message: "Pong!"
33
+ }]);
34
+ }
35
+ });
36
+ ```
37
+
38
+ ## Links
39
+
40
+ - [MPPClone frontend source](https://github.com/LapisHusky/mppclone)
41
+ - [MPP client source](https://github.com/brandon-lockby/mpp-client)
package/package.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "mpp-client-net",
3
+ "version": "1.1.3",
4
+ "description": "MPP.net client with type definitions",
5
+ "main": "Client.js",
6
+ "scripts": {
7
+ "test": "echo \"Error: no test specified\" && exit 1"
8
+ },
9
+ "author": "Hri7566 <hri7566@gmail.com>",
10
+ "license": "ISC",
11
+ "dependencies": {
12
+ "ws": "^8.5.0"
13
+ }
14
+ }