wispjs 2.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,480 @@
1
+ import { WebsocketPool } from "./pool.js";
2
+ import { ConsoleMessage, FilesearchResults } from "./pool";
3
+ import { GitPullData, GitPullResult } from "./pool.js";
4
+ import { GitCloneData, GitCloneResult } from "./pool.js";
5
+
6
+ import type { WispAPI } from "../wisp_api/index.js";
7
+
8
+
9
+ /**
10
+ * The Websocket information returned from the API
11
+ *
12
+ * @param token The token to use when authenticating with the `auth` command in the Websocket
13
+ * @param url The actual URL of the Websocket
14
+ *
15
+ * @internal
16
+ */
17
+ export interface WebsocketInfo {
18
+ token: string;
19
+ url: string;
20
+ }
21
+
22
+
23
+ export type WebsocketDetailsPreprocessor = (info: WebsocketInfo) => void;
24
+
25
+
26
+ export interface WispSocket {
27
+ pool: WebsocketPool;
28
+ logger: any;
29
+ api: WispAPI;
30
+ url: string | undefined;
31
+ token: string | undefined;
32
+ ghToken: string | undefined;
33
+ consoleCallbacks: ((message: string) => void)[];
34
+ detailsPreprocessor: WebsocketDetailsPreprocessor | undefined;
35
+ }
36
+
37
+
38
+ /**
39
+ * The primary interface to the Websocket API
40
+ *
41
+ * @internal
42
+ */
43
+ export class WispSocket {
44
+ constructor(logger: any, api: any, ghToken: string | undefined) {
45
+ this.logger = logger
46
+ this.api = api
47
+ this.ghToken = ghToken
48
+ this.consoleCallbacks = []
49
+ }
50
+
51
+
52
+ /**
53
+ * Sets a callback to run on the Websocket Info before saving the details.
54
+ *
55
+ * @example
56
+ * ```js
57
+ * // Change the URL of the Websocket
58
+ * wisp.socket.setWebsocketDetailsPreprocessor((info) => {
59
+ * info.url = "wss://newurl.com"
60
+ * })
61
+ * ```
62
+ *
63
+ * @remarks
64
+ * ℹ️ This can be used to modify the URL or token after its retrieved from the API
65
+ *
66
+ * @param preprocessor The callback to run when the data is received from the API
67
+ *
68
+ * @public
69
+ */
70
+ setWebsocketDetailsPreprocessor(preprocessor: WebsocketDetailsPreprocessor) {
71
+ this.detailsPreprocessor = preprocessor;
72
+ }
73
+
74
+
75
+ /**
76
+ * Creates a new Websocket Pool
77
+ *
78
+ * @throws Throws an error if the URL or token are not set yet
79
+ * @internal
80
+ */
81
+ createPool() {
82
+ if (!this.url || !this.token) {
83
+ throw new Error("Attempted to create a pool without a URL or token")
84
+ }
85
+
86
+ this.pool = new WebsocketPool(this.url, this.token)
87
+ }
88
+
89
+
90
+ /**
91
+ * Requests and saves the Websocket details from the API
92
+ *
93
+ * @internal
94
+ */
95
+ async setDetails() {
96
+ try {
97
+ const websocketInfo: WebsocketInfo = await this.api.Servers.GetWebsocketDetails()
98
+ if (this.detailsPreprocessor) {
99
+ this.detailsPreprocessor(websocketInfo);
100
+ }
101
+
102
+ this.url = websocketInfo.url
103
+ this.token = websocketInfo.token
104
+
105
+ this.logger.info("Got Websocket Details.", this.url, this.token)
106
+ } catch(e) {
107
+ this.logger.error(`Failed to get websocket details: ${e}`)
108
+ throw(e)
109
+ }
110
+ }
111
+
112
+
113
+ /**
114
+ * Disconnects from the websocket
115
+ *
116
+ * @internal
117
+ */
118
+ async disconnect() {
119
+ if (this.pool) {
120
+ await this.pool.disconnect()
121
+ }
122
+ }
123
+
124
+
125
+ /**
126
+ * Verifies that the pool is created and ready to use
127
+ *
128
+ * @internal
129
+ */
130
+ async verifyPool() {
131
+ if (!this.pool) {
132
+ await this.setDetails()
133
+ this.createPool()
134
+ }
135
+ }
136
+
137
+
138
+ /**
139
+ * Searches all file contents for the given query
140
+ *
141
+ * @param query The query string to search for
142
+ *
143
+ * @public
144
+ */
145
+ async filesearch(query: string): Promise<FilesearchResults> {
146
+ this.logger.info("Running filesearch with: ", query)
147
+ await this.verifyPool()
148
+
149
+ return await this.pool.run((worker) => {
150
+ const socket = worker.socket
151
+ const logger = worker.logger
152
+ logger.log("Running filesearch:", query)
153
+
154
+ return new Promise<FilesearchResults>((resolve, reject) => {
155
+ let done = false
156
+
157
+ socket.once("filesearch-results", (data) => {
158
+ done = true
159
+ resolve(data)
160
+ })
161
+
162
+ socket.emit("filesearch-start", query)
163
+
164
+ // This uses a longer timeout because filesearch can take a while
165
+ setTimeout(() => {
166
+ if (!done) {
167
+ socket.off("filesearch-results")
168
+ logger.error("Rejected filesearch: 'Timeout'")
169
+ reject()
170
+ }
171
+ }, 10000)
172
+ })
173
+ })
174
+ }
175
+
176
+
177
+ /**
178
+ * Performs a git pull operation on the given directory
179
+ *
180
+ * @param dir The full directory path to perform a pull on
181
+ *
182
+ * @public
183
+ */
184
+ async gitPull(dir: string, useAuth: boolean = false) {
185
+ await this.verifyPool()
186
+
187
+ const pullResult = await this.pool.run((worker) => {
188
+ const socket = worker.socket;
189
+ const logger = worker.logger;
190
+ logger.log("Running gitPull:", dir);
191
+
192
+ return new Promise<GitPullResult>((resolve, reject) => {
193
+ let isPrivate = false
194
+
195
+ const finished = (success: boolean, output: string) => {
196
+ socket.removeAllListeners("git-pull")
197
+ socket.removeAllListeners("git-error")
198
+ socket.removeAllListeners("git-success")
199
+
200
+ const result: GitPullResult = {
201
+ output: output,
202
+ isPrivate: isPrivate
203
+ }
204
+
205
+ if (success) {
206
+ resolve(result)
207
+ } else {
208
+ logger.error("Rejected gitPull:", dir, output);
209
+ reject(output)
210
+ }
211
+ }
212
+
213
+ const sendRequest = (includeAuth: boolean = false) => {
214
+ const data: GitPullData = { dir: dir }
215
+
216
+ if (includeAuth) {
217
+ if (!this.ghToken) {
218
+ logger.error("No GitHub token set, can't authenticate")
219
+ return finished(false, "Authentication is required, but no GitHub token was set. Can't pull!")
220
+ }
221
+
222
+ isPrivate = true
223
+ data.authkey = this.ghToken
224
+ }
225
+
226
+ socket.emit("git-pull", data)
227
+ }
228
+
229
+ socket.once("git-pull", (data) => {
230
+ logger.log(`Updating ${data}`)
231
+ });
232
+
233
+ socket.once("git-success", (commit) => {
234
+ logger.log(`Addon updated to ${commit}`)
235
+
236
+ if (!commit) {
237
+ logger.log("No commit given!")
238
+ }
239
+
240
+ finished(true, commit || "")
241
+ });
242
+
243
+ socket.on("git-error", (message) => {
244
+ if (message === "Remote authentication required but no callback set") {
245
+ logger.log(`Remote authentication required, trying again with authkey: ${dir}`)
246
+ sendRequest(true)
247
+ } else {
248
+ logger.log(`Error updating addon: ${message}`)
249
+ finished(false, message)
250
+ }
251
+ })
252
+
253
+ sendRequest(useAuth)
254
+ })
255
+ })
256
+
257
+ return pullResult
258
+ }
259
+
260
+
261
+ /**
262
+ * Clones a new Repo to the given directory
263
+ *
264
+ * @param url The HTTPS URL of the repository
265
+ * @param dir The full path of the directory to clone the repository to
266
+ * @param branch The branch of the repository to clone
267
+ *
268
+ * @public
269
+ */
270
+ async gitClone(url: string, dir: string, branch: string) {
271
+ await this.verifyPool()
272
+
273
+ return await this.pool.run((worker) => {
274
+ const socket = worker.socket;
275
+ const logger = worker.logger;
276
+ logger.log("Running gitClone:", url, dir, branch);
277
+
278
+ return new Promise<GitCloneResult>((resolve, reject) => {
279
+ let isPrivate = false;
280
+
281
+ const finished = (success: boolean, message?: string) => {
282
+ socket.removeAllListeners("git-clone");
283
+ socket.removeAllListeners("git-error");
284
+ socket.removeAllListeners("git-success");
285
+
286
+ if (success) {
287
+ const result: GitCloneResult = {
288
+ isPrivate: isPrivate
289
+ }
290
+
291
+ resolve(result);
292
+ } else {
293
+ logger.error("Rejected gitClone:", url, dir, branch, message);
294
+ reject(message);
295
+ }
296
+ }
297
+
298
+ const sendRequest = (includeAuth: boolean = false) => {
299
+ const data: GitCloneData = { dir: dir, url: url, branch: branch };
300
+
301
+ if (includeAuth) {
302
+ if (!this.ghToken) {
303
+ logger.error("No GitHub token set, can't authenticate")
304
+ return finished(false, "Authentication is required, but no GitHub token was set. Can't clone!")
305
+ }
306
+
307
+ isPrivate = true;
308
+ data.authkey = this.ghToken;
309
+ }
310
+
311
+ socket.emit("git-clone", data);
312
+ }
313
+
314
+ socket.once("git-clone", (data) => {
315
+ logger.log(`Cloning ${data}`);
316
+ });
317
+
318
+ socket.once("git-success", () => {
319
+ logger.log("Project successfully cloned");
320
+ finished(true);
321
+ });
322
+
323
+ socket.on("git-error", (message) => {
324
+ if (message === "Remote authentication required but no callback set") {
325
+ logger.log(`Remote authentication required, trying again with authkey: ${dir}`);
326
+ sendRequest(true);
327
+ } else {
328
+ logger.log("Error cloning repo:", url, dir, branch, message);
329
+ finished(false, message);
330
+ }
331
+ });
332
+
333
+ sendRequest();
334
+ });
335
+ });
336
+ }
337
+
338
+
339
+ /**
340
+ * Sets up the console listener worker
341
+ *
342
+ * @internal
343
+ */
344
+ setupConsoleListener() {
345
+ this.verifyPool().then(() => {
346
+ this.pool.run((worker) => {
347
+ const logger = worker.logger;
348
+ logger.log("Running setupConsoleListener");
349
+
350
+ return new Promise<void>((resolve) => {
351
+ worker.socket.on("console", (data: ConsoleMessage) => {
352
+ const line = data.line;
353
+
354
+ if (this.consoleCallbacks.length == 0) {
355
+ return resolve();
356
+ }
357
+
358
+ this.consoleCallbacks.forEach((callback) => {
359
+ try {
360
+ callback(line);
361
+ } catch(e) {
362
+ logger.error("Failed to run console callback", e);
363
+ }
364
+ });
365
+ });
366
+ });
367
+ });
368
+ })
369
+ }
370
+
371
+
372
+ /**
373
+ * Adds a new callback that will run any time a console message is rececived
374
+ *
375
+ * @param callback The callback to run, takes a single param, `message`, a string
376
+ *
377
+ * @public
378
+ */
379
+ addConsoleListener(callback: (message: string) => void) {
380
+ if (this.consoleCallbacks.length == 0) {
381
+ this.setupConsoleListener();
382
+ }
383
+
384
+ this.consoleCallbacks.push(callback);
385
+ }
386
+
387
+
388
+ /**
389
+ * Removes a previously added console message callback
390
+ *
391
+ * @param callback The callback function that was previously added
392
+ *
393
+ * @public
394
+ */
395
+ removeConsoleListener(callback: (message: string) => void) {
396
+ const index = this.consoleCallbacks.indexOf(callback)
397
+ if (index == -1) { return }
398
+
399
+ this.consoleCallbacks.splice(index, 1)
400
+ }
401
+
402
+
403
+ /**
404
+ * Sends a command to the server and then waits until output with the given prefix is seen in a console message
405
+ *
406
+ * @example
407
+ * Runs a custom lua command that will prefix its output with our nonce, then prints the output from that command
408
+ * ```lua
409
+ * -- lua/autorun/server/nonce_example.lua
410
+ * concommand.Add( "myCommand", function( ply, _, args )
411
+ * if IsValid( ply ) then return end
412
+ *
413
+ * local nonce = args[1]
414
+ * print( nonce .. "Command output" )
415
+ * end )
416
+ * ```
417
+ * ```js
418
+ * const nonce = "abc123";
419
+ * const command = `myCommand "${nonce}"`;
420
+ * try {
421
+ * const output = await wisp.socket.sendCommandNonce(nonce, command);
422
+ * console.log("Output from command:", output);
423
+ * catch (error) {
424
+ * console.error(error);
425
+ * }
426
+ * ```
427
+ *
428
+ * @remarks
429
+ * ℹ️ This is useful if you run code on your Server that will print output with the same prefix, letting you run commands and also receive output for it
430
+ *
431
+ * @param nonce The short, unique string that your output will be prefixed with
432
+ * @param command The full command string to send
433
+ * @param timeout How long to wait for output before timing out
434
+ *
435
+ * @public
436
+ */
437
+ async sendCommandNonce(nonce: string, command: string, timeout: number = 1000) {
438
+ await this.verifyPool()
439
+
440
+ return await this.pool.run((worker) => {
441
+ const socket = worker.socket
442
+ const logger = worker.logger
443
+ logger.log("Running sendCommandNonce: ", nonce, command)
444
+
445
+ return new Promise<string>((resolve: Function, reject: Function) => {
446
+ let timeoutObj: NodeJS.Timeout
447
+ let callback: (data: ConsoleMessage) => void;
448
+
449
+ let output = ""
450
+
451
+ callback = (data: ConsoleMessage) => {
452
+ const line = data.line
453
+ if (line.startsWith(nonce)) {
454
+ const message = line.slice(nonce.length)
455
+
456
+ if (message === "Done.") {
457
+ socket.off("console", callback)
458
+ clearTimeout(timeoutObj)
459
+
460
+ resolve(output)
461
+ } else {
462
+ output += message
463
+ timeoutObj.refresh()
464
+ }
465
+ }
466
+ }
467
+
468
+ socket.on("console", callback)
469
+ socket.emit("send command", command)
470
+
471
+ timeoutObj = setTimeout(() => {
472
+ logger.error(`Command timed out current output: '${output}'`);
473
+ socket.off("console", callback);
474
+ logger.log("Rejected sendCommandNonce 'Timeout'", nonce, command)
475
+ reject("Timeout");
476
+ }, timeout);
477
+ });
478
+ });
479
+ }
480
+ }