ogi-addon 1.1.0 → 1.1.6
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.
- package/LICENSE +660 -660
- package/README.md +40 -40
- package/build/EventResponse.cjs.map +1 -1
- package/build/EventResponse.js.map +1 -1
- package/build/SearchEngine.cjs.map +1 -1
- package/build/config/Configuration.cjs.map +1 -1
- package/build/config/Configuration.js.map +1 -1
- package/build/config/ConfigurationBuilder.cjs.map +1 -1
- package/build/config/ConfigurationBuilder.js.map +1 -1
- package/build/main.cjs +91 -19
- package/build/main.cjs.map +1 -1
- package/build/main.d.cts +60 -5
- package/build/main.d.ts +60 -5
- package/build/main.js +88 -18
- package/build/main.js.map +1 -1
- package/package.json +50 -49
- package/schema.json +30 -30
- package/src/EventResponse.ts +59 -59
- package/src/SearchEngine.ts +16 -16
- package/src/config/Configuration.ts +82 -82
- package/src/config/ConfigurationBuilder.ts +270 -270
- package/src/main.ts +521 -430
- package/tsconfig.json +38 -38
- package/tsup.config.js +10 -10
package/src/main.ts
CHANGED
|
@@ -1,430 +1,521 @@
|
|
|
1
|
-
import ws, { WebSocket } from 'ws';
|
|
2
|
-
import events from 'node:events';
|
|
3
|
-
import { ConfigurationBuilder, ConfigurationFile } from './config/ConfigurationBuilder';
|
|
4
|
-
import { Configuration } from './config/Configuration';
|
|
5
|
-
import EventResponse from './EventResponse';
|
|
6
|
-
import { SearchResult } from './SearchEngine';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
export type
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
export
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
*
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
*
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
*
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
export interface
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
public
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
this.
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
this.
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
},
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
1
|
+
import ws, { WebSocket } from 'ws';
|
|
2
|
+
import events from 'node:events';
|
|
3
|
+
import { ConfigurationBuilder, ConfigurationFile } from './config/ConfigurationBuilder';
|
|
4
|
+
import { Configuration } from './config/Configuration';
|
|
5
|
+
import EventResponse from './EventResponse';
|
|
6
|
+
import { SearchResult } from './SearchEngine';
|
|
7
|
+
import Fuse from 'fuse.js';
|
|
8
|
+
|
|
9
|
+
export type OGIAddonEvent = 'connect' | 'disconnect' | 'configure' | 'authenticate' | 'search' | 'setup' | 'library-search' | 'game-details' | 'exit' | 'request-dl';
|
|
10
|
+
export type OGIAddonClientSentEvent = 'response' | 'authenticate' | 'configure' | 'defer-update' | 'notification' | 'input-asked' | 'steam-search' | 'task-update';
|
|
11
|
+
|
|
12
|
+
export type OGIAddonServerSentEvent = 'authenticate' | 'configure' | 'config-update' | 'search' | 'setup' | 'response' | 'library-search' | 'game-details' | 'request-dl';
|
|
13
|
+
export { ConfigurationBuilder, Configuration, EventResponse, SearchResult };
|
|
14
|
+
const defaultPort = 7654;
|
|
15
|
+
import pjson from '../package.json';
|
|
16
|
+
export const VERSION = pjson.version;
|
|
17
|
+
|
|
18
|
+
export interface ClientSentEventTypes {
|
|
19
|
+
response: any;
|
|
20
|
+
authenticate: {
|
|
21
|
+
name: string;
|
|
22
|
+
id: string;
|
|
23
|
+
description: string;
|
|
24
|
+
version: string;
|
|
25
|
+
author: string;
|
|
26
|
+
};
|
|
27
|
+
configure: ConfigurationFile;
|
|
28
|
+
'defer-update': {
|
|
29
|
+
logs: string[],
|
|
30
|
+
progress: number
|
|
31
|
+
};
|
|
32
|
+
notification: Notification;
|
|
33
|
+
'input-asked': ConfigurationBuilder;
|
|
34
|
+
'steam-search': {
|
|
35
|
+
query: string;
|
|
36
|
+
strict: boolean;
|
|
37
|
+
};
|
|
38
|
+
'task-update': {
|
|
39
|
+
id: string;
|
|
40
|
+
progress: number;
|
|
41
|
+
logs: string[];
|
|
42
|
+
finished: boolean;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type BasicLibraryInfo = {
|
|
47
|
+
name: string;
|
|
48
|
+
capsuleImage: string;
|
|
49
|
+
appID: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface EventListenerTypes {
|
|
53
|
+
/**
|
|
54
|
+
* This event is emitted when the addon connects to the OGI Addon Server. Addon does not need to resolve anything.
|
|
55
|
+
* @param socket
|
|
56
|
+
* @returns
|
|
57
|
+
*/
|
|
58
|
+
connect: (socket: ws) => void;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* This event is emitted when the client requests for the addon to disconnect. Addon does not need to resolve this event, but we recommend `process.exit(0)` so the addon can exit gracefully instead of by force by the addon server.
|
|
62
|
+
* @param reason
|
|
63
|
+
* @returns
|
|
64
|
+
*/
|
|
65
|
+
disconnect: (reason: string) => void;
|
|
66
|
+
/**
|
|
67
|
+
* This event is emitted when the client requests for the addon to configure itself. Addon should resolve the event with the internal configuration. (See ConfigurationBuilder)
|
|
68
|
+
* @param config
|
|
69
|
+
* @returns
|
|
70
|
+
*/
|
|
71
|
+
configure: (config: ConfigurationBuilder) => ConfigurationBuilder;
|
|
72
|
+
/**
|
|
73
|
+
* This event is called when the client provides a response to any event. This should be treated as middleware.
|
|
74
|
+
* @param response
|
|
75
|
+
* @returns
|
|
76
|
+
*/
|
|
77
|
+
response: (response: any) => void;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* This event is called when the client requests for the addon to authenticate itself. You don't need to provide any info.
|
|
81
|
+
* @param config
|
|
82
|
+
* @returns
|
|
83
|
+
*/
|
|
84
|
+
authenticate: (config: any) => void;
|
|
85
|
+
/**
|
|
86
|
+
* This event is emitted when the client requests for a torrent/direct download search to be performed. Addon is given the gameID (could be a steam appID or custom store appID), along with the storefront type. Addon should resolve the event with the search results. (See SearchResult)
|
|
87
|
+
* @param query
|
|
88
|
+
* @param event
|
|
89
|
+
* @returns
|
|
90
|
+
*/
|
|
91
|
+
search: (query: { type: 'steamapp' | 'internal', text: string }, event: EventResponse<SearchResult[]>) => void;
|
|
92
|
+
/**
|
|
93
|
+
* This event is emitted when the client requests for app setup to be performed. Addon should resolve the event with the metadata for the library entry. (See LibraryInfo)
|
|
94
|
+
* @param data
|
|
95
|
+
* @param event
|
|
96
|
+
* @returns
|
|
97
|
+
*/
|
|
98
|
+
setup: (
|
|
99
|
+
data: {
|
|
100
|
+
path: string,
|
|
101
|
+
type: 'direct' | 'torrent' | 'magnet',
|
|
102
|
+
name: string,
|
|
103
|
+
usedRealDebrid: boolean,
|
|
104
|
+
multiPartFiles?: {
|
|
105
|
+
name: string,
|
|
106
|
+
downloadURL: string
|
|
107
|
+
}[],
|
|
108
|
+
appID: number,
|
|
109
|
+
storefront: 'steam' | 'internal'
|
|
110
|
+
}, event: EventResponse<LibraryInfo>
|
|
111
|
+
) => void;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* This event is emitted when the client requires for a search to be performed. Input is the search query.
|
|
115
|
+
* @param query
|
|
116
|
+
* @param event
|
|
117
|
+
* @returns
|
|
118
|
+
*/
|
|
119
|
+
'library-search': (query: string, event: EventResponse<BasicLibraryInfo[]>) => void;
|
|
120
|
+
'game-details': (appID: number, event: EventResponse<StoreData>) => void;
|
|
121
|
+
exit: () => void;
|
|
122
|
+
'request-dl': (appID: number, info: SearchResult, event: EventResponse<SearchResult>) => void;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface StoreData {
|
|
126
|
+
name: string;
|
|
127
|
+
publishers: string[];
|
|
128
|
+
developers: string[];
|
|
129
|
+
appID: number;
|
|
130
|
+
releaseDate: string;
|
|
131
|
+
capsuleImage: string;
|
|
132
|
+
coverImage: string;
|
|
133
|
+
basicDescription: string;
|
|
134
|
+
description: string;
|
|
135
|
+
headerImage: string;
|
|
136
|
+
}
|
|
137
|
+
export interface WebsocketMessageClient {
|
|
138
|
+
event: OGIAddonClientSentEvent;
|
|
139
|
+
id?: string;
|
|
140
|
+
args: any;
|
|
141
|
+
}
|
|
142
|
+
export interface WebsocketMessageServer {
|
|
143
|
+
event: OGIAddonServerSentEvent;
|
|
144
|
+
id?: string;
|
|
145
|
+
args: any;
|
|
146
|
+
}
|
|
147
|
+
export interface OGIAddonConfiguration {
|
|
148
|
+
name: string;
|
|
149
|
+
id: string;
|
|
150
|
+
description: string;
|
|
151
|
+
version: string;
|
|
152
|
+
|
|
153
|
+
author: string;
|
|
154
|
+
repository: string;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* The main class for the OGI Addon. This class is used to interact with the OGI Addon Server. The OGI Addon Server provides a `--addonSecret` to the addon so it can securely connect.
|
|
159
|
+
* @example
|
|
160
|
+
* ```typescript
|
|
161
|
+
* const addon = new OGIAddon({
|
|
162
|
+
* name: 'Test Addon',
|
|
163
|
+
* id: 'test-addon',
|
|
164
|
+
* description: 'A test addon',
|
|
165
|
+
* version: '1.0.0',
|
|
166
|
+
* author: 'OGI Developers',
|
|
167
|
+
* repository: ''
|
|
168
|
+
* });
|
|
169
|
+
* ```
|
|
170
|
+
*
|
|
171
|
+
*/
|
|
172
|
+
export default class OGIAddon {
|
|
173
|
+
public eventEmitter = new events.EventEmitter();
|
|
174
|
+
public addonWSListener: OGIAddonWSListener;
|
|
175
|
+
public addonInfo: OGIAddonConfiguration;
|
|
176
|
+
public config: Configuration = new Configuration({});
|
|
177
|
+
|
|
178
|
+
constructor(addonInfo: OGIAddonConfiguration) {
|
|
179
|
+
this.addonInfo = addonInfo;
|
|
180
|
+
this.addonWSListener = new OGIAddonWSListener(this, this.eventEmitter);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Register an event listener for the addon. (See EventListenerTypes)
|
|
185
|
+
* @param event {OGIAddonEvent}
|
|
186
|
+
* @param listener {EventListenerTypes[OGIAddonEvent]}
|
|
187
|
+
*/
|
|
188
|
+
public on<T extends OGIAddonEvent>(event: T, listener: EventListenerTypes[T]) {
|
|
189
|
+
this.eventEmitter.on(event, listener);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
public emit<T extends OGIAddonEvent>(event: T, ...args: Parameters<EventListenerTypes[T]>) {
|
|
193
|
+
this.eventEmitter.emit(event, ...args);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Notify the client using a notification. Provide the type of notification, the message, and an ID.
|
|
198
|
+
* @param notification {Notification}
|
|
199
|
+
*/
|
|
200
|
+
public notify(notification: Notification) {
|
|
201
|
+
this.addonWSListener.send('notification', [notification]);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Search for items in the OGI Steam-Synced Library. Query can either be a Steam AppID or a Steam Game Name.
|
|
206
|
+
* @param query {string}
|
|
207
|
+
* @param event {EventResponse<BasicLibraryInfo[]>}
|
|
208
|
+
*/
|
|
209
|
+
public async steamSearch(query: string, strict: boolean = false) {
|
|
210
|
+
const id = this.addonWSListener.send('steam-search', { query, strict });
|
|
211
|
+
return await this.addonWSListener.waitForResponseFromServer<Omit<BasicLibraryInfo, 'capsuleImage'>[]>(id);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Notify the OGI Addon Server that you are performing a background task. This can be used to help users understand what is happening in the background.
|
|
216
|
+
* @param id {string}
|
|
217
|
+
* @param progress {number}
|
|
218
|
+
* @param logs {string[]}
|
|
219
|
+
*/
|
|
220
|
+
public async task() {
|
|
221
|
+
const id = Math.random().toString(36).substring(7);
|
|
222
|
+
const progress = 0;
|
|
223
|
+
const logs: string[] = [];
|
|
224
|
+
const task = new CustomTask(this.addonWSListener, id, progress, logs);
|
|
225
|
+
this.addonWSListener.send('task-update', { id, progress, logs, finished: false });
|
|
226
|
+
return task;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export class CustomTask {
|
|
231
|
+
public readonly id: string;
|
|
232
|
+
public progress: number;
|
|
233
|
+
public logs: string[];
|
|
234
|
+
public finished: boolean = false;
|
|
235
|
+
public ws: OGIAddonWSListener;
|
|
236
|
+
constructor(ws: OGIAddonWSListener, id: string, progress: number, logs: string[]) {
|
|
237
|
+
this.id = id;
|
|
238
|
+
this.progress = progress;
|
|
239
|
+
this.logs = logs;
|
|
240
|
+
this.ws = ws;
|
|
241
|
+
}
|
|
242
|
+
public log(log: string) {
|
|
243
|
+
this.logs.push(log);
|
|
244
|
+
this.update();
|
|
245
|
+
}
|
|
246
|
+
public finish() {
|
|
247
|
+
this.finished = true;
|
|
248
|
+
this.update();
|
|
249
|
+
}
|
|
250
|
+
public setProgress(progress: number) {
|
|
251
|
+
this.progress = progress;
|
|
252
|
+
this.update();
|
|
253
|
+
}
|
|
254
|
+
public update() {
|
|
255
|
+
this.ws.send('task-update', { id: this.id, progress: this.progress, logs: this.logs, finished: this.finished });
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* A search tool for the OGI Addon. This tool is used to search for items in the library. Powered by Fuse.js, bundled into OGI.
|
|
260
|
+
* @example
|
|
261
|
+
* ```typescript
|
|
262
|
+
* const searchTool = new SearchTool<LibraryInfo>([], ['name']);
|
|
263
|
+
* const results = searchTool.search('test', 10);
|
|
264
|
+
* ```
|
|
265
|
+
*/
|
|
266
|
+
export class SearchTool<T> {
|
|
267
|
+
private fuse: Fuse<T>;
|
|
268
|
+
constructor(items: T[], keys: string[]) {
|
|
269
|
+
this.fuse = new Fuse(items, {
|
|
270
|
+
keys,
|
|
271
|
+
threshold: 0.3,
|
|
272
|
+
includeScore: true
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
public search(query: string, limit: number = 10): T[] {
|
|
276
|
+
return this.fuse.search(query).slice(0, limit).map(result => result.item);
|
|
277
|
+
}
|
|
278
|
+
public addItems(items: T[]) {
|
|
279
|
+
items.map(item => this.fuse.add(item));
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Library Info is the metadata for a library entry after setting up a game.
|
|
284
|
+
*/
|
|
285
|
+
export interface LibraryInfo {
|
|
286
|
+
name: string;
|
|
287
|
+
version: string;
|
|
288
|
+
cwd: string;
|
|
289
|
+
appID: number;
|
|
290
|
+
launchExecutable: string;
|
|
291
|
+
launchArguments?: string;
|
|
292
|
+
capsuleImage: string;
|
|
293
|
+
storefront: 'steam' | 'internal';
|
|
294
|
+
addonsource: string;
|
|
295
|
+
coverImage: string;
|
|
296
|
+
titleImage?: string;
|
|
297
|
+
}
|
|
298
|
+
interface Notification {
|
|
299
|
+
type: 'warning' | 'error' | 'info' | 'success';
|
|
300
|
+
message: string;
|
|
301
|
+
id: string
|
|
302
|
+
}
|
|
303
|
+
class OGIAddonWSListener {
|
|
304
|
+
private socket: WebSocket;
|
|
305
|
+
public eventEmitter: events.EventEmitter;
|
|
306
|
+
public addon: OGIAddon;
|
|
307
|
+
|
|
308
|
+
constructor(ogiAddon: OGIAddon, eventEmitter: events.EventEmitter) {
|
|
309
|
+
if (process.argv[process.argv.length - 1].split('=')[0] !== '--addonSecret') {
|
|
310
|
+
throw new Error('No secret provided. This usually happens because the addon was not started by the OGI Addon Server.');
|
|
311
|
+
}
|
|
312
|
+
this.addon = ogiAddon;
|
|
313
|
+
this.eventEmitter = eventEmitter;
|
|
314
|
+
this.socket = new ws('ws://localhost:' + defaultPort);
|
|
315
|
+
this.socket.on('open', () => {
|
|
316
|
+
console.log('Connected to OGI Addon Server');
|
|
317
|
+
console.log('OGI Addon Server Version:', VERSION);
|
|
318
|
+
|
|
319
|
+
// Authenticate with OGI Addon Server
|
|
320
|
+
this.send('authenticate', {
|
|
321
|
+
...this.addon.addonInfo,
|
|
322
|
+
secret: process.argv[process.argv.length - 1].split('=')[1],
|
|
323
|
+
ogiVersion: VERSION
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
this.eventEmitter.emit('connect');
|
|
327
|
+
|
|
328
|
+
// send a configuration request
|
|
329
|
+
let configBuilder = new ConfigurationBuilder();
|
|
330
|
+
this.eventEmitter.emit('configure', configBuilder);
|
|
331
|
+
this.send('configure', configBuilder.build(false));
|
|
332
|
+
this.addon.config = new Configuration(configBuilder.build(true));
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
this.socket.on('error', (error) => {
|
|
336
|
+
if (error.message.includes('Failed to connect')) {
|
|
337
|
+
throw new Error('OGI Addon Server is not running/is unreachable. Please start the server and try again.');
|
|
338
|
+
}
|
|
339
|
+
console.error('An error occurred:', error);
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
this.socket.on('close', (code, reason) => {
|
|
343
|
+
if (code === 1008) {
|
|
344
|
+
console.error('Authentication failed:', reason);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
this.eventEmitter.emit('disconnect', reason);
|
|
348
|
+
console.log("Disconnected from OGI Addon Server")
|
|
349
|
+
console.error(reason.toString())
|
|
350
|
+
this.eventEmitter.emit('exit');
|
|
351
|
+
this.socket.close();
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
this.registerMessageReceiver();
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
private async userInputAsked(configBuilt: ConfigurationBuilder, name: string, description: string, socket: WebSocket): Promise<{ [key: string]: number | boolean | string }> {
|
|
358
|
+
const config = configBuilt.build(false);
|
|
359
|
+
const id = Math.random().toString(36).substring(7);
|
|
360
|
+
if (!socket) {
|
|
361
|
+
return {};
|
|
362
|
+
}
|
|
363
|
+
socket.send(JSON.stringify({
|
|
364
|
+
event: 'input-asked',
|
|
365
|
+
args: {
|
|
366
|
+
config,
|
|
367
|
+
name,
|
|
368
|
+
description
|
|
369
|
+
},
|
|
370
|
+
id: id
|
|
371
|
+
}));
|
|
372
|
+
return await this.waitForResponseFromServer(id);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
private registerMessageReceiver() {
|
|
376
|
+
this.socket.on('message', async (data: string) => {
|
|
377
|
+
const message: WebsocketMessageServer = JSON.parse(data);
|
|
378
|
+
switch (message.event) {
|
|
379
|
+
case 'config-update':
|
|
380
|
+
const result = this.addon.config.updateConfig(message.args);
|
|
381
|
+
if (!result[0]) {
|
|
382
|
+
this.respondToMessage(message.id!!, { success: false, error: result[1] });
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
this.respondToMessage(message.id!!, { success: true });
|
|
386
|
+
}
|
|
387
|
+
break
|
|
388
|
+
case 'search':
|
|
389
|
+
let searchResultEvent = new EventResponse<SearchResult[]>((screen, name, description) => this.userInputAsked(screen, name, description, this.socket));
|
|
390
|
+
this.eventEmitter.emit('search', message.args, searchResultEvent);
|
|
391
|
+
const searchResult = await this.waitForEventToRespond(searchResultEvent);
|
|
392
|
+
this.respondToMessage(message.id!!, searchResult.data);
|
|
393
|
+
break
|
|
394
|
+
case 'setup':
|
|
395
|
+
let setupEvent = new EventResponse<LibraryInfo>((screen, name, description) => this.userInputAsked(screen, name, description, this.socket));
|
|
396
|
+
this.eventEmitter.emit('setup', { path: message.args.path, appID: message.args.appID, storefront: message.args.storefront, type: message.args.type, name: message.args.name, usedRealDebrid: message.args.usedRealDebrid, multiPartFiles: message.args.multiPartFiles }, setupEvent);
|
|
397
|
+
const interval = setInterval(() => {
|
|
398
|
+
if (setupEvent.resolved) {
|
|
399
|
+
clearInterval(interval);
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
this.send('defer-update', {
|
|
403
|
+
logs: setupEvent.logs,
|
|
404
|
+
deferID: message.args.deferID,
|
|
405
|
+
progress: setupEvent.progress
|
|
406
|
+
} as any);
|
|
407
|
+
}, 100);
|
|
408
|
+
const setupResult = await this.waitForEventToRespond(setupEvent);
|
|
409
|
+
this.respondToMessage(message.id!!, setupResult.data);
|
|
410
|
+
break
|
|
411
|
+
case 'library-search':
|
|
412
|
+
let librarySearchEvent = new EventResponse<BasicLibraryInfo[]>((screen, name, description) => this.userInputAsked(screen, name, description, this.socket));
|
|
413
|
+
if (this.eventEmitter.listenerCount('game-details') === 0) {
|
|
414
|
+
this.respondToMessage(message.id!!, []);
|
|
415
|
+
break;
|
|
416
|
+
}
|
|
417
|
+
this.eventEmitter.emit('library-search', message.args, librarySearchEvent);
|
|
418
|
+
const librarySearchResult = await this.waitForEventToRespond(librarySearchEvent);
|
|
419
|
+
this.respondToMessage(message.id!!, librarySearchResult.data);
|
|
420
|
+
break
|
|
421
|
+
case 'game-details':
|
|
422
|
+
let gameDetailsEvent = new EventResponse<StoreData>((screen, name, description) => this.userInputAsked(screen, name, description, this.socket));
|
|
423
|
+
if (this.eventEmitter.listenerCount('game-details') === 0) {
|
|
424
|
+
this.respondToMessage(message.id!!, { error: 'No event listener for game-details' });
|
|
425
|
+
break;
|
|
426
|
+
}
|
|
427
|
+
this.eventEmitter.emit('game-details', message.args, gameDetailsEvent);
|
|
428
|
+
const gameDetailsResult = await this.waitForEventToRespond(gameDetailsEvent);
|
|
429
|
+
this.respondToMessage(message.id!!, gameDetailsResult.data);
|
|
430
|
+
break
|
|
431
|
+
case 'request-dl':
|
|
432
|
+
let requestDLEvent = new EventResponse<SearchResult>((screen, name, description) => this.userInputAsked(screen, name, description, this.socket));
|
|
433
|
+
if (this.eventEmitter.listenerCount('request-dl') === 0) {
|
|
434
|
+
this.respondToMessage(message.id!!, { error: 'No event listener for request-dl' });
|
|
435
|
+
break;
|
|
436
|
+
}
|
|
437
|
+
this.eventEmitter.emit('request-dl', message.args.appID, message.args.info, requestDLEvent);
|
|
438
|
+
const requestDLResult = await this.waitForEventToRespond(requestDLEvent);
|
|
439
|
+
if (requestDLEvent.data === null || requestDLEvent.data?.downloadType === 'request') {
|
|
440
|
+
throw new Error('Request DL event did not return a valid result. Please ensure that the event does not resolve with another `request` download type.');
|
|
441
|
+
}
|
|
442
|
+
this.respondToMessage(message.id!!, requestDLResult.data);
|
|
443
|
+
break
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
private waitForEventToRespond<T>(event: EventResponse<T>): Promise<EventResponse<T>> {
|
|
449
|
+
// check the handlers to see if there even is any
|
|
450
|
+
return new Promise((resolve, reject) => {
|
|
451
|
+
const dataGet = setInterval(() => {
|
|
452
|
+
if (event.resolved) {
|
|
453
|
+
resolve(event);
|
|
454
|
+
clearTimeout(timeout);
|
|
455
|
+
}
|
|
456
|
+
}, 5);
|
|
457
|
+
|
|
458
|
+
const timeout = setTimeout(() => {
|
|
459
|
+
if (event.deffered) {
|
|
460
|
+
clearInterval(dataGet);
|
|
461
|
+
const interval = setInterval(() => {
|
|
462
|
+
if (event.resolved) {
|
|
463
|
+
clearInterval(interval);
|
|
464
|
+
resolve(event);
|
|
465
|
+
}
|
|
466
|
+
}, 100);
|
|
467
|
+
}
|
|
468
|
+
else {
|
|
469
|
+
reject('Event did not respond in time');
|
|
470
|
+
}
|
|
471
|
+
}, 5000)
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
public respondToMessage(messageID: string, response: any) {
|
|
476
|
+
this.socket.send(JSON.stringify({
|
|
477
|
+
event: 'response',
|
|
478
|
+
id: messageID,
|
|
479
|
+
args: response
|
|
480
|
+
}));
|
|
481
|
+
console.log("dispatched response to " + messageID)
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
public waitForResponseFromServer<T>(messageID: string): Promise<T> {
|
|
485
|
+
return new Promise((resolve) => {
|
|
486
|
+
const waiter = (data: string) => {
|
|
487
|
+
const message: WebsocketMessageClient = JSON.parse(data);
|
|
488
|
+
if (message.event !== 'response') {
|
|
489
|
+
this.socket.once('message', waiter);
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
console.log("received response from " + messageID)
|
|
493
|
+
|
|
494
|
+
if (message.id === messageID) {
|
|
495
|
+
resolve(message.args);
|
|
496
|
+
}
|
|
497
|
+
else {
|
|
498
|
+
this.socket.once('message', waiter);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
this.socket.once('message', waiter);
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
public send(event: OGIAddonClientSentEvent, args: ClientSentEventTypes[OGIAddonClientSentEvent]): string {
|
|
506
|
+
// generate a random id
|
|
507
|
+
const id = Math.random().toString(36).substring(7);
|
|
508
|
+
this.socket.send(JSON.stringify({
|
|
509
|
+
event,
|
|
510
|
+
args,
|
|
511
|
+
id
|
|
512
|
+
}));
|
|
513
|
+
return id;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
public close() {
|
|
517
|
+
this.socket.close();
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
}
|