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/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
- export type OGIAddonEvent = 'connect' | 'disconnect' | 'configure' | 'authenticate' | 'search' | 'setup' | 'library-search' | 'game-details' | 'exit' | 'request-dl';
9
- export type OGIAddonClientSentEvent = 'response' | 'authenticate' | 'configure' | 'defer-update' | 'notification' | 'input-asked';
10
-
11
- export type OGIAddonServerSentEvent = 'authenticate' | 'configure' | 'config-update' | 'search' | 'setup' | 'response' | 'library-search' | 'game-details' | 'request-dl';
12
- export { ConfigurationBuilder, Configuration, EventResponse, SearchResult };
13
- const defaultPort = 7654;
14
- import pjson from '../package.json';
15
- export const version = pjson.version;
16
-
17
- export interface ClientSentEventTypes {
18
- response: any;
19
- authenticate: any;
20
- configure: ConfigurationFile;
21
- 'defer-update': {
22
- logs: string[],
23
- progress: number
24
- };
25
- notification: Notification;
26
- 'input-asked': ConfigurationBuilder;
27
- }
28
-
29
- export type BasicLibraryInfo = {
30
- name: string;
31
- capsuleImage: string;
32
- appID: number;
33
- }
34
-
35
- export interface EventListenerTypes {
36
- /**
37
- * This event is emitted when the addon connects to the OGI Addon Server. Addon does not need to resolve anything.
38
- * @param socket
39
- * @returns
40
- */
41
- connect: (socket: ws) => void;
42
-
43
- /**
44
- * 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.
45
- * @param reason
46
- * @returns
47
- */
48
- disconnect: (reason: string) => void;
49
- /**
50
- * 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)
51
- * @param config
52
- * @returns
53
- */
54
- configure: (config: ConfigurationBuilder) => ConfigurationBuilder;
55
- /**
56
- * This event is called when the client provides a response to any event. This should be treated as middleware.
57
- * @param response
58
- * @returns
59
- */
60
- response: (response: any) => void;
61
-
62
- /**
63
- * This event is called when the client requests for the addon to authenticate itself. You don't need to provide any info.
64
- * @param config
65
- * @returns
66
- */
67
- authenticate: (config: any) => void;
68
- /**
69
- * 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)
70
- * @param query
71
- * @param event
72
- * @returns
73
- */
74
- search: (query: { type: 'steamapp' | 'internal', text: string }, event: EventResponse<SearchResult[]>) => void;
75
- /**
76
- * 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)
77
- * @param data
78
- * @param event
79
- * @returns
80
- */
81
- setup: (
82
- data: {
83
- path: string,
84
- type: 'direct' | 'torrent' | 'magnet',
85
- name: string,
86
- usedRealDebrid: boolean,
87
- multiPartFiles?: {
88
- name: string,
89
- downloadURL: string
90
- }[],
91
- appID: number,
92
- storefront: 'steam' | 'internal'
93
- }, event: EventResponse<LibraryInfo>
94
- ) => void;
95
-
96
- /**
97
- * This event is emitted when the client requires for a search to be performed. Input is the search query.
98
- * @param query
99
- * @param event
100
- * @returns
101
- */
102
- 'library-search': (query: string, event: EventResponse<BasicLibraryInfo[]>) => void;
103
- 'game-details': (appID: number, event: EventResponse<StoreData>) => void;
104
- exit: () => void;
105
- 'request-dl': (appID: number, info: SearchResult, event: EventResponse<SearchResult>) => void;
106
- }
107
-
108
- export interface StoreData {
109
- name: string;
110
- publishers: string[];
111
- developers: string[];
112
- appID: number;
113
- releaseDate: string;
114
- capsuleImage: string;
115
- coverImage: string;
116
- basicDescription: string;
117
- description: string;
118
- headerImage: string;
119
- }
120
- export interface WebsocketMessageClient {
121
- event: OGIAddonClientSentEvent;
122
- id?: string;
123
- args: any;
124
- }
125
- export interface WebsocketMessageServer {
126
- event: OGIAddonServerSentEvent;
127
- id?: string;
128
- args: any;
129
- }
130
- export interface OGIAddonConfiguration {
131
- name: string;
132
- id: string;
133
- description: string;
134
- version: string;
135
-
136
- author: string;
137
- repository: string;
138
- }
139
-
140
- /**
141
- * 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.
142
- * @example
143
- * ```typescript
144
- * const addon = new OGIAddon({
145
- * name: 'Test Addon',
146
- * id: 'test-addon',
147
- * description: 'A test addon',
148
- * version: '1.0.0',
149
- * author: 'OGI Developers',
150
- * repository: ''
151
- * });
152
- * ```
153
- *
154
- */
155
- export default class OGIAddon {
156
- public eventEmitter = new events.EventEmitter();
157
- public addonWSListener: OGIAddonWSListener;
158
- public addonInfo: OGIAddonConfiguration;
159
- public config: Configuration = new Configuration({});
160
-
161
- constructor(addonInfo: OGIAddonConfiguration) {
162
- this.addonInfo = addonInfo;
163
- this.addonWSListener = new OGIAddonWSListener(this, this.eventEmitter);
164
- }
165
-
166
- /**
167
- * Register an event listener for the addon. (See EventListenerTypes)
168
- * @param event {OGIAddonEvent}
169
- * @param listener {EventListenerTypes[OGIAddonEvent]}
170
- */
171
- public on<T extends OGIAddonEvent>(event: T, listener: EventListenerTypes[T]) {
172
- this.eventEmitter.on(event, listener);
173
- }
174
-
175
- public emit<T extends OGIAddonEvent>(event: T, ...args: Parameters<EventListenerTypes[T]>) {
176
- this.eventEmitter.emit(event, ...args);
177
- }
178
-
179
- /**
180
- * Notify the client using a notification. Provide the type of notification, the message, and an ID.
181
- * @param notification {Notification}
182
- */
183
- public notify(notification: Notification) {
184
- this.addonWSListener.send('notification', [ notification ]);
185
- }
186
- }
187
-
188
- /**
189
- * Library Info is the metadata for a library entry after setting up a game.
190
- */
191
- export interface LibraryInfo {
192
- name: string;
193
- version: string;
194
- cwd: string;
195
- appID: number;
196
- launchExecutable: string;
197
- launchArguments?: string;
198
- capsuleImage: string;
199
- storefront: 'steam' | 'internal';
200
- addonsource: string;
201
- coverImage: string;
202
- titleImage?: string;
203
- }
204
- interface Notification {
205
- type: 'warning' | 'error' | 'info' | 'success';
206
- message: string;
207
- id: string
208
- }
209
- class OGIAddonWSListener {
210
- private socket: WebSocket;
211
- public eventEmitter: events.EventEmitter;
212
- public addon: OGIAddon;
213
-
214
- constructor(ogiAddon: OGIAddon, eventEmitter: events.EventEmitter) {
215
- if (process.argv[process.argv.length - 1].split('=')[0] !== '--addonSecret') {
216
- throw new Error('No secret provided. This usually happens because the addon was not started by the OGI Addon Server.');
217
- }
218
- this.addon = ogiAddon;
219
- this.eventEmitter = eventEmitter;
220
- this.socket = new ws('ws://localhost:' + defaultPort);
221
- this.socket.on('open', () => {
222
- console.log('Connected to OGI Addon Server');
223
- console.log('OGI Addon Server Version:', version);
224
-
225
- // Authenticate with OGI Addon Server
226
- this.socket.send(JSON.stringify({
227
- event: 'authenticate',
228
- args: {
229
- ...this.addon.addonInfo,
230
- secret: process.argv[process.argv.length - 1].split('=')[1],
231
- ogiVersion: version
232
- }
233
- }));
234
-
235
- this.eventEmitter.emit('connect');
236
-
237
- // send a configuration request
238
- let configBuilder = new ConfigurationBuilder();
239
- this.eventEmitter.emit('configure', configBuilder);
240
-
241
- this.socket.send(JSON.stringify({
242
- event: 'configure',
243
- args: configBuilder.build(false)
244
- }));
245
- this.addon.config = new Configuration(configBuilder.build(true));
246
- });
247
-
248
- this.socket.on('error', (error) => {
249
- if (error.message.includes('Failed to connect')) {
250
- throw new Error('OGI Addon Server is not running/is unreachable. Please start the server and try again.');
251
- }
252
- console.error('An error occurred:', error);
253
- })
254
-
255
- this.socket.on('close', (code, reason) => {
256
- if (code === 1008) {
257
- console.error('Authentication failed:', reason);
258
- return;
259
- }
260
- this.eventEmitter.emit('disconnect', reason);
261
- console.log("Disconnected from OGI Addon Server")
262
- console.error(reason.toString())
263
- this.eventEmitter.emit('exit');
264
- this.socket.close();
265
- });
266
-
267
- this.registerMessageReceiver();
268
- }
269
-
270
- private async userInputAsked(configBuilt: ConfigurationBuilder, name: string, description: string, socket: WebSocket): Promise<{ [key: string]: number | boolean | string }> {
271
- const config = configBuilt.build(false);
272
- const id = Math.random().toString(36).substring(7);
273
- if (!socket) {
274
- return {};
275
- }
276
- socket.send(JSON.stringify({
277
- event: 'input-asked',
278
- args: {
279
- config,
280
- name,
281
- description
282
- },
283
- id: id
284
- }));
285
- return await this.waitForResponseFromServer(id);
286
- }
287
-
288
- private registerMessageReceiver() {
289
- this.socket.on('message', async (data: string) => {
290
- const message: WebsocketMessageServer = JSON.parse(data);
291
- switch (message.event) {
292
- case 'config-update':
293
- const result = this.addon.config.updateConfig(message.args);
294
- if (!result[0]) {
295
- this.respondToMessage(message.id!!, { success: false, error: result[1] });
296
- }
297
- else {
298
- this.respondToMessage(message.id!!, { success: true });
299
- }
300
- break
301
- case 'search':
302
- let searchResultEvent = new EventResponse<SearchResult[]>((screen, name, description) => this.userInputAsked(screen, name, description, this.socket));
303
- this.eventEmitter.emit('search', message.args, searchResultEvent);
304
- const searchResult = await this.waitForEventToRespond(searchResultEvent);
305
- this.respondToMessage(message.id!!, searchResult.data);
306
- break
307
- case 'setup':
308
- let setupEvent = new EventResponse<LibraryInfo>((screen, name, description) => this.userInputAsked(screen, name, description, this.socket));
309
- 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);
310
- const interval = setInterval(() => {
311
- if (setupEvent.resolved) {
312
- clearInterval(interval);
313
- return;
314
- }
315
- this.send('defer-update', {
316
- logs: setupEvent.logs,
317
- deferID: message.args.deferID,
318
- progress: setupEvent.progress
319
- } as any);
320
- }, 100);
321
- const setupResult = await this.waitForEventToRespond(setupEvent);
322
- this.respondToMessage(message.id!!, setupResult.data);
323
- break
324
- case 'library-search':
325
- let librarySearchEvent = new EventResponse<BasicLibraryInfo[]>((screen, name, description) => this.userInputAsked(screen, name, description, this.socket));
326
- if (this.eventEmitter.listenerCount('game-details') === 0) {
327
- this.respondToMessage(message.id!!, []);
328
- break;
329
- }
330
- this.eventEmitter.emit('library-search', message.args, librarySearchEvent);
331
- const librarySearchResult = await this.waitForEventToRespond(librarySearchEvent);
332
- this.respondToMessage(message.id!!, librarySearchResult.data);
333
- break
334
- case 'game-details':
335
- let gameDetailsEvent = new EventResponse<StoreData>((screen, name, description) => this.userInputAsked(screen, name, description, this.socket));
336
- if (this.eventEmitter.listenerCount('game-details') === 0) {
337
- this.respondToMessage(message.id!!, { error: 'No event listener for game-details' });
338
- break;
339
- }
340
- this.eventEmitter.emit('game-details', message.args, gameDetailsEvent);
341
- const gameDetailsResult = await this.waitForEventToRespond(gameDetailsEvent);
342
- this.respondToMessage(message.id!!, gameDetailsResult.data);
343
- break
344
- case 'request-dl':
345
- let requestDLEvent = new EventResponse<SearchResult>((screen, name, description) => this.userInputAsked(screen, name, description, this.socket));
346
- if (this.eventEmitter.listenerCount('request-dl') === 0) {
347
- this.respondToMessage(message.id!!, { error: 'No event listener for request-dl' });
348
- break;
349
- }
350
- this.eventEmitter.emit('request-dl', message.args.appID, message.args.info, requestDLEvent);
351
- const requestDLResult = await this.waitForEventToRespond(requestDLEvent);
352
- if (requestDLEvent.data === null || requestDLEvent.data?.downloadType === 'request') {
353
- 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.');
354
- }
355
- this.respondToMessage(message.id!!, requestDLResult.data);
356
- break
357
- }
358
- });
359
- }
360
-
361
- private waitForEventToRespond<T>(event: EventResponse<T>): Promise<EventResponse<T>> {
362
- // check the handlers to see if there even is any
363
- return new Promise((resolve, reject) => {
364
- const dataGet = setInterval(() => {
365
- if (event.resolved) {
366
- resolve(event);
367
- clearTimeout(timeout);
368
- }
369
- }, 5);
370
-
371
- const timeout = setTimeout(() => {
372
- if (event.deffered) {
373
- clearInterval(dataGet);
374
- const interval = setInterval(() => {
375
- if (event.resolved) {
376
- clearInterval(interval);
377
- resolve(event);
378
- }
379
- }, 100);
380
- }
381
- else {
382
- reject('Event did not respond in time');
383
- }
384
- }, 5000)
385
- });
386
- }
387
-
388
- public respondToMessage(messageID: string, response: any) {
389
- this.socket.send(JSON.stringify({
390
- event: 'response',
391
- id: messageID,
392
- args: response
393
- }));
394
- console.log("dispatched response to " + messageID)
395
- }
396
-
397
- public waitForResponseFromServer<T>(messageID: string): Promise<T> {
398
- return new Promise((resolve) => {
399
- const waiter = (data: string) => {
400
- const message: WebsocketMessageClient = JSON.parse(data);
401
- if (message.event !== 'response') {
402
- this.socket.once('message', waiter);
403
- return;
404
- }
405
- console.log("received response from " + messageID)
406
-
407
- if (message.id === messageID) {
408
- resolve(message.args);
409
- }
410
- else {
411
- this.socket.once('message', waiter);
412
- }
413
- }
414
- this.socket.once('message', waiter);
415
- });
416
- }
417
-
418
- public send(event: OGIAddonClientSentEvent, args: Parameters<ClientSentEventTypes[OGIAddonClientSentEvent]>) {
419
- this.socket.send(JSON.stringify({
420
- event,
421
- args
422
- }));
423
- }
424
-
425
- public close() {
426
- this.socket.close();
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
+ }