ogi-addon 3.0.0 → 4.0.0
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/build/SearchEngine.d.cts +2 -31
- package/build/SearchEngine.d.mts +2 -31
- package/build/config/Configuration.cjs.map +1 -1
- package/build/config/Configuration.d.cts +5 -4
- package/build/config/Configuration.d.mts +5 -4
- package/build/config/Configuration.mjs.map +1 -1
- package/build/config/ConfigurationBuilder.cjs +2 -2
- package/build/config/ConfigurationBuilder.cjs.map +1 -1
- package/build/config/ConfigurationBuilder.d.cts +13 -15
- package/build/config/ConfigurationBuilder.d.mts +13 -15
- package/build/config/ConfigurationBuilder.mjs +2 -2
- package/build/config/ConfigurationBuilder.mjs.map +1 -1
- package/build/extraction.cjs +80 -0
- package/build/extraction.cjs.map +1 -0
- package/build/extraction.d.cts +5 -0
- package/build/extraction.d.mts +5 -0
- package/build/extraction.mjs +78 -0
- package/build/extraction.mjs.map +1 -0
- package/build/main.cjs +91 -169
- package/build/main.cjs.map +1 -1
- package/build/main.d.cts +27 -409
- package/build/main.d.mts +27 -409
- package/build/main.mjs +91 -168
- package/build/main.mjs.map +1 -1
- package/package.json +4 -4
- package/src/SearchEngine.ts +1 -34
- package/src/config/Configuration.ts +6 -8
- package/src/config/ConfigurationBuilder.ts +47 -30
- package/src/extraction.ts +87 -0
- package/src/main.ts +358 -765
- package/tsconfig.json +1 -1
package/src/main.ts
CHANGED
|
@@ -1,400 +1,85 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { EventResponseSocket, randomMessageId } from '@ogi-sdk/connect';
|
|
2
|
+
import type {
|
|
3
|
+
AddonClientToServerEventArgs,
|
|
4
|
+
AddonClientToServerEventName,
|
|
5
|
+
AddonClientToServerWebsocketMessage,
|
|
6
|
+
AddonNotificationMessage,
|
|
7
|
+
AddonProtocolEventListenerTypes,
|
|
8
|
+
AddonSDKLifecycleEventListenerTypes,
|
|
9
|
+
AddonServerToClientWebsocketMessage,
|
|
10
|
+
BasicLibraryInfo,
|
|
11
|
+
CatalogResponse,
|
|
12
|
+
LibraryInfo,
|
|
13
|
+
OGIAddonConfiguration,
|
|
14
|
+
OGIAddonSDKEventListener,
|
|
15
|
+
SearchResult,
|
|
16
|
+
SetupResponse,
|
|
17
|
+
StoreData,
|
|
18
|
+
AddonTaskRunEventArgs,
|
|
19
|
+
} from '@ogi-sdk/connect';
|
|
2
20
|
import events from 'node:events';
|
|
3
21
|
import { ConfigurationBuilder } from './config/ConfigurationBuilder';
|
|
4
|
-
import
|
|
5
|
-
import { Configuration } from './config/Configuration';
|
|
22
|
+
import { Configuration, DefiniteConfig } from './config/Configuration';
|
|
6
23
|
import EventResponse from './EventResponse';
|
|
7
|
-
import type { SearchResult } from './SearchEngine';
|
|
8
24
|
import Fuse, { IFuseOptions } from 'fuse.js';
|
|
9
25
|
|
|
10
|
-
/**
|
|
11
|
-
* Exposed events that the programmer can use to listen to and emit events.
|
|
12
|
-
*/
|
|
13
|
-
export type OGIAddonEvent =
|
|
14
|
-
| 'connect'
|
|
15
|
-
| 'disconnect'
|
|
16
|
-
| 'configure'
|
|
17
|
-
| 'authenticate'
|
|
18
|
-
| 'search'
|
|
19
|
-
| 'setup'
|
|
20
|
-
| 'library-search'
|
|
21
|
-
| 'game-details'
|
|
22
|
-
| 'exit'
|
|
23
|
-
| 'check-for-updates'
|
|
24
|
-
| 'request-dl'
|
|
25
|
-
| 'catalog'
|
|
26
|
-
| 'launch-app';
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* The events that the client can send to the server and are handled by the server.
|
|
30
|
-
*/
|
|
31
|
-
export type OGIAddonClientSentEvent =
|
|
32
|
-
| 'response'
|
|
33
|
-
| 'authenticate'
|
|
34
|
-
| 'configure'
|
|
35
|
-
| 'defer-update'
|
|
36
|
-
| 'notification'
|
|
37
|
-
| 'input-asked'
|
|
38
|
-
| 'get-app-details'
|
|
39
|
-
| 'search-app-name'
|
|
40
|
-
| 'flag'
|
|
41
|
-
| 'task-update';
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* The events that the server sends to the client
|
|
45
|
-
* This is the events that the server can send to the client and are handled by the client.
|
|
46
|
-
*/
|
|
47
|
-
export type OGIAddonServerSentEvent =
|
|
48
|
-
| 'authenticate'
|
|
49
|
-
| 'configure'
|
|
50
|
-
| 'config-update'
|
|
51
|
-
| 'launch-app'
|
|
52
|
-
| 'search'
|
|
53
|
-
| 'setup'
|
|
54
|
-
| 'response'
|
|
55
|
-
| 'library-search'
|
|
56
|
-
| 'check-for-updates'
|
|
57
|
-
| 'task-run'
|
|
58
|
-
| 'game-details'
|
|
59
|
-
| 'request-dl'
|
|
60
|
-
| 'catalog';
|
|
61
26
|
export { ConfigurationBuilder, Configuration, EventResponse };
|
|
62
|
-
export
|
|
27
|
+
export { extraction };
|
|
63
28
|
const defaultPort = 7654;
|
|
64
29
|
import pjson from '../package.json';
|
|
65
|
-
import { exec, spawn } from 'node:child_process';
|
|
66
|
-
import fs from 'node:fs';
|
|
67
30
|
import { z } from 'zod';
|
|
31
|
+
import { extraction } from './extraction';
|
|
68
32
|
export const VERSION = pjson.version;
|
|
69
33
|
|
|
70
|
-
export
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
'input-asked': ConfigurationBuilder<
|
|
86
|
-
Record<string, string | number | boolean>
|
|
87
|
-
>;
|
|
88
|
-
'task-update': {
|
|
89
|
-
id: string;
|
|
90
|
-
progress: number;
|
|
91
|
-
logs: string[];
|
|
92
|
-
finished: boolean;
|
|
93
|
-
failed: string | undefined;
|
|
94
|
-
};
|
|
95
|
-
'get-app-details': {
|
|
96
|
-
appID: number;
|
|
97
|
-
storefront: string;
|
|
98
|
-
};
|
|
99
|
-
'search-app-name': {
|
|
100
|
-
query: string;
|
|
101
|
-
storefront: string;
|
|
102
|
-
};
|
|
103
|
-
flag: {
|
|
104
|
-
flag: string;
|
|
105
|
-
value: string | string[];
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
export type BasicLibraryInfo = {
|
|
110
|
-
name: string;
|
|
111
|
-
capsuleImage: string;
|
|
112
|
-
appID: number;
|
|
113
|
-
storefront: string;
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
export interface CatalogSection {
|
|
117
|
-
name: string;
|
|
118
|
-
description: string;
|
|
119
|
-
listings: BasicLibraryInfo[];
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export interface CatalogCarouselItem {
|
|
123
|
-
name: string;
|
|
124
|
-
description: string;
|
|
125
|
-
carouselImage: string;
|
|
126
|
-
fullBannerImage?: string;
|
|
127
|
-
appID?: number;
|
|
128
|
-
storefront?: string;
|
|
129
|
-
capsuleImage?: string;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
export interface CatalogWithCarousel {
|
|
133
|
-
sections: Record<string, CatalogSection>;
|
|
134
|
-
carousel?: Record<string, CatalogCarouselItem> | CatalogCarouselItem[];
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
export type CatalogResponse =
|
|
138
|
-
| Record<string, CatalogSection>
|
|
139
|
-
| CatalogWithCarousel;
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* UMU ID format: 'steam:${number}' or 'umu:${string | number}'
|
|
143
|
-
* - steam:${number} → maps to umu-${number} for Steam games
|
|
144
|
-
* - umu:${string | number} → maps to umu-${string | number} for non-Steam games
|
|
145
|
-
*/
|
|
146
|
-
export type UmuId = `steam:${number}` | `umu:${string | number}`;
|
|
147
|
-
|
|
148
|
-
export type SetupEventResponse = Omit<
|
|
34
|
+
export type {
|
|
35
|
+
AddonClientToServerEventArgs,
|
|
36
|
+
AddonClientToServerEventName,
|
|
37
|
+
AddonClientToServerWebsocketMessage,
|
|
38
|
+
AddonNotificationMessage,
|
|
39
|
+
AddonServerToClientEventName,
|
|
40
|
+
AddonServerToClientWebsocketMessage,
|
|
41
|
+
BasicLibraryInfo,
|
|
42
|
+
CatalogCarouselItem,
|
|
43
|
+
CatalogResponse,
|
|
44
|
+
CatalogSection,
|
|
45
|
+
CatalogWithCarousel,
|
|
46
|
+
ConfigurationFile,
|
|
47
|
+
ConfigurationOptionType,
|
|
48
|
+
ConfigurationOptionWire,
|
|
149
49
|
LibraryInfo,
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
* UMU ID for the game. Format: 'steam:${number}' or 'umu:${string | number}'
|
|
168
|
-
* - steam:${number} → maps to umu-${number} for Steam games
|
|
169
|
-
* - umu:${string | number} → maps to umu-${string | number} for non-Steam games
|
|
170
|
-
*/
|
|
171
|
-
umuId: UmuId;
|
|
172
|
-
/**
|
|
173
|
-
* Optional DLL overrides. Can be WINEDLLOVERRIDES-style (e.g. "dinput8=n,b") or bare DLL names.
|
|
174
|
-
* Bare names get "=n,b" inferred; entries that already include "=..." are used as-is.
|
|
175
|
-
*/
|
|
176
|
-
dllOverrides?: string[];
|
|
177
|
-
/**
|
|
178
|
-
* Optional Proton version to use (e.g., 'GE-Proton9-5', 'GE-Proton')
|
|
179
|
-
* If not specified, uses latest UMU-Proton
|
|
180
|
-
*/
|
|
181
|
-
protonVersion?: string;
|
|
182
|
-
/**
|
|
183
|
-
* Optional store identifier for protonfixes (e.g., 'gog', 'egs', 'none')
|
|
184
|
-
*/
|
|
185
|
-
store?: string;
|
|
186
|
-
/**
|
|
187
|
-
* Cached Steam shortcut app ID after adding UMU game to Steam (avoids re-adding on each launch)
|
|
188
|
-
*/
|
|
189
|
-
steamShortcutId?: number;
|
|
190
|
-
};
|
|
191
|
-
};
|
|
192
|
-
|
|
193
|
-
export interface EventListenerTypes {
|
|
194
|
-
/**
|
|
195
|
-
* This event is emitted when the addon connects to the OGI Addon Server. Addon does not need to resolve anything.
|
|
196
|
-
* @param event
|
|
197
|
-
* @returns
|
|
198
|
-
*/
|
|
199
|
-
connect: (event: EventResponse<void>) => void;
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* 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.
|
|
203
|
-
* @param reason
|
|
204
|
-
* @returns
|
|
205
|
-
*/
|
|
206
|
-
disconnect: (reason: string) => void;
|
|
207
|
-
/**
|
|
208
|
-
* 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)
|
|
209
|
-
* @param config
|
|
210
|
-
* @returns
|
|
211
|
-
*/
|
|
212
|
-
configure: (config: ConfigurationBuilder) => ConfigurationBuilder;
|
|
213
|
-
/**
|
|
214
|
-
* This event is called when the client provides a response to any event. This should be treated as middleware.
|
|
215
|
-
* @param response
|
|
216
|
-
* @returns
|
|
217
|
-
*/
|
|
218
|
-
response: (response: any) => void;
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* This event is called when the client requests for the addon to authenticate itself. You don't need to provide any info.
|
|
222
|
-
* @param config
|
|
223
|
-
* @returns
|
|
224
|
-
*/
|
|
225
|
-
authenticate: (config: any) => void;
|
|
226
|
-
/**
|
|
227
|
-
* 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)
|
|
228
|
-
* @param query
|
|
229
|
-
* @param event
|
|
230
|
-
* @returns
|
|
231
|
-
*/
|
|
232
|
-
search: (
|
|
233
|
-
query: {
|
|
234
|
-
storefront: string;
|
|
235
|
-
appID: number;
|
|
236
|
-
} & (
|
|
237
|
-
| {
|
|
238
|
-
for: 'game' | 'task' | 'all';
|
|
239
|
-
}
|
|
240
|
-
| {
|
|
241
|
-
for: 'update';
|
|
242
|
-
libraryInfo: LibraryInfo;
|
|
243
|
-
}
|
|
244
|
-
),
|
|
245
|
-
event: EventResponse<SearchResult[]>
|
|
246
|
-
) => void;
|
|
247
|
-
/**
|
|
248
|
-
* 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)
|
|
249
|
-
* @param data
|
|
250
|
-
* @param event
|
|
251
|
-
* @returns
|
|
252
|
-
*/
|
|
253
|
-
setup: (
|
|
254
|
-
data: {
|
|
255
|
-
path: string;
|
|
256
|
-
type: 'direct' | 'torrent' | 'magnet' | 'empty';
|
|
257
|
-
name: string;
|
|
258
|
-
usedRealDebrid: boolean;
|
|
259
|
-
clearOldFilesBeforeUpdate?: boolean;
|
|
260
|
-
multiPartFiles?: {
|
|
261
|
-
name: string;
|
|
262
|
-
downloadURL: string;
|
|
263
|
-
}[];
|
|
264
|
-
appID: number;
|
|
265
|
-
storefront: string;
|
|
266
|
-
manifest?: Record<string, unknown>;
|
|
267
|
-
} & (
|
|
268
|
-
| {
|
|
269
|
-
for: 'game';
|
|
270
|
-
}
|
|
271
|
-
| {
|
|
272
|
-
for: 'update';
|
|
273
|
-
currentLibraryInfo: LibraryInfo;
|
|
274
|
-
}
|
|
275
|
-
),
|
|
276
|
-
event: EventResponse<SetupEventResponse>
|
|
277
|
-
) => void;
|
|
278
|
-
|
|
279
|
-
/**
|
|
280
|
-
* This event is emitted when the client requires for a search to be performed. Input is the search query.
|
|
281
|
-
* @param query
|
|
282
|
-
* @param event
|
|
283
|
-
* @returns
|
|
284
|
-
*/
|
|
285
|
-
'library-search': (
|
|
286
|
-
query: string,
|
|
287
|
-
event: EventResponse<BasicLibraryInfo[]>
|
|
288
|
-
) => void;
|
|
289
|
-
|
|
290
|
-
/**
|
|
291
|
-
* This event is emitted when the client requests for a game details to be fetched. Addon should resolve the event with the game details. This is used to generate a store page for the game.
|
|
292
|
-
* @param appID
|
|
293
|
-
* @param event
|
|
294
|
-
* @returns
|
|
295
|
-
*/
|
|
296
|
-
'game-details': (
|
|
297
|
-
details: { appID: number; storefront: string },
|
|
298
|
-
event: EventResponse<StoreData | undefined>
|
|
299
|
-
) => void;
|
|
300
|
-
|
|
301
|
-
/**
|
|
302
|
-
* This event is emitted when the client requests for the addon to exit. Use this to perform any cleanup tasks, ending with a `process.exit(0)`.
|
|
303
|
-
* @returns
|
|
304
|
-
*/
|
|
305
|
-
exit: () => void;
|
|
306
|
-
|
|
307
|
-
/**
|
|
308
|
-
* This event is emitted when the client requests for a download to be performed with the 'request' type. Addon should resolve the event with a SearchResult containing the actual download info.
|
|
309
|
-
* @param appID
|
|
310
|
-
* @param info
|
|
311
|
-
* @param event
|
|
312
|
-
* @returns
|
|
313
|
-
*/
|
|
314
|
-
'request-dl': (
|
|
315
|
-
appID: number,
|
|
316
|
-
info: SearchResult,
|
|
317
|
-
event: EventResponse<SearchResult>
|
|
318
|
-
) => void;
|
|
319
|
-
|
|
320
|
-
/**
|
|
321
|
-
* This event is emitted when the client requests for a catalog to be fetched. Addon should resolve the event with the catalog.
|
|
322
|
-
* @param event
|
|
323
|
-
* @returns
|
|
324
|
-
*/
|
|
325
|
-
catalog: (event: Omit<EventResponse<CatalogResponse>, 'askForInput'>) => void;
|
|
326
|
-
|
|
327
|
-
/**
|
|
328
|
-
* This event is emitted when the client requests for an addon to check for updates. Addon should resolve the event with the update information.
|
|
329
|
-
* @param data
|
|
330
|
-
* @param event
|
|
331
|
-
* @returns
|
|
332
|
-
*/
|
|
333
|
-
'check-for-updates': (
|
|
334
|
-
data: { appID: number; storefront: string; currentVersion: string },
|
|
335
|
-
event: EventResponse<
|
|
336
|
-
| {
|
|
337
|
-
available: true;
|
|
338
|
-
version: string;
|
|
339
|
-
}
|
|
340
|
-
| {
|
|
341
|
-
available: false;
|
|
342
|
-
}
|
|
343
|
-
>
|
|
344
|
-
) => void;
|
|
345
|
-
|
|
346
|
-
/**
|
|
347
|
-
* This event is emitted when the client is going to launch an app. Addon should use this to perform any pre or post launch tasks.
|
|
348
|
-
* @param data {LibraryInfo} The library information for the app to be launched.
|
|
349
|
-
* @param launchType { 'pre' | 'post' } The type of launch task to perform.
|
|
350
|
-
* @param event {EventResponse<void>} The event response from the server.
|
|
351
|
-
*/
|
|
352
|
-
'launch-app': (
|
|
353
|
-
data: { libraryInfo: LibraryInfo; launchType: 'pre' | 'post' },
|
|
354
|
-
event: EventResponse<void>
|
|
355
|
-
) => void;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
export interface StoreData {
|
|
359
|
-
name: string;
|
|
360
|
-
publishers: string[];
|
|
361
|
-
developers: string[];
|
|
362
|
-
appID: number;
|
|
363
|
-
releaseDate: string;
|
|
364
|
-
capsuleImage: string;
|
|
365
|
-
coverImage: string;
|
|
366
|
-
basicDescription: string;
|
|
367
|
-
description: string;
|
|
368
|
-
headerImage: string;
|
|
369
|
-
latestVersion: string;
|
|
370
|
-
}
|
|
371
|
-
export interface WebsocketMessageClient {
|
|
372
|
-
event: OGIAddonClientSentEvent;
|
|
373
|
-
id?: string;
|
|
374
|
-
args: any;
|
|
375
|
-
statusError?: string;
|
|
376
|
-
}
|
|
377
|
-
export interface WebsocketMessageServer {
|
|
378
|
-
event: OGIAddonServerSentEvent;
|
|
379
|
-
id?: string;
|
|
380
|
-
args: any;
|
|
381
|
-
statusError?: string;
|
|
382
|
-
}
|
|
50
|
+
OGIAddonConfiguration,
|
|
51
|
+
OGIAddonSDKEventListener,
|
|
52
|
+
SearchResult,
|
|
53
|
+
SetupResponse,
|
|
54
|
+
SetupEventResponse,
|
|
55
|
+
StoreData,
|
|
56
|
+
UmuId,
|
|
57
|
+
SetupCommandData,
|
|
58
|
+
AddonProtocolEventListenerTypes,
|
|
59
|
+
AddonSDKLifecycleEventListenerTypes,
|
|
60
|
+
AddonServerHostEventListeners,
|
|
61
|
+
AddonServerHostEventName,
|
|
62
|
+
AddonServerLifecycleEvent,
|
|
63
|
+
} from '@ogi-sdk/connect';
|
|
64
|
+
|
|
65
|
+
/** @deprecated Use {@link AddonNotificationMessage}. */
|
|
66
|
+
export type Notification = AddonNotificationMessage;
|
|
383
67
|
|
|
384
68
|
/**
|
|
385
|
-
*
|
|
386
|
-
*
|
|
69
|
+
* Addon SDK listener signatures. Protocol commands come from `addonProtocol` in
|
|
70
|
+
* `@ogi-sdk/connect`; lifecycle and builder-specific hooks are merged below.
|
|
387
71
|
*/
|
|
388
|
-
export
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
72
|
+
export type EventListenerTypes = AddonSDKLifecycleEventListenerTypes<
|
|
73
|
+
EventResponse<unknown>
|
|
74
|
+
> &
|
|
75
|
+
AddonProtocolEventListenerTypes<
|
|
76
|
+
EventResponse<unknown>,
|
|
77
|
+
'authenticate' | 'configure' | 'catalog'
|
|
78
|
+
> & {
|
|
79
|
+
authenticate: (config: unknown) => void;
|
|
80
|
+
configure: (config: ConfigurationBuilder) => ConfigurationBuilder;
|
|
81
|
+
catalog: (event: Omit<EventResponse<CatalogResponse>, 'askForInput'>) => void;
|
|
82
|
+
};
|
|
398
83
|
|
|
399
84
|
/**
|
|
400
85
|
* 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.
|
|
@@ -416,7 +101,7 @@ export default class OGIAddon {
|
|
|
416
101
|
public addonWSListener: OGIAddonWSListener;
|
|
417
102
|
public addonInfo: OGIAddonConfiguration;
|
|
418
103
|
public config: Configuration = new Configuration({});
|
|
419
|
-
private eventsAvailable:
|
|
104
|
+
private eventsAvailable: OGIAddonSDKEventListener[] = [];
|
|
420
105
|
private registeredConnectEvent: boolean = false;
|
|
421
106
|
private taskHandlers: Map<
|
|
422
107
|
string,
|
|
@@ -438,10 +123,10 @@ export default class OGIAddon {
|
|
|
438
123
|
|
|
439
124
|
/**
|
|
440
125
|
* Register an event listener for the addon. (See EventListenerTypes)
|
|
441
|
-
* @param event {
|
|
442
|
-
* @param listener {EventListenerTypes[
|
|
126
|
+
* @param event {OGIAddonSDKEventListener}
|
|
127
|
+
* @param listener {EventListenerTypes[OGIAddonSDKEventListener]}
|
|
443
128
|
*/
|
|
444
|
-
public on<T extends
|
|
129
|
+
public on<T extends OGIAddonSDKEventListener>(
|
|
445
130
|
event: T,
|
|
446
131
|
listener: EventListenerTypes[T]
|
|
447
132
|
) {
|
|
@@ -459,7 +144,7 @@ export default class OGIAddon {
|
|
|
459
144
|
}
|
|
460
145
|
}
|
|
461
146
|
|
|
462
|
-
public emit<T extends
|
|
147
|
+
public emit<T extends OGIAddonSDKEventListener>(
|
|
463
148
|
event: T,
|
|
464
149
|
...args: Parameters<EventListenerTypes[T]>
|
|
465
150
|
) {
|
|
@@ -470,7 +155,7 @@ export default class OGIAddon {
|
|
|
470
155
|
* Notify the client using a notification. Provide the type of notification, the message, and an ID.
|
|
471
156
|
* @param notification {Notification}
|
|
472
157
|
*/
|
|
473
|
-
public notify(notification:
|
|
158
|
+
public notify(notification: AddonNotificationMessage) {
|
|
474
159
|
this.addonWSListener.send('notification', [notification]);
|
|
475
160
|
}
|
|
476
161
|
|
|
@@ -481,23 +166,23 @@ export default class OGIAddon {
|
|
|
481
166
|
* @returns {Promise<StoreData>}
|
|
482
167
|
*/
|
|
483
168
|
public async getAppDetails(appID: number, storefront: string) {
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
169
|
+
return await this.addonWSListener.requestResponse<StoreData | undefined>(
|
|
170
|
+
'get-app-details',
|
|
171
|
+
{
|
|
172
|
+
appID,
|
|
173
|
+
storefront,
|
|
174
|
+
}
|
|
175
|
+
);
|
|
491
176
|
}
|
|
492
177
|
|
|
493
178
|
public async searchGame(query: string, storefront: string) {
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
179
|
+
return await this.addonWSListener.requestResponse<BasicLibraryInfo[]>(
|
|
180
|
+
'search-app-name',
|
|
181
|
+
{
|
|
182
|
+
query,
|
|
183
|
+
storefront,
|
|
184
|
+
}
|
|
185
|
+
);
|
|
501
186
|
}
|
|
502
187
|
|
|
503
188
|
/**
|
|
@@ -581,117 +266,10 @@ export default class OGIAddon {
|
|
|
581
266
|
* Extract a file using 7-Zip on Windows, unzip on Linux/Mac.
|
|
582
267
|
* @param path {string}
|
|
583
268
|
* @param outputPath {string}
|
|
584
|
-
* @param type {'unrar' | 'unzip'}
|
|
585
269
|
* @returns {Promise<void>}
|
|
586
270
|
*/
|
|
587
|
-
public async extractFile(
|
|
588
|
-
path
|
|
589
|
-
outputPath: string,
|
|
590
|
-
type: 'unrar' | 'unzip'
|
|
591
|
-
) {
|
|
592
|
-
return new Promise<void>((resolve, reject) => {
|
|
593
|
-
// Ensure outputPath exists
|
|
594
|
-
if (!fs.existsSync(outputPath)) {
|
|
595
|
-
fs.mkdirSync(outputPath, { recursive: true });
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
if (type === 'unzip') {
|
|
599
|
-
// Prefer 7-Zip on Windows, unzip on Linux/Mac
|
|
600
|
-
if (process.platform === 'win32') {
|
|
601
|
-
// 7-Zip path (default install location)
|
|
602
|
-
const s7ZipPath = '"C:\\Program Files\\7-Zip\\7z.exe"';
|
|
603
|
-
exec(
|
|
604
|
-
`${s7ZipPath} x "${path}" -o"${outputPath}"`,
|
|
605
|
-
(err: any, stdout: any, stderr: any) => {
|
|
606
|
-
if (err) {
|
|
607
|
-
console.error(err);
|
|
608
|
-
console.log(stderr);
|
|
609
|
-
reject(new Error('Failed to extract ZIP file'));
|
|
610
|
-
return;
|
|
611
|
-
}
|
|
612
|
-
console.log(stdout);
|
|
613
|
-
console.log(stderr);
|
|
614
|
-
resolve();
|
|
615
|
-
}
|
|
616
|
-
);
|
|
617
|
-
} else {
|
|
618
|
-
// Use unzip on Linux/Mac
|
|
619
|
-
const unzipProcess = spawn(
|
|
620
|
-
'unzip',
|
|
621
|
-
[
|
|
622
|
-
'-o', // overwrite files without prompting
|
|
623
|
-
path,
|
|
624
|
-
'-d', // specify output directory
|
|
625
|
-
outputPath,
|
|
626
|
-
],
|
|
627
|
-
{
|
|
628
|
-
env: {
|
|
629
|
-
...process.env,
|
|
630
|
-
UNZIP_DISABLE_ZIPBOMB_DETECTION: 'TRUE',
|
|
631
|
-
},
|
|
632
|
-
}
|
|
633
|
-
);
|
|
634
|
-
|
|
635
|
-
unzipProcess.stdout.on('data', (data: Buffer) => {
|
|
636
|
-
console.log(`[unzip stdout]: ${data}`);
|
|
637
|
-
});
|
|
638
|
-
|
|
639
|
-
unzipProcess.stderr.on('data', (data: Buffer) => {
|
|
640
|
-
console.error(`[unzip stderr]: ${data}`);
|
|
641
|
-
});
|
|
642
|
-
|
|
643
|
-
unzipProcess.on('close', (code: number) => {
|
|
644
|
-
if (code !== 0) {
|
|
645
|
-
console.error(`unzip process exited with code ${code}`);
|
|
646
|
-
reject(new Error('Failed to extract ZIP file'));
|
|
647
|
-
return;
|
|
648
|
-
}
|
|
649
|
-
resolve();
|
|
650
|
-
});
|
|
651
|
-
}
|
|
652
|
-
} else if (type === 'unrar') {
|
|
653
|
-
if (process.platform === 'win32') {
|
|
654
|
-
// 7-Zip path (default install location)
|
|
655
|
-
const s7ZipPath = '"C:\\Program Files\\7-Zip\\7z.exe"';
|
|
656
|
-
exec(
|
|
657
|
-
`${s7ZipPath} x "${path}" -o"${outputPath}"`,
|
|
658
|
-
(err: any, stdout: any, stderr: any) => {
|
|
659
|
-
if (err) {
|
|
660
|
-
console.error(err);
|
|
661
|
-
console.log(stderr);
|
|
662
|
-
reject(new Error('Failed to extract RAR file'));
|
|
663
|
-
return;
|
|
664
|
-
}
|
|
665
|
-
console.log(stdout);
|
|
666
|
-
console.log(stderr);
|
|
667
|
-
resolve();
|
|
668
|
-
}
|
|
669
|
-
);
|
|
670
|
-
} else {
|
|
671
|
-
// Use unrar on Linux/Mac
|
|
672
|
-
const unrarProcess = spawn('unrar', ['x', '-y', path, outputPath]);
|
|
673
|
-
|
|
674
|
-
unrarProcess.stdout.on('data', (data: Buffer) => {
|
|
675
|
-
console.log(`[unrar stdout]: ${data}`);
|
|
676
|
-
});
|
|
677
|
-
|
|
678
|
-
unrarProcess.stderr.on('data', (data: Buffer) => {
|
|
679
|
-
console.error(`[unrar stderr]: ${data}`);
|
|
680
|
-
});
|
|
681
|
-
|
|
682
|
-
unrarProcess.on('close', (code: number) => {
|
|
683
|
-
if (code !== 0) {
|
|
684
|
-
console.error(`unrar process exited with code ${code}`);
|
|
685
|
-
reject(new Error('Failed to extract RAR file'));
|
|
686
|
-
return;
|
|
687
|
-
}
|
|
688
|
-
resolve();
|
|
689
|
-
});
|
|
690
|
-
}
|
|
691
|
-
} else {
|
|
692
|
-
reject(new Error('Unknown extraction type'));
|
|
693
|
-
}
|
|
694
|
-
});
|
|
271
|
+
public async extractFile(path: string, outputPath: string) {
|
|
272
|
+
return await extraction(path, outputPath);
|
|
695
273
|
}
|
|
696
274
|
}
|
|
697
275
|
|
|
@@ -879,7 +457,7 @@ export class SearchTool<T> {
|
|
|
879
457
|
/**
|
|
880
458
|
* Library Info is the metadata for a library entry after setting up a game.
|
|
881
459
|
*/
|
|
882
|
-
export const ZodLibraryInfo = z.object({
|
|
460
|
+
export const ZodLibraryInfo: z.ZodType<LibraryInfo> = z.object({
|
|
883
461
|
name: z.string(),
|
|
884
462
|
version: z.string(),
|
|
885
463
|
cwd: z.string(),
|
|
@@ -922,36 +500,52 @@ export const ZodLibraryInfo = z.object({
|
|
|
922
500
|
)
|
|
923
501
|
.optional(),
|
|
924
502
|
});
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
message: string;
|
|
929
|
-
id: string;
|
|
930
|
-
}
|
|
503
|
+
|
|
504
|
+
export type { AddonTaskRunEventArgs as TaskRunMessageArgs } from '@ogi-sdk/connect';
|
|
505
|
+
|
|
931
506
|
class OGIAddonWSListener {
|
|
932
|
-
private socket: WebSocket
|
|
507
|
+
private socket: InstanceType<typeof globalThis.WebSocket>;
|
|
508
|
+
private transport: EventResponseSocket<
|
|
509
|
+
AddonServerToClientWebsocketMessage,
|
|
510
|
+
AddonClientToServerWebsocketMessage
|
|
511
|
+
>;
|
|
933
512
|
public eventEmitter: events.EventEmitter;
|
|
934
513
|
public addon: OGIAddon;
|
|
935
514
|
|
|
936
515
|
constructor(ogiAddon: OGIAddon, eventEmitter: events.EventEmitter) {
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
516
|
+
const secret = process.argv
|
|
517
|
+
.find((arg) => arg.startsWith('--addonSecret='))
|
|
518
|
+
?.split('=')[1];
|
|
519
|
+
if (!secret) {
|
|
940
520
|
throw new Error(
|
|
941
521
|
'No secret provided. This usually happens because the addon was not started by the OGI Addon Server.'
|
|
942
522
|
);
|
|
943
523
|
}
|
|
524
|
+
|
|
525
|
+
// get the port from the arguments
|
|
526
|
+
let port = process.argv
|
|
527
|
+
.find((arg) => arg.startsWith('--addonPort='))
|
|
528
|
+
?.split('=')[1];
|
|
529
|
+
if (!port) {
|
|
530
|
+
port = defaultPort.toString();
|
|
531
|
+
}
|
|
532
|
+
|
|
944
533
|
this.addon = ogiAddon;
|
|
945
534
|
this.eventEmitter = eventEmitter;
|
|
946
|
-
|
|
947
|
-
|
|
535
|
+
const WebSocketConstructor = globalThis.WebSocket;
|
|
536
|
+
if (!WebSocketConstructor) {
|
|
537
|
+
throw new Error('WebSocket is not available in this runtime');
|
|
538
|
+
}
|
|
539
|
+
this.socket = new WebSocketConstructor('ws://localhost:' + port);
|
|
540
|
+
this.transport = new EventResponseSocket(this.socket);
|
|
541
|
+
this.socket.addEventListener('open', () => {
|
|
948
542
|
console.log('Connected to OGI Addon Server');
|
|
949
543
|
console.log('OGI Addon Server Version:', VERSION);
|
|
950
544
|
|
|
951
545
|
// Authenticate with OGI Addon Server
|
|
952
|
-
this.send('authenticate', {
|
|
546
|
+
this.send('authenticate' as AddonClientToServerEventName, {
|
|
953
547
|
...this.addon.addonInfo,
|
|
954
|
-
secret
|
|
548
|
+
secret,
|
|
955
549
|
ogiVersion: VERSION,
|
|
956
550
|
});
|
|
957
551
|
|
|
@@ -962,59 +556,44 @@ class OGIAddonWSListener {
|
|
|
962
556
|
this.addon.config = new Configuration(configBuilder.build(true));
|
|
963
557
|
|
|
964
558
|
// wait for the config-update to be received then send connect
|
|
965
|
-
const
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
let data: string;
|
|
969
|
-
if (typeof event === 'string') {
|
|
970
|
-
data = event;
|
|
971
|
-
} else if (event instanceof Buffer) {
|
|
972
|
-
data = event.toString();
|
|
973
|
-
} else if (event && typeof (event as any).data === 'string') {
|
|
974
|
-
data = (event as any).data;
|
|
975
|
-
} else if (event && (event as any).data instanceof Buffer) {
|
|
976
|
-
data = (event as any).data.toString();
|
|
977
|
-
} else {
|
|
978
|
-
// fallback for other types
|
|
979
|
-
data = event.toString();
|
|
980
|
-
}
|
|
981
|
-
const message: WebsocketMessageServer = JSON.parse(data);
|
|
982
|
-
if (message.event === 'config-update') {
|
|
559
|
+
const unsubscribeConfigListener = this.transport.on(
|
|
560
|
+
'config-update',
|
|
561
|
+
() => {
|
|
983
562
|
console.log('Config update received');
|
|
984
|
-
|
|
563
|
+
unsubscribeConfigListener();
|
|
985
564
|
this.eventEmitter.emit(
|
|
986
565
|
'connect',
|
|
987
566
|
new EventResponse<void>((screen, name, description) => {
|
|
988
|
-
return this.userInputAsked(
|
|
989
|
-
screen,
|
|
990
|
-
name,
|
|
991
|
-
description,
|
|
992
|
-
this.socket
|
|
993
|
-
);
|
|
567
|
+
return this.userInputAsked(screen, name, description);
|
|
994
568
|
})
|
|
995
569
|
);
|
|
996
570
|
}
|
|
997
|
-
|
|
998
|
-
this.socket.on('message', configListener);
|
|
571
|
+
);
|
|
999
572
|
});
|
|
1000
573
|
|
|
1001
|
-
this.socket.
|
|
1002
|
-
|
|
574
|
+
this.socket.addEventListener('error', (event) => {
|
|
575
|
+
this.transport.rejectPendingResponses('Websocket error');
|
|
576
|
+
const message =
|
|
577
|
+
event instanceof ErrorEvent
|
|
578
|
+
? event.message
|
|
579
|
+
: event.type;
|
|
580
|
+
if (message.includes('Failed to connect')) {
|
|
1003
581
|
throw new Error(
|
|
1004
582
|
'OGI Addon Server is not running/is unreachable. Please start the server and try again.'
|
|
1005
583
|
);
|
|
1006
584
|
}
|
|
1007
|
-
console.error('An error occurred:',
|
|
585
|
+
console.error('An error occurred:', event);
|
|
1008
586
|
});
|
|
1009
587
|
|
|
1010
|
-
this.socket.
|
|
1011
|
-
|
|
1012
|
-
|
|
588
|
+
this.socket.addEventListener('close', (event) => {
|
|
589
|
+
this.transport.rejectPendingResponses('Websocket closed');
|
|
590
|
+
if (event.code === 1008) {
|
|
591
|
+
console.error('Authentication failed:', event.reason);
|
|
1013
592
|
return;
|
|
1014
593
|
}
|
|
1015
|
-
this.eventEmitter.emit('disconnect', reason);
|
|
594
|
+
this.eventEmitter.emit('disconnect', event.reason);
|
|
1016
595
|
console.log('Disconnected from OGI Addon Server');
|
|
1017
|
-
console.error(reason
|
|
596
|
+
console.error(event.reason);
|
|
1018
597
|
this.eventEmitter.emit('exit');
|
|
1019
598
|
this.socket.close();
|
|
1020
599
|
});
|
|
@@ -1027,173 +606,188 @@ class OGIAddonWSListener {
|
|
|
1027
606
|
>(
|
|
1028
607
|
configBuilt: ConfigurationBuilder<U>,
|
|
1029
608
|
name: string,
|
|
1030
|
-
description: string
|
|
1031
|
-
socket: WebSocket
|
|
609
|
+
description: string
|
|
1032
610
|
): Promise<U> {
|
|
1033
611
|
const config = configBuilt.build(false);
|
|
1034
|
-
const
|
|
1035
|
-
|
|
1036
|
-
throw new Error('Socket is not connected');
|
|
1037
|
-
}
|
|
1038
|
-
socket.send(
|
|
1039
|
-
JSON.stringify({
|
|
612
|
+
const response = await this.transport.send(
|
|
613
|
+
{
|
|
1040
614
|
event: 'input-asked',
|
|
1041
615
|
args: {
|
|
1042
616
|
config,
|
|
1043
617
|
name,
|
|
1044
618
|
description,
|
|
1045
619
|
},
|
|
1046
|
-
|
|
1047
|
-
}
|
|
620
|
+
} as AddonClientToServerWebsocketMessage,
|
|
621
|
+
{ expectResponse: true }
|
|
1048
622
|
);
|
|
1049
|
-
return
|
|
623
|
+
return response.args as U;
|
|
1050
624
|
}
|
|
1051
625
|
|
|
1052
626
|
/**
|
|
1053
627
|
* Registers the message receiver for the socket. This is used to receive messages from the server and handle them.
|
|
1054
628
|
*/
|
|
1055
629
|
private registerMessageReceiver() {
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
630
|
+
const events: AddonServerToClientWebsocketMessage['event'][] = [
|
|
631
|
+
'config-update',
|
|
632
|
+
'search',
|
|
633
|
+
'setup',
|
|
634
|
+
'library-search',
|
|
635
|
+
'game-details',
|
|
636
|
+
'check-for-updates',
|
|
637
|
+
'request-dl',
|
|
638
|
+
'catalog',
|
|
639
|
+
'task-run',
|
|
640
|
+
'launch-app',
|
|
641
|
+
];
|
|
642
|
+
|
|
643
|
+
for (const event of events) {
|
|
644
|
+
this.transport.on(event, async (message) => {
|
|
645
|
+
switch (message.event) {
|
|
646
|
+
case 'config-update':
|
|
647
|
+
const result = this.addon.config.updateConfig(
|
|
648
|
+
message.args as DefiniteConfig
|
|
649
|
+
);
|
|
650
|
+
if (!result[0]) {
|
|
651
|
+
this.respondToMessage(
|
|
652
|
+
message.id!!,
|
|
653
|
+
{
|
|
654
|
+
success: false,
|
|
655
|
+
error: result[1],
|
|
656
|
+
},
|
|
657
|
+
undefined
|
|
658
|
+
);
|
|
659
|
+
} else {
|
|
660
|
+
this.respondToMessage(message.id!!, { success: true }, undefined);
|
|
661
|
+
}
|
|
662
|
+
break;
|
|
663
|
+
case 'search':
|
|
664
|
+
await this.handleEventWithResponse<SearchResult[]>(
|
|
665
|
+
message,
|
|
666
|
+
(event) => this.eventEmitter.emit('search', message.args, event)
|
|
1069
667
|
);
|
|
1070
|
-
|
|
1071
|
-
|
|
668
|
+
break;
|
|
669
|
+
case 'setup': {
|
|
670
|
+
let setupEvent = new EventResponse<SetupResponse>(
|
|
671
|
+
(screen, name, description) =>
|
|
672
|
+
this.userInputAsked(screen, name, description)
|
|
673
|
+
);
|
|
674
|
+
this.eventEmitter.emit('setup', message.args, setupEvent);
|
|
675
|
+
const interval = setInterval(() => {
|
|
676
|
+
if (setupEvent.resolved) {
|
|
677
|
+
clearInterval(interval);
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
this.send('defer-update', {
|
|
681
|
+
logs: setupEvent.logs,
|
|
682
|
+
deferID:
|
|
683
|
+
message.args as AddonClientToServerEventArgs['defer-update']['deferID'],
|
|
684
|
+
progress: setupEvent.progress,
|
|
685
|
+
failed: setupEvent.failed,
|
|
686
|
+
} as AddonClientToServerEventArgs['defer-update']);
|
|
687
|
+
}, 100);
|
|
688
|
+
const setupResult = await this.waitForEventToRespond(setupEvent);
|
|
689
|
+
this.respondToMessage(message.id!!, setupResult.data, setupEvent);
|
|
690
|
+
break;
|
|
1072
691
|
}
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
692
|
+
case 'library-search':
|
|
693
|
+
await this.handleEventWithResponse<BasicLibraryInfo[]>(
|
|
694
|
+
message,
|
|
695
|
+
(event) =>
|
|
696
|
+
this.eventEmitter.emit('library-search', message.args, event)
|
|
697
|
+
);
|
|
698
|
+
break;
|
|
699
|
+
case 'game-details':
|
|
700
|
+
await this.handleEventWithResponse<StoreData | undefined>(
|
|
701
|
+
message,
|
|
702
|
+
(event) =>
|
|
703
|
+
this.eventEmitter.emit('game-details', message.args, event),
|
|
704
|
+
{
|
|
705
|
+
requireListener: 'game-details',
|
|
706
|
+
noListenerError: 'No event listener for game-details',
|
|
707
|
+
}
|
|
708
|
+
);
|
|
709
|
+
break;
|
|
710
|
+
case 'check-for-updates':
|
|
711
|
+
await this.handleEventWithResponse<
|
|
712
|
+
{ available: true; version: string } | { available: false }
|
|
713
|
+
>(message, (event) =>
|
|
714
|
+
this.eventEmitter.emit('check-for-updates', message.args, event)
|
|
715
|
+
);
|
|
716
|
+
break;
|
|
717
|
+
case 'request-dl':
|
|
718
|
+
let requestDLEvent = new EventResponse<SearchResult>(
|
|
719
|
+
(screen, name, description) =>
|
|
720
|
+
this.userInputAsked(screen, name, description)
|
|
721
|
+
);
|
|
722
|
+
if (this.eventEmitter.listenerCount('request-dl') === 0) {
|
|
723
|
+
this.respondToMessage(
|
|
724
|
+
message.id!!,
|
|
725
|
+
{
|
|
726
|
+
error: 'No event listener for request-dl',
|
|
727
|
+
},
|
|
728
|
+
requestDLEvent
|
|
729
|
+
);
|
|
730
|
+
break;
|
|
1089
731
|
}
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
{
|
|
1114
|
-
requireListener: 'game-details',
|
|
1115
|
-
noListenerError: 'No event listener for game-details',
|
|
732
|
+
const { appID, info } = message.args as {
|
|
733
|
+
appID: number;
|
|
734
|
+
info: SearchResult;
|
|
735
|
+
};
|
|
736
|
+
this.eventEmitter.emit(
|
|
737
|
+
'request-dl',
|
|
738
|
+
appID,
|
|
739
|
+
info,
|
|
740
|
+
requestDLEvent as EventResponse<SearchResult>
|
|
741
|
+
);
|
|
742
|
+
const requestDLResult =
|
|
743
|
+
await this.waitForEventToRespond(requestDLEvent);
|
|
744
|
+
if (requestDLEvent.failed) {
|
|
745
|
+
this.respondToMessage(message.id!!, undefined, requestDLEvent);
|
|
746
|
+
break;
|
|
747
|
+
}
|
|
748
|
+
if (
|
|
749
|
+
requestDLEvent.data === undefined ||
|
|
750
|
+
requestDLEvent.data?.downloadType === 'request'
|
|
751
|
+
) {
|
|
752
|
+
throw new Error(
|
|
753
|
+
'Request DL event did not return a valid result. Please ensure that the event does not resolve with another `request` download type.'
|
|
754
|
+
);
|
|
1116
755
|
}
|
|
1117
|
-
);
|
|
1118
|
-
break;
|
|
1119
|
-
case 'check-for-updates':
|
|
1120
|
-
await this.handleEventWithResponse<
|
|
1121
|
-
{ available: true; version: string } | { available: false }
|
|
1122
|
-
>(message, (event) =>
|
|
1123
|
-
this.eventEmitter.emit('check-for-updates', message.args, event)
|
|
1124
|
-
);
|
|
1125
|
-
break;
|
|
1126
|
-
case 'request-dl':
|
|
1127
|
-
let requestDLEvent = new EventResponse<SearchResult>(
|
|
1128
|
-
(screen, name, description) =>
|
|
1129
|
-
this.userInputAsked(screen, name, description, this.socket)
|
|
1130
|
-
);
|
|
1131
|
-
if (this.eventEmitter.listenerCount('request-dl') === 0) {
|
|
1132
756
|
this.respondToMessage(
|
|
1133
757
|
message.id!!,
|
|
1134
|
-
|
|
1135
|
-
error: 'No event listener for request-dl',
|
|
1136
|
-
},
|
|
758
|
+
requestDLResult.data,
|
|
1137
759
|
requestDLEvent
|
|
1138
760
|
);
|
|
1139
761
|
break;
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
requestDLEvent
|
|
1146
|
-
);
|
|
1147
|
-
const requestDLResult =
|
|
1148
|
-
await this.waitForEventToRespond(requestDLEvent);
|
|
1149
|
-
if (requestDLEvent.failed) {
|
|
1150
|
-
this.respondToMessage(message.id!!, undefined, requestDLEvent);
|
|
762
|
+
case 'catalog':
|
|
763
|
+
await this.handleEventWithResponseNoInput<CatalogResponse>(
|
|
764
|
+
message,
|
|
765
|
+
(event) => this.eventEmitter.emit('catalog', event)
|
|
766
|
+
);
|
|
1151
767
|
break;
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
) {
|
|
1157
|
-
throw new Error(
|
|
1158
|
-
'Request DL event did not return a valid result. Please ensure that the event does not resolve with another `request` download type.'
|
|
768
|
+
case 'task-run': {
|
|
769
|
+
let taskRunEvent = new EventResponse<void>(
|
|
770
|
+
(screen, name, description) =>
|
|
771
|
+
this.userInputAsked(screen, name, description)
|
|
1159
772
|
);
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
// Check for taskName: first from args directly (from SearchResult), then from manifest.__taskName (for ActionOption)
|
|
1180
|
-
const taskName =
|
|
1181
|
-
message.args.taskName && typeof message.args.taskName === 'string'
|
|
1182
|
-
? message.args.taskName
|
|
1183
|
-
: message.args.manifest &&
|
|
1184
|
-
typeof message.args.manifest === 'object'
|
|
1185
|
-
? (message.args.manifest as Record<string, unknown>).__taskName
|
|
1186
|
-
: undefined;
|
|
1187
|
-
|
|
1188
|
-
if (
|
|
1189
|
-
taskName &&
|
|
1190
|
-
typeof taskName === 'string' &&
|
|
1191
|
-
this.addon.hasTaskHandler(taskName)
|
|
1192
|
-
) {
|
|
1193
|
-
// Use the registered task handler
|
|
1194
|
-
const handler = this.addon.getTaskHandler(taskName)!;
|
|
1195
|
-
const task = new Task(taskRunEvent);
|
|
1196
|
-
try {
|
|
773
|
+
const args = message.args as AddonTaskRunEventArgs;
|
|
774
|
+
|
|
775
|
+
// Check for taskName: first from args directly (from SearchResult), then from manifest.__taskName (for ActionOption)
|
|
776
|
+
const taskName =
|
|
777
|
+
args.taskName && typeof args.taskName === 'string'
|
|
778
|
+
? args.taskName
|
|
779
|
+
: args.manifest && typeof args.manifest === 'object'
|
|
780
|
+
? args.manifest.__taskName
|
|
781
|
+
: undefined;
|
|
782
|
+
|
|
783
|
+
if (
|
|
784
|
+
taskName &&
|
|
785
|
+
typeof taskName === 'string' &&
|
|
786
|
+
this.addon.hasTaskHandler(taskName)
|
|
787
|
+
) {
|
|
788
|
+
// Use the registered task handler
|
|
789
|
+
const handler = this.addon.getTaskHandler(taskName)!;
|
|
790
|
+
const task = new Task(taskRunEvent);
|
|
1197
791
|
const interval = setInterval(() => {
|
|
1198
792
|
if (taskRunEvent.resolved) {
|
|
1199
793
|
clearInterval(interval);
|
|
@@ -1201,48 +795,55 @@ class OGIAddonWSListener {
|
|
|
1201
795
|
}
|
|
1202
796
|
this.send('defer-update', {
|
|
1203
797
|
logs: taskRunEvent.logs,
|
|
1204
|
-
deferID:
|
|
798
|
+
deferID: args.deferID ?? '',
|
|
1205
799
|
progress: taskRunEvent.progress,
|
|
1206
800
|
failed: taskRunEvent.failed,
|
|
1207
|
-
} as
|
|
801
|
+
} as AddonClientToServerEventArgs['defer-update']);
|
|
1208
802
|
}, 100);
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
803
|
+
try {
|
|
804
|
+
const result = handler(task, {
|
|
805
|
+
manifest: args.manifest || {},
|
|
806
|
+
downloadPath: args.downloadPath || '',
|
|
807
|
+
name: args.name || '',
|
|
808
|
+
libraryInfo: args.libraryInfo,
|
|
809
|
+
});
|
|
810
|
+
// If handler returns a promise, wait for it
|
|
811
|
+
if (result instanceof Promise) {
|
|
812
|
+
await result;
|
|
813
|
+
}
|
|
814
|
+
} catch (error) {
|
|
815
|
+
taskRunEvent.fail(
|
|
816
|
+
error instanceof Error ? error.message : String(error)
|
|
817
|
+
);
|
|
818
|
+
} finally {
|
|
819
|
+
clearInterval(interval);
|
|
1218
820
|
}
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
} catch (error) {
|
|
821
|
+
} else {
|
|
822
|
+
// No handler found - fail the task
|
|
1222
823
|
taskRunEvent.fail(
|
|
1223
|
-
|
|
824
|
+
taskName
|
|
825
|
+
? `No task handler registered for task name: ${taskName}`
|
|
826
|
+
: 'No task name provided'
|
|
1224
827
|
);
|
|
1225
828
|
}
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
829
|
+
|
|
830
|
+
const taskRunResult =
|
|
831
|
+
await this.waitForEventToRespond(taskRunEvent);
|
|
832
|
+
this.respondToMessage(
|
|
833
|
+
message.id!!,
|
|
834
|
+
taskRunResult.data,
|
|
835
|
+
taskRunEvent
|
|
1232
836
|
);
|
|
837
|
+
break;
|
|
1233
838
|
}
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
839
|
+
case 'launch-app':
|
|
840
|
+
await this.handleEventWithResponse<void>(message, (event) =>
|
|
841
|
+
this.eventEmitter.emit('launch-app', message.args, event)
|
|
842
|
+
);
|
|
843
|
+
break;
|
|
1238
844
|
}
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
this.eventEmitter.emit('launch-app', message.args, event)
|
|
1242
|
-
);
|
|
1243
|
-
break;
|
|
1244
|
-
}
|
|
1245
|
-
});
|
|
845
|
+
});
|
|
846
|
+
}
|
|
1246
847
|
}
|
|
1247
848
|
|
|
1248
849
|
private waitForEventToRespond<T>(
|
|
@@ -1278,12 +879,12 @@ class OGIAddonWSListener {
|
|
|
1278
879
|
* If options.requireListener is set and that event has no listeners, responds with options.noListenerError and returns.
|
|
1279
880
|
*/
|
|
1280
881
|
private async handleEventWithResponse<T>(
|
|
1281
|
-
message:
|
|
882
|
+
message: AddonServerToClientWebsocketMessage,
|
|
1282
883
|
emit: (event: EventResponse<T>) => void,
|
|
1283
884
|
options?: { requireListener: string; noListenerError: string }
|
|
1284
885
|
): Promise<void> {
|
|
1285
886
|
const event = new EventResponse<T>((screen, name, description) =>
|
|
1286
|
-
this.userInputAsked(screen, name, description
|
|
887
|
+
this.userInputAsked(screen, name, description)
|
|
1287
888
|
);
|
|
1288
889
|
if (
|
|
1289
890
|
options &&
|
|
@@ -1305,7 +906,7 @@ class OGIAddonWSListener {
|
|
|
1305
906
|
* Same as handleEventWithResponse but for events that don't need userInputAsked (e.g. catalog).
|
|
1306
907
|
*/
|
|
1307
908
|
private async handleEventWithResponseNoInput<T>(
|
|
1308
|
-
message:
|
|
909
|
+
message: AddonServerToClientWebsocketMessage,
|
|
1309
910
|
emit: (event: EventResponse<T>) => void
|
|
1310
911
|
): Promise<void> {
|
|
1311
912
|
const event = new EventResponse<T>();
|
|
@@ -1319,49 +920,41 @@ class OGIAddonWSListener {
|
|
|
1319
920
|
response: any,
|
|
1320
921
|
originalEvent: EventResponse<any> | undefined
|
|
1321
922
|
) {
|
|
1322
|
-
this.
|
|
1323
|
-
|
|
923
|
+
void this.transport.send(
|
|
924
|
+
{
|
|
1324
925
|
event: 'response',
|
|
1325
926
|
id: messageID,
|
|
1326
927
|
args: response,
|
|
1327
928
|
statusError: originalEvent ? originalEvent.failed : undefined,
|
|
1328
|
-
}
|
|
929
|
+
} as AddonClientToServerWebsocketMessage,
|
|
930
|
+
{ expectResponse: false }
|
|
1329
931
|
);
|
|
1330
932
|
console.log('dispatched response to ' + messageID);
|
|
1331
933
|
}
|
|
1332
934
|
|
|
1333
|
-
public
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
if (message.id === messageID) {
|
|
1344
|
-
resolve(message.args);
|
|
1345
|
-
} else {
|
|
1346
|
-
this.socket.once('message', waiter);
|
|
1347
|
-
}
|
|
1348
|
-
};
|
|
1349
|
-
this.socket.once('message', waiter);
|
|
1350
|
-
});
|
|
935
|
+
public async requestResponse<T>(
|
|
936
|
+
event: AddonClientToServerEventName,
|
|
937
|
+
args: AddonClientToServerEventArgs[AddonClientToServerEventName]
|
|
938
|
+
): Promise<T> {
|
|
939
|
+
const response = await this.transport.send(
|
|
940
|
+
{ event, args } as AddonClientToServerWebsocketMessage,
|
|
941
|
+
{ expectResponse: true }
|
|
942
|
+
);
|
|
943
|
+
return response.args as T;
|
|
1351
944
|
}
|
|
1352
945
|
|
|
1353
946
|
public send(
|
|
1354
|
-
event:
|
|
1355
|
-
args:
|
|
947
|
+
event: AddonClientToServerEventName,
|
|
948
|
+
args: AddonClientToServerEventArgs[AddonClientToServerEventName]
|
|
1356
949
|
): string {
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
JSON.stringify({
|
|
950
|
+
const id = randomMessageId();
|
|
951
|
+
void this.transport.send(
|
|
952
|
+
{
|
|
1361
953
|
event,
|
|
1362
954
|
args,
|
|
1363
955
|
id,
|
|
1364
|
-
}
|
|
956
|
+
} as AddonClientToServerWebsocketMessage,
|
|
957
|
+
{ expectResponse: false }
|
|
1365
958
|
);
|
|
1366
959
|
return id;
|
|
1367
960
|
}
|