worldstate-emitter 2.4.0 → 2.4.1

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/dist/index.js ADDED
@@ -0,0 +1,1288 @@
1
+ //#region \0rolldown/runtime.js
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
10
+ key = keys[i];
11
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
12
+ get: ((k) => from[k]).bind(null, key),
13
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
14
+ });
15
+ }
16
+ return to;
17
+ };
18
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
19
+ value: mod,
20
+ enumerable: true
21
+ }) : target, mod));
22
+ //#endregion
23
+ let node_events = require("node:events");
24
+ node_events = __toESM(node_events);
25
+ let rss_feed_emitter = require("rss-feed-emitter");
26
+ rss_feed_emitter = __toESM(rss_feed_emitter);
27
+ let sanitize_html = require("sanitize-html");
28
+ sanitize_html = __toESM(sanitize_html);
29
+ let winston = require("winston");
30
+ let twitter = require("twitter");
31
+ twitter = __toESM(twitter);
32
+ let warframe_worldstate_data = require("warframe-worldstate-data");
33
+ warframe_worldstate_data = __toESM(warframe_worldstate_data);
34
+ let cron = require("cron");
35
+ //#region resources/rssFeeds.json
36
+ var rssFeeds_default = [
37
+ {
38
+ "url": "https://forums.warframe.com/forum/38-players-helping-players.xml",
39
+ "key": "players_helping_players",
40
+ "defaultAttach": "https://i.imgur.com/cuk4ro9.png"
41
+ },
42
+ {
43
+ "url": "https://forums.warframe.com/forum/3-pc-update-notes.xml",
44
+ "key": "forum.updates.pc",
45
+ "defaultAttach": "https://i.imgur.com/eY1NkzO.png"
46
+ },
47
+ {
48
+ "url": "https://forums.warframe.com/forum/170-announcements-events.xml",
49
+ "key": "forum.news",
50
+ "defaultAttach": "https://i.imgur.com/CNrsc7V.png"
51
+ },
52
+ {
53
+ "url": "https://forums.warframe.com/forum/123-developer-workshop-update-notes.xml",
54
+ "key": "forum.workshop"
55
+ },
56
+ {
57
+ "url": "https://forums.warframe.com/discover/837.xml",
58
+ "key": "forum.staff.megan",
59
+ "author": {
60
+ "name": "[DE]Megan",
61
+ "url": "https://forums.warframe.com/profile/384139-demegan/",
62
+ "icon_url": "https://content.invisioncic.com/Mwarframe/monthly_2017_06/ezgif.com-crop.thumb.gif.e510920610e8489a54dd5dbe8b9fd4db.gif"
63
+ }
64
+ },
65
+ {
66
+ "url": "https://forums.warframe.com/discover/839.xml",
67
+ "key": "forum.staff.rebecca",
68
+ "author": {
69
+ "name": "[DE]Rebecca",
70
+ "url": "https://forums.warframe.com/profile/4-derebecca/",
71
+ "icon_url": "https://content.invisioncic.com/Mwarframe/pages_media/1_PlayerAvatarsInkary.png"
72
+ }
73
+ },
74
+ {
75
+ "url": "https://forums.warframe.com/discover/840.xml",
76
+ "key": "forum.staff.danielle",
77
+ "author": {
78
+ "name": "[DE]Danielle",
79
+ "url": "https://forums.warframe.com/profile/869879-dedanielle/",
80
+ "icon_url": "https://content.invisioncic.com/Mwarframe/monthly_2018_04/profile.thumb.jpg.fe072e16d5b54892b95030ea410264e7.jpg"
81
+ }
82
+ },
83
+ {
84
+ "url": "https://forums.warframe.com/discover/841.xml",
85
+ "key": "forum.staff.drew",
86
+ "author": {
87
+ "name": "[DE]Drew",
88
+ "url": "https://forums.warframe.com/profile/488958-dedrew/",
89
+ "icon_url": "https://content.invisioncic.com/Mwarframe/monthly_2016_06/576c323f14e92_AnimatedAvatarMirrored.thumb.gif.f12f2373d0d4b91363647f35b30026e0.gif"
90
+ }
91
+ },
92
+ {
93
+ "url": "https://forums.warframe.com/discover/842.xml",
94
+ "key": "forum.staff.glen",
95
+ "author": {
96
+ "name": "[DE]Glen",
97
+ "url": "https://forums.warframe.com/profile/10-deglen/",
98
+ "icon_url": "https://content.invisioncic.com/Mwarframe/monthly_2016_05/57474a4312a72_GlensHorse.thumb.png.7b47cb0660f5af2e6101ed84d3192c03.png"
99
+ }
100
+ },
101
+ {
102
+ "url": "https://forums.warframe.com/discover/1171.xml",
103
+ "key": "forum.staff.taylor",
104
+ "author": {
105
+ "name": "[DE]taylor",
106
+ "url": "https://forums.warframe.com/profile/1943322-detaylor/",
107
+ "icon_url": "https://content.invisioncic.com/Mwarframe/monthly_2017_07/596ceb9b4e182_6FUC4s3%281%29.thumb.png.7045d25fc7b057c70ddcffe14cd1f43e.png"
108
+ }
109
+ },
110
+ {
111
+ "url": "https://forums.warframe.com/discover/1777.xml",
112
+ "key": "forum.staff.steve",
113
+ "author": {
114
+ "name": "[DE]Steve",
115
+ "url": "https://forums.warframe.com/profile/24-desteve/",
116
+ "icon_url": "https://content.invisioncic.com/Mwarframe/monthly_2016_06/575f13eaf3080_Pastedimageat2016_06_1304_11PM.thumb.png.fb98af24931a1820d00293a55c05baef.png"
117
+ }
118
+ },
119
+ {
120
+ "url": "https://forums.warframe.com/discover/1291.xml",
121
+ "key": "forum.staff.helen",
122
+ "author": {
123
+ "name": "[DE]Helen",
124
+ "url": "https://forums.warframe.com/profile/2522846-dehelen/",
125
+ "icon_url": "https://content.invisioncic.com/Mwarframe/monthly_2017_06/590e937e598f2_Helen-Icon.jpg.0ac6d3d86c1d48f3f3e1a035344ab87a.thumb.jpg.d2225fb0dcd980f081ddddbf46458072.jpg"
126
+ }
127
+ },
128
+ {
129
+ "url": "https://forums.warframe.com/discover/1294.xml",
130
+ "key": "forum.staff.saske",
131
+ "author": {
132
+ "name": "[DE]Saske",
133
+ "url": "https://forums.warframe.com/profile/4513168-nswdesaske/",
134
+ "icon_url": "https://content.invisioncic.com/Mwarframe/pages_media/1_TennoCon2018Glyph.png"
135
+ }
136
+ },
137
+ {
138
+ "url": "https://forums.warframe.com/discover/1295.xml",
139
+ "key": "forum.staff.syncrasis",
140
+ "author": {
141
+ "name": "[DE]Syncrasis",
142
+ "url": "https://forums.warframe.com/profile/2514676-desyncrasis//",
143
+ "icon_url": "https://content.invisioncic.com/Mwarframe/monthly_2017_02/aladv.thumb.jpg.a06cbfa091579eb9ce2ae96eb0b42e34.jpg"
144
+ }
145
+ },
146
+ {
147
+ "url": "https://forums.warframe.com/discover/1299.xml",
148
+ "key": "forum.staff.pablo",
149
+ "author": {
150
+ "name": "[DE]Pablo",
151
+ "url": "https://forums.warframe.com/profile/217656-depablo/",
152
+ "icon_url": "https://content.invisioncic.com/Mwarframe/monthly_2016_05/Pablo.thumb.png.35bb0384ef7b88e807d55ffc31af0896.png"
153
+ }
154
+ },
155
+ {
156
+ "url": "https://forums.warframe.com/discover/1779.xml",
157
+ "key": "forum.staff.marcus",
158
+ "author": {
159
+ "name": "[DE]Marcus",
160
+ "url": "https://forums.warframe.com/profile/3443485-demarcus/",
161
+ "icon_url": "https://content.invisioncic.com/Mwarframe/monthly_2018_01/5a55278d71caa_MarcusIIPrint.thumb.jpg.16cb689d778112333cffb6fe546b89ec.jpg"
162
+ }
163
+ }
164
+ ];
165
+ //#endregion
166
+ //#region utilities/env.ts
167
+ const LOG_LEVEL = process?.env?.LOG_LEVEL || "error";
168
+ const twiClientInfo = {
169
+ consumer_key: process?.env?.TWITTER_KEY,
170
+ consumer_secret: process?.env?.TWITTER_SECRET,
171
+ bearer_token: process?.env?.TWITTER_BEARER_TOKEN
172
+ };
173
+ const TWITTER_TIMEOUT = Number(process?.env?.TWITTER_TIMEOUT) || 6e4;
174
+ //#endregion
175
+ //#region utilities/index.ts
176
+ let tempLogger;
177
+ try {
178
+ const { combine, label, printf, colorize } = winston.format;
179
+ const transport = new winston.transports.Console();
180
+ const logFormat = printf((info) => `[${info.label}] ${info.level}: ${info.message}`);
181
+ tempLogger = (0, winston.createLogger)({
182
+ format: combine(colorize(), label({ label: "WS" }), logFormat),
183
+ transports: [transport]
184
+ });
185
+ tempLogger.level = LOG_LEVEL;
186
+ } catch (_e) {
187
+ tempLogger = (0, winston.createLogger)({ transports: [new winston.transports.Console()] });
188
+ }
189
+ const logger = tempLogger;
190
+ /**
191
+ * Group an array by a field value
192
+ * @param array - array of objects to group
193
+ * @param field - field to group by
194
+ * @returns Grouped object
195
+ */
196
+ const groupBy = (array, field) => {
197
+ const grouped = {};
198
+ if (!array) return void 0;
199
+ for (const item of array) {
200
+ const fVal = String(item[field]);
201
+ if (!grouped[fVal]) grouped[fVal] = [];
202
+ grouped[fVal].push(item);
203
+ }
204
+ return grouped;
205
+ };
206
+ const allowedDeviation = 3e4;
207
+ /**
208
+ * Validate that b is between a and c
209
+ * @param a - The first Date, should be the last time things were updated
210
+ * @param b - The second Date, should be the activation time of an event
211
+ * @param c - The third Date, should be the start time of this update cycle
212
+ * @returns if the event date is between the server start time and the last update time
213
+ */
214
+ const between = (a, b, c = Date.now()) => b + allowedDeviation > a && b - allowedDeviation < c;
215
+ /**
216
+ * Returns the number of milliseconds between now and a given date
217
+ * @param d - The date from which the current time will be subtracted
218
+ * @param now - A function that returns the current UNIX time in milliseconds
219
+ * @returns Milliseconds from now
220
+ */
221
+ function fromNow(d, now = Date.now) {
222
+ return new Date(d).getTime() - now();
223
+ }
224
+ /**
225
+ * Map of last updated dates/times
226
+ */
227
+ const lastUpdated = {
228
+ pc: { en: 0 },
229
+ ps4: { en: Date.now() },
230
+ xb1: { en: Date.now() },
231
+ swi: { en: Date.now() }
232
+ };
233
+ //#endregion
234
+ //#region handlers/RSS.ts
235
+ /**
236
+ * RSS Emitter, leverages rss-feed-emitter
237
+ */
238
+ var RSS = class {
239
+ logger = logger;
240
+ emitter;
241
+ feeder;
242
+ startTime;
243
+ feeds;
244
+ /**
245
+ * Set up emitting events for warframe forum entries
246
+ * @param eventEmitter - Emitter to send events from
247
+ * @param options - Optional configuration
248
+ * @param options.autoStart - Whether to automatically start the feeder (default: true)
249
+ * @param options.feeds - Custom feed list (default: uses rssFeeds.json)
250
+ * @param options.startTime - Custom start time for filtering old items (default: Date.now())
251
+ * @param options.logger - Custom logger instance (default: uses global logger)
252
+ */
253
+ constructor(eventEmitter, options = {}) {
254
+ this.emitter = eventEmitter;
255
+ this.feeds = options.feeds || rssFeeds_default;
256
+ this.startTime = options.startTime ?? Date.now();
257
+ if (options.logger) this.logger = options.logger;
258
+ this.feeder = new rss_feed_emitter.default({
259
+ userAgent: "WFCD Feed Notifier",
260
+ skipFirstLoad: true
261
+ });
262
+ this.feeder.on("error", this.logger.error.bind(this.logger));
263
+ this.feeder.on("new-item", this.handleNew.bind(this));
264
+ if (options.autoStart !== false) this.start();
265
+ }
266
+ /**
267
+ * Start the RSS feed polling
268
+ */
269
+ start() {
270
+ for (const feed of this.feeds) this.feeder.add({
271
+ url: feed.url,
272
+ refresh: 3e4
273
+ });
274
+ this.logger.debug("RSS Feed active");
275
+ }
276
+ destroy() {
277
+ this.feeder.destroy();
278
+ this.logger.debug("RSS Feed destroyed");
279
+ }
280
+ /**
281
+ * Extract image URL from RSS item description
282
+ * @param description - The RSS item description HTML
283
+ * @param feed - The feed configuration
284
+ * @returns The image URL or undefined
285
+ * @private
286
+ */
287
+ extractImage(description, feed) {
288
+ const firstImg = ((description || "").match(/<img.*src="(.*)".*>/i) || [])[1];
289
+ if (!firstImg) return feed.defaultAttach;
290
+ if (firstImg.startsWith("//")) return firstImg.replace("//", "https://");
291
+ return firstImg;
292
+ }
293
+ /**
294
+ * Find the feed configuration for an RSS item
295
+ * @param item - The RSS item
296
+ * @returns The feed configuration or undefined
297
+ * @private
298
+ */
299
+ findFeed(item) {
300
+ let feed = this.feeds.find((feedEntry) => feedEntry.url === item.meta.link);
301
+ if (feed) return feed;
302
+ const rssLink = item.meta["rss:link"]?.["#"];
303
+ if (rssLink) {
304
+ feed = this.feeds.find((feedEntry) => feedEntry.url === rssLink);
305
+ if (feed) return feed;
306
+ }
307
+ const registeredFeeds = this.feeder.list;
308
+ if (Array.isArray(registeredFeeds)) for (const registeredFeed of registeredFeeds) {
309
+ const matchingFeed = this.feeds.find((f) => f.url === registeredFeed.url);
310
+ if (matchingFeed) return matchingFeed;
311
+ }
312
+ this.logger.debug(`No feed found for item: ${item.title} (meta.link: ${item.meta.link})`);
313
+ }
314
+ /**
315
+ * Handle a new RSS item
316
+ * @param item - The RSS item from the feed
317
+ * @private
318
+ */
319
+ handleNew(item) {
320
+ try {
321
+ if (item.image && Object.keys(item.image).length) this.logger.debug(`Image: ${JSON.stringify(item.image)}`);
322
+ if (new Date(item.pubDate).getTime() <= this.startTime) return;
323
+ const feed = this.findFeed(item);
324
+ if (!feed) return;
325
+ const firstImg = this.extractImage(item.description, feed);
326
+ const rssSummary = {
327
+ body: (0, sanitize_html.default)(item.description || "​", {
328
+ allowedTags: [],
329
+ allowedAttributes: {}
330
+ }).replace(/\n\n+\s*/gm, "\n\n"),
331
+ url: item.link,
332
+ timestamp: item.pubDate,
333
+ description: item.meta.description,
334
+ author: feed.author || {
335
+ name: "Warframe Forums",
336
+ url: item.meta["rss:link"]?.["#"] || item.link,
337
+ icon_url: "https://i.imgur.com/hE2jdpv.png"
338
+ },
339
+ title: item.title,
340
+ ...firstImg && { image: firstImg },
341
+ id: feed.key
342
+ };
343
+ this.emitter.emit("rss", rssSummary);
344
+ } catch (error) {
345
+ this.logger.error(error);
346
+ }
347
+ }
348
+ };
349
+ //#endregion
350
+ //#region resources/tweeters.json
351
+ var tweeters_default = [
352
+ {
353
+ "acc_name": "@playwarframe",
354
+ "plain": "warframe"
355
+ },
356
+ {
357
+ "acc_name": "@digitalextremes",
358
+ "plain": "digitalextremes"
359
+ },
360
+ {
361
+ "acc_name": "@PabloMakes",
362
+ "plain": "pablo"
363
+ },
364
+ {
365
+ "acc_name": "@Cam_Rogers",
366
+ "plain": "cameron"
367
+ },
368
+ {
369
+ "acc_name": "@rebbford",
370
+ "plain": "rebecca"
371
+ },
372
+ {
373
+ "acc_name": "@sj_sinclair",
374
+ "plain": "steve"
375
+ },
376
+ {
377
+ "acc_name": "@soelloo",
378
+ "plain": "danielle"
379
+ },
380
+ {
381
+ "acc_name": "@moitoi",
382
+ "plain": "megan"
383
+ },
384
+ {
385
+ "acc_name": "@GameSoundDesign",
386
+ "plain": "george"
387
+ },
388
+ {
389
+ "acc_name": "@msinilo",
390
+ "plain": "maciej"
391
+ },
392
+ {
393
+ "acc_name": "@sheldoncarter",
394
+ "plain": "sheldon"
395
+ },
396
+ {
397
+ "acc_name": "@MarcusKretz",
398
+ "plain": "marcus"
399
+ },
400
+ {
401
+ "acc_name": "@Helen_Heikkila",
402
+ "plain": "helen"
403
+ },
404
+ {
405
+ "acc_name": "@tobitenno",
406
+ "plain": "tobiah"
407
+ },
408
+ {
409
+ "acc_name": "@wfdiscord",
410
+ "plain": "wfdiscord"
411
+ }
412
+ ];
413
+ //#endregion
414
+ //#region handlers/Twitter.ts
415
+ const determineTweetType = (tweet) => {
416
+ if (tweet.in_reply_to_status_id) return "reply";
417
+ if (tweet.quoted_status_id) return "quote";
418
+ if (tweet.retweeted_status) return "retweet";
419
+ return "tweet";
420
+ };
421
+ const parseAuthor = (tweet) => ({
422
+ name: tweet.user.name,
423
+ handle: tweet.user.screen_name,
424
+ url: `https://twitter.com/${tweet.user.screen_name}`,
425
+ avatar: tweet.user.profile_image_url ? tweet.user.profile_image_url.replace("_normal.jpg", ".jpg") : ""
426
+ });
427
+ const parseQuoted = (tweet, type) => tweet[type] ? {
428
+ text: tweet[type].full_text || tweet[type].text,
429
+ author: {
430
+ name: tweet[type].user.name,
431
+ handle: tweet[type].user.screen_name
432
+ }
433
+ } : void 0;
434
+ const parseTweet = (tweets, watchable) => {
435
+ if (!tweets.length) throw new Error(`No tweets found for ${watchable.acc_name}`);
436
+ const [tweet] = tweets;
437
+ const type = determineTweetType(tweet);
438
+ return {
439
+ id: `twitter.${watchable.plain}.${type}`,
440
+ uniqueId: String(tweets[0].id_str),
441
+ text: tweet.full_text || tweet.text,
442
+ url: `https://twitter.com/${tweet.user.screen_name}/status/${tweet.id_str}`,
443
+ mediaUrl: tweet.entities.media ? tweet.entities.media[0].media_url : void 0,
444
+ isReply: typeof tweet.in_reply_to_status_id !== "undefined",
445
+ author: parseAuthor(tweet),
446
+ quote: parseQuoted(tweet, "quoted_status"),
447
+ retweet: parseQuoted(tweet, "retweeted_status"),
448
+ createdAt: new Date(tweet.created_at)
449
+ };
450
+ };
451
+ /**
452
+ * Twitter event handler
453
+ */
454
+ var TwitterCache = class {
455
+ emitter;
456
+ timeout;
457
+ clientInfoValid;
458
+ client;
459
+ toWatch;
460
+ currentData;
461
+ lastUpdated;
462
+ updateInterval;
463
+ updating;
464
+ disposed;
465
+ /**
466
+ * Create a new Twitter self-updating cache
467
+ * @param eventEmitter - Emitter to push new tweets to
468
+ * @param options - Optional configuration
469
+ * @param options.autoStart - Whether to automatically start polling (default: true)
470
+ * @param options.clientInfo - Custom Twitter client credentials
471
+ * @param options.watchList - Custom list of Twitter accounts to watch
472
+ * @param options.timeout - Polling interval in milliseconds
473
+ */
474
+ constructor(eventEmitter, options = {}) {
475
+ this.emitter = eventEmitter;
476
+ this.timeout = options.timeout ?? TWITTER_TIMEOUT;
477
+ this.lastUpdated = Date.now() - 6e4;
478
+ this.disposed = false;
479
+ const clientInfo = options.clientInfo ?? twiClientInfo;
480
+ this.clientInfoValid = !!clientInfo.consumer_key && !!clientInfo.consumer_secret && !!clientInfo.bearer_token;
481
+ if (options.watchList) this.toWatch = options.watchList;
482
+ if (options.autoStart !== false) this.initClient(clientInfo);
483
+ }
484
+ initClient(clientInfo) {
485
+ try {
486
+ if (this.clientInfoValid && clientInfo.consumer_key && clientInfo.consumer_secret && clientInfo.bearer_token) {
487
+ this.client = new twitter.default({
488
+ consumer_key: clientInfo.consumer_key,
489
+ consumer_secret: clientInfo.consumer_secret,
490
+ bearer_token: clientInfo.bearer_token
491
+ });
492
+ if (!this.toWatch) this.toWatch = tweeters_default;
493
+ this.currentData = void 0;
494
+ this.lastUpdated = Date.now() - 6e4;
495
+ this.updateInterval = setInterval(() => this.update(), this.timeout);
496
+ this.update();
497
+ } else {
498
+ logger.warn(`Twitter client not initialized... invalid token: ${clientInfo.bearer_token}`);
499
+ this.dispose();
500
+ }
501
+ } catch (err) {
502
+ logger.error(err);
503
+ this.dispose();
504
+ }
505
+ }
506
+ /**
507
+ * Set a mock Twitter client for testing
508
+ * @param mockClient - Mock Twitter client
509
+ * @internal
510
+ */
511
+ setClient(mockClient) {
512
+ this.client = mockClient;
513
+ if (!this.toWatch) this.toWatch = tweeters_default;
514
+ }
515
+ /**
516
+ * Force the cache to update
517
+ * @returns The currently updating promise
518
+ */
519
+ async update() {
520
+ if (this.disposed || !this.clientInfoValid) return void 0;
521
+ if (!this.toWatch || this.toWatch.length === 0) {
522
+ logger.verbose("Not processing twitter, no data to watch.");
523
+ return;
524
+ }
525
+ if (!this.client) {
526
+ logger.verbose("Not processing twitter, no client to connect.");
527
+ return;
528
+ }
529
+ this.updating = this.getParseableData();
530
+ return this.updating;
531
+ }
532
+ /**
533
+ * Get data able to be parsed from twitter.
534
+ * @returns Tweets
535
+ */
536
+ async getParseableData() {
537
+ logger.silly("Starting Twitter update...");
538
+ const parsedData = [];
539
+ try {
540
+ await Promise.all((this.toWatch || []).map(async (watchable) => {
541
+ const tweet = parseTweet(await this.client.get("statuses/user_timeline", {
542
+ screen_name: watchable.acc_name,
543
+ tweet_mode: "extended",
544
+ count: 1
545
+ }), watchable);
546
+ parsedData.push(tweet);
547
+ if (tweet.createdAt.getTime() > this.lastUpdated) this.emitter.emit("tweet", tweet);
548
+ }));
549
+ this.currentData = parsedData;
550
+ this.lastUpdated = Date.now();
551
+ } catch (error) {
552
+ this.onError(error);
553
+ }
554
+ return parsedData;
555
+ }
556
+ /**
557
+ * Handle errors that arise while fetching data from twitter
558
+ * @param error - Twitter error
559
+ */
560
+ onError(error) {
561
+ if (Array.isArray(error) && error[0] && error[0].code === 32) {
562
+ logger.info("wiping twitter client data, could not authenticate...");
563
+ this.dispose();
564
+ } else logger.debug(JSON.stringify(error));
565
+ }
566
+ /**
567
+ * Get the current data or a promise with the current data
568
+ * @returns Either the current data if it's not updating, or the promise returning the new data
569
+ */
570
+ async getData() {
571
+ if (!this.clientInfoValid) return void 0;
572
+ if (this.updating) return this.updating;
573
+ return this.currentData || [];
574
+ }
575
+ /**
576
+ * Stop polling and clean up resources
577
+ */
578
+ dispose() {
579
+ this.disposed = true;
580
+ if (this.updateInterval) {
581
+ clearInterval(this.updateInterval);
582
+ this.updateInterval = void 0;
583
+ }
584
+ this.client = void 0;
585
+ this.clientInfoValid = false;
586
+ logger.verbose("Twitter polling stopped and resources cleaned up");
587
+ }
588
+ };
589
+ //#endregion
590
+ //#region handlers/events/eKeyOverrides.ts
591
+ const fissures = (data) => {
592
+ const fissure = data;
593
+ return `fissures.t${fissure.tierNum}.${(fissure.missionType || "").toLowerCase()}`;
594
+ };
595
+ const enemies = (data) => {
596
+ const enemy = data;
597
+ return {
598
+ eventKey: `enemies${enemy.isDiscovered ? "" : ".departed"}`,
599
+ activation: new Date(enemy.lastDiscoveredAt)
600
+ };
601
+ };
602
+ const arbitration = (data) => {
603
+ const arbi = data;
604
+ if (!arbi?.enemy) return "";
605
+ let k;
606
+ try {
607
+ k = `arbitration.${arbi.enemy.toLowerCase()}.${arbi.type.replace(/\s/g, "").toLowerCase()}`;
608
+ } catch (e) {
609
+ logger.error(`Unable to parse arbitration: ${JSON.stringify(arbi)}\n${e}`);
610
+ return "";
611
+ }
612
+ return k;
613
+ };
614
+ const events = "operations";
615
+ const overrides = {
616
+ fissures,
617
+ enemies,
618
+ arbitration,
619
+ events,
620
+ persistentEnemies: "enemies"
621
+ };
622
+ //#endregion
623
+ //#region handlers/events/checkOverrides.ts
624
+ /**
625
+ * Find overrides for the provided key
626
+ * @param key - worldstate field to find overrides
627
+ * @param data - data corresponding to the key from provided worldstate
628
+ * @returns overrided key or object
629
+ */
630
+ var checkOverrides_default = (key, data) => {
631
+ const override = overrides[key];
632
+ if (typeof override === "string") return override;
633
+ if (typeof override === "function") return override(data);
634
+ return key;
635
+ };
636
+ //#endregion
637
+ //#region handlers/events/objectLike.ts
638
+ /**
639
+ * Process object-like worldstate events
640
+ * @param data - Event data
641
+ * @param deps - Dependencies for processing
642
+ * @returns Packet to emit or undefined
643
+ */
644
+ var objectLike_default = (data, deps) => {
645
+ if (!data) return void 0;
646
+ const last = new Date(lastUpdated[deps.platform][deps.language]);
647
+ const activation = new Date(data.activation ?? 0);
648
+ const start = new Date(deps.cycleStart ?? 0);
649
+ if (between(last.getTime(), activation.getTime(), start.getTime())) return {
650
+ ...deps,
651
+ data,
652
+ id: deps.id || deps.key
653
+ };
654
+ };
655
+ //#endregion
656
+ //#region handlers/events/arrayLike.ts
657
+ /**
658
+ * arrayLike are all just arrays of objectLike
659
+ * @param deps - dependencies for processing
660
+ * @returns object(s) to emit from arrayLike processing
661
+ */
662
+ var arrayLike_default = (deps) => {
663
+ const newPackets = [];
664
+ try {
665
+ for (const arrayItem of deps.data) {
666
+ const k = checkOverrides_default(deps.key, arrayItem);
667
+ const result = objectLike_default(arrayItem, {
668
+ ...deps,
669
+ data: arrayItem,
670
+ id: typeof k === "string" ? k : k.eventKey
671
+ });
672
+ if (result) newPackets.push(result);
673
+ }
674
+ return newPackets;
675
+ } catch (err) {
676
+ logger.error(err);
677
+ return newPackets;
678
+ }
679
+ };
680
+ //#endregion
681
+ //#region handlers/events/cycleLike.ts
682
+ /**
683
+ * CycleData parser
684
+ * @param cycleData - data for parsing all cycles like this
685
+ * @param deps - dependencies for processing
686
+ * @returns Array of packets to emit
687
+ */
688
+ var cycleLike_default = (cycleData, deps) => {
689
+ const packet = {
690
+ ...deps,
691
+ data: cycleData,
692
+ id: `${deps.key.replace("Cycle", "")}.${cycleData.state}`
693
+ };
694
+ const last = new Date(lastUpdated[deps.platform]?.[deps.language] ?? 0);
695
+ const activation = new Date(cycleData.activation ?? 0);
696
+ const start = new Date(deps.cycleStart);
697
+ const packets = [];
698
+ if (between(last.getTime(), activation.getTime(), start.getTime())) packets.push(packet);
699
+ if (cycleData.expiry) {
700
+ const timePacket = {
701
+ ...packet,
702
+ id: `${packet.id}.${Math.round(fromNow(cycleData.expiry.toString()) / 6e4)}`
703
+ };
704
+ packets.push(timePacket);
705
+ }
706
+ return packets;
707
+ };
708
+ //#endregion
709
+ //#region handlers/events/kuva.ts
710
+ /**
711
+ * Process kuva fields
712
+ * @param deps - dependencies for processing
713
+ * @param packets - packets to emit
714
+ * @returns object(s) to emit from kuva stuff
715
+ */
716
+ var kuva_default = (deps, packets) => {
717
+ if (!deps.data) {
718
+ logger.error("no kuva data");
719
+ return;
720
+ }
721
+ const data = groupBy(deps.data, "type");
722
+ if (!data) return void 0;
723
+ for (const type of Object.keys(data)) {
724
+ const typeData = data[type];
725
+ if (!data[type]?.length) continue;
726
+ const updatedDeps = {
727
+ ...deps,
728
+ data: typeData[0],
729
+ id: `kuva.${typeData[0].type.replace(/\s/g, "").toLowerCase()}`,
730
+ activation: typeData[0].activation,
731
+ expiry: typeData[0].expiry
732
+ };
733
+ const p = objectLike_default(typeData[0], updatedDeps);
734
+ if (p) packets.push(p);
735
+ }
736
+ return packets.filter((p) => p !== void 0);
737
+ };
738
+ //#endregion
739
+ //#region handlers/events/nightwave.ts
740
+ /**
741
+ * Process nightwave challenges
742
+ * @param nightwave - Nightwave data
743
+ * @param deps - Dependencies for processing
744
+ * @returns Array of packets to emit
745
+ */
746
+ var nightwave_default = (nightwave, deps) => {
747
+ const groups = {
748
+ daily: [],
749
+ weekly: [],
750
+ elite: []
751
+ };
752
+ for (const challenge of nightwave.activeChallenges || []) if (challenge.isDaily) groups.daily.push(challenge);
753
+ else if (challenge.isElite) groups.elite.push(challenge);
754
+ else groups.weekly.push(challenge);
755
+ const packets = [];
756
+ for (const group of Object.keys(groups)) {
757
+ const nightwaveWithGroup = {
758
+ ...nightwave,
759
+ activeChallenges: groups[group]
760
+ };
761
+ const p = objectLike_default(nightwaveWithGroup, {
762
+ ...deps,
763
+ data: nightwaveWithGroup,
764
+ id: `nightwave.${group}`
765
+ });
766
+ if (p) packets.push(p);
767
+ }
768
+ return packets;
769
+ };
770
+ //#endregion
771
+ //#region handlers/events/parse.ts
772
+ /**
773
+ * Set up current cycle start if it's not been initiated
774
+ * @param deps - dependencies for processing
775
+ */
776
+ const initCycleStart = (deps) => {
777
+ if (!lastUpdated[deps.platform][deps.language]) lastUpdated[deps.platform][deps.language] = typeof deps.cycleStart === "number" ? deps.cycleStart : deps.cycleStart.getTime();
778
+ };
779
+ /**
780
+ * Parse new events from the provided worldstate
781
+ * @param deps - dependencies to parse out events
782
+ * @returns packet(s) to emit
783
+ */
784
+ var parse_default = (deps) => {
785
+ initCycleStart(deps);
786
+ const packets = [];
787
+ switch (deps.key) {
788
+ case "kuva": {
789
+ const kuvaData = Array.isArray(deps.data) ? deps.data : [deps.data];
790
+ return kuva_default({
791
+ ...deps,
792
+ data: kuvaData
793
+ }, packets);
794
+ }
795
+ case "events": {
796
+ const eventsOverride = events;
797
+ const arrayData = Array.isArray(deps.data) ? deps.data : [deps.data];
798
+ const updatedDeps = {
799
+ ...deps,
800
+ data: arrayData,
801
+ id: eventsOverride
802
+ };
803
+ packets.push(...arrayLike_default(updatedDeps));
804
+ break;
805
+ }
806
+ case "alerts":
807
+ case "conclaveChallenges":
808
+ case "dailyDeals":
809
+ case "flashSales":
810
+ case "fissures":
811
+ case "globalUpgrades":
812
+ case "invasions":
813
+ case "syndicateMissions":
814
+ case "weeklyChallenges": {
815
+ const arrayData = Array.isArray(deps.data) ? deps.data : [deps.data];
816
+ const arrayDeps = {
817
+ ...deps,
818
+ data: arrayData
819
+ };
820
+ packets.push(...arrayLike_default(arrayDeps));
821
+ break;
822
+ }
823
+ case "cetusCycle":
824
+ case "earthCycle":
825
+ case "vallisCycle": {
826
+ const singleData = Array.isArray(deps.data) ? deps.data[0] : deps.data;
827
+ const cyclePackets = cycleLike_default(singleData, {
828
+ ...deps,
829
+ data: singleData
830
+ });
831
+ packets.push(...cyclePackets);
832
+ break;
833
+ }
834
+ case "persistentEnemies": {
835
+ const singleData = Array.isArray(deps.data) ? deps.data[0] : deps.data;
836
+ const overrides = checkOverrides_default(deps.key, singleData);
837
+ const packet = objectLike_default(singleData, {
838
+ ...deps,
839
+ data: singleData,
840
+ ...typeof overrides === "object" ? overrides : {},
841
+ id: typeof overrides === "string" ? overrides : overrides.eventKey
842
+ });
843
+ if (packet) packets.push(packet);
844
+ break;
845
+ }
846
+ case "sortie":
847
+ case "voidTrader":
848
+ case "arbitration":
849
+ case "sentientOutposts": {
850
+ const singleData = Array.isArray(deps.data) ? deps.data[0] : deps.data;
851
+ const override = checkOverrides_default(deps.key, singleData);
852
+ const packet = objectLike_default(singleData, {
853
+ ...deps,
854
+ data: singleData,
855
+ id: typeof override === "string" ? override : override.eventKey
856
+ });
857
+ if (packet) packets.push(packet);
858
+ break;
859
+ }
860
+ case "nightwave": {
861
+ const singleData = Array.isArray(deps.data) ? deps.data[0] : deps.data;
862
+ const nightwavePackets = nightwave_default(singleData, {
863
+ ...deps,
864
+ data: singleData
865
+ });
866
+ packets.push(...nightwavePackets);
867
+ break;
868
+ }
869
+ default: break;
870
+ }
871
+ return packets;
872
+ };
873
+ //#endregion
874
+ //#region resources/config.ts
875
+ const worldstateUrl = process.env.WORLDSTATE_URL ?? "https://api.warframe.com/cdn/worldState.php";
876
+ const kuvaUrl = process.env.KUVA_URL ?? "https://10o.io/arbitrations.json";
877
+ const sentientUrl = process.env.SENTIENT_URL ?? "https://semlar.com/anomaly.json";
878
+ const worldstateCron = process.env.WORLDSTATE_CRON ?? "25 */5 * * * *";
879
+ const externalCron = process.env.WS_EXTERNAL_CRON ?? "0 */10 * * * *";
880
+ const FEATURES = process.env.WS_EMITTER_FEATURES ? process.env.WS_EMITTER_FEATURES.split(",") : [];
881
+ //#endregion
882
+ //#region utilities/Cache.ts
883
+ /**
884
+ * Cron-based cache that periodically fetches data from a URL
885
+ */
886
+ var CronCache = class CronCache extends node_events.EventEmitter {
887
+ #url;
888
+ #pattern = "0 */10 * * * *";
889
+ #job;
890
+ #data = "";
891
+ #updating = void 0;
892
+ #logger = logger;
893
+ /**
894
+ * Create and initialize a CronCache instance
895
+ * @param url - The URL to fetch data from
896
+ * @param pattern - Optional cron pattern for update frequency
897
+ * @returns Initialized CronCache instance
898
+ */
899
+ static async make(url, pattern) {
900
+ const cache = new CronCache(url, pattern);
901
+ await cache.#update();
902
+ return cache;
903
+ }
904
+ /**
905
+ * Create a new CronCache
906
+ * @param url - The URL to fetch data from
907
+ * @param pattern - Optional cron pattern for update frequency
908
+ */
909
+ constructor(url, pattern) {
910
+ super();
911
+ this.#url = url;
912
+ if (pattern) this.#pattern = pattern;
913
+ this.#job = new cron.CronJob(this.#pattern, () => void this.#update(), void 0, true);
914
+ this.#job.start();
915
+ }
916
+ /**
917
+ * Update the cached data by fetching from the URL
918
+ * @private
919
+ */
920
+ async #update() {
921
+ this.#updating = this.#fetch();
922
+ this.#logger.debug(`update starting for ${this.#url}`);
923
+ let error;
924
+ try {
925
+ this.#data = await this.#updating;
926
+ return this.#updating;
927
+ } catch (e) {
928
+ this.#data = void 0;
929
+ error = e;
930
+ } finally {
931
+ if (this.#data) {
932
+ this.emit("update", this.#data);
933
+ this.#logger.debug(`update done for ${this.#url}`);
934
+ } else this.#logger.debug(`update failed for ${this.#url} : ${error}`);
935
+ this.#updating = void 0;
936
+ }
937
+ }
938
+ /**
939
+ * Fetch data from the configured URL
940
+ * @private
941
+ */
942
+ async #fetch() {
943
+ logger.silly(`fetching... ${this.#url}`);
944
+ const response = await fetch(this.#url);
945
+ if (!response.ok) {
946
+ const responseText = await response.text();
947
+ const errorMessage = `Failed to fetch ${this.#url}: ${response.status} ${response.statusText}`;
948
+ logger.error(errorMessage, { responseText });
949
+ throw new Error(errorMessage);
950
+ }
951
+ this.#data = await response.text();
952
+ return this.#data;
953
+ }
954
+ /**
955
+ * Get the cached data, optionally waiting for an in-progress update
956
+ * @returns The cached data
957
+ */
958
+ async get() {
959
+ if (this.#updating) {
960
+ logger.silly("returning in-progress update promise");
961
+ return this.#updating;
962
+ }
963
+ if (!this.#data) {
964
+ logger.silly("returning new update promise");
965
+ return this.#update();
966
+ }
967
+ logger.silly("returning cached data");
968
+ return this.#data;
969
+ }
970
+ /**
971
+ * Stop the cron job and cleanup
972
+ */
973
+ stop() {
974
+ this.#job.stop();
975
+ this.#logger.debug(`Cron job stopped for ${this.#url}`);
976
+ }
977
+ };
978
+ //#endregion
979
+ //#region utilities/WSCache.ts
980
+ /**
981
+ * Warframe WorldState Cache - store and retrieve current worldstate data
982
+ */
983
+ var WSCache = class {
984
+ #inner;
985
+ #kuvaCache;
986
+ #sentientCache;
987
+ #logger = logger;
988
+ #emitter;
989
+ #platform = "pc";
990
+ #language;
991
+ /**
992
+ * Set up a cache checking for data and updates to a specific worldstate set
993
+ * @param options - Configuration options
994
+ * @param options.language - Language/translation to track
995
+ * @param options.kuvaCache - Cache of kuva data, provided by Semlar
996
+ * @param options.sentientCache - Cache of sentient outpost data, provided by Semlar
997
+ * @param options.eventEmitter - Emitter to push new worldstate updates to
998
+ */
999
+ constructor({ language, kuvaCache, sentientCache, eventEmitter }) {
1000
+ this.#inner = void 0;
1001
+ this.#kuvaCache = kuvaCache;
1002
+ this.#sentientCache = sentientCache;
1003
+ this.#language = language;
1004
+ this.#emitter = eventEmitter;
1005
+ }
1006
+ /**
1007
+ * Update the current data with new data
1008
+ * @param newData - updated worldstate data
1009
+ * @private
1010
+ */
1011
+ #update = async (newData) => {
1012
+ const deps = {
1013
+ locale: this.#language,
1014
+ kuvaData: {},
1015
+ sentientData: {}
1016
+ };
1017
+ try {
1018
+ const kuvaRaw = await this.#kuvaCache.get();
1019
+ if (kuvaRaw) deps.kuvaData = JSON.parse(kuvaRaw);
1020
+ } catch (err) {
1021
+ logger.debug(`Error parsing kuva data for ${this.#language}: ${err}`);
1022
+ }
1023
+ try {
1024
+ const sentientRaw = await this.#sentientCache.get();
1025
+ if (sentientRaw) deps.sentientData = JSON.parse(sentientRaw);
1026
+ } catch (err) {
1027
+ logger.warn(`Error parsing sentient data for ${this.#language}: ${err}`);
1028
+ }
1029
+ let t;
1030
+ try {
1031
+ t = await WorldState.build(newData, deps);
1032
+ if (!t?.timestamp) return;
1033
+ } catch (err) {
1034
+ this.#logger.warn(`Error parsing worldstate data for ${this.#language}: ${err}`);
1035
+ return;
1036
+ }
1037
+ this.#inner = t;
1038
+ this.#emitter.emit("ws:update:parsed", {
1039
+ language: this.#language,
1040
+ platform: this.#platform,
1041
+ data: t
1042
+ });
1043
+ };
1044
+ /**
1045
+ * Get the latest worldstate data from this cache
1046
+ * @returns Current worldstate data
1047
+ */
1048
+ get data() {
1049
+ return this.#inner;
1050
+ }
1051
+ /**
1052
+ * Set the current data, also parses and emits data
1053
+ * @param newData - New string data to parse
1054
+ */
1055
+ set data(newData) {
1056
+ logger.debug(`got new data for ${this.#language}, parsing...`);
1057
+ this.#update(newData);
1058
+ }
1059
+ /**
1060
+ * Set the current twitter data for the worldstate
1061
+ * @param newTwitter - twitter data
1062
+ */
1063
+ set twitter(newTwitter) {
1064
+ if (!newTwitter?.length) return;
1065
+ if (this.#inner) this.#inner.twitter = newTwitter;
1066
+ }
1067
+ };
1068
+ //#endregion
1069
+ //#region handlers/Worldstate.ts
1070
+ const { locales } = warframe_worldstate_data.default;
1071
+ const debugEvents = [
1072
+ "arbitration",
1073
+ "kuva",
1074
+ "nightwave"
1075
+ ];
1076
+ /**
1077
+ * Handler for worldstate data
1078
+ */
1079
+ var Worldstate = class {
1080
+ #emitter;
1081
+ #locale;
1082
+ #worldStates = {};
1083
+ #wsRawCache;
1084
+ #kuvaCache;
1085
+ #sentientCache;
1086
+ /**
1087
+ * Set up listening for specific platform and locale if provided.
1088
+ * @param eventEmitter - Emitter to push new worldstate events to
1089
+ * @param locale - Locale (actually just language) to watch
1090
+ */
1091
+ constructor(eventEmitter, locale) {
1092
+ this.#emitter = eventEmitter;
1093
+ this.#locale = locale;
1094
+ logger.debug("starting up worldstate listener...");
1095
+ if (locale) logger.debug(`only listening for ${locale}...`);
1096
+ }
1097
+ async init() {
1098
+ this.#wsRawCache = await CronCache.make(worldstateUrl, worldstateCron);
1099
+ this.#kuvaCache = await CronCache.make(kuvaUrl, externalCron);
1100
+ this.#sentientCache = await CronCache.make(sentientUrl, externalCron);
1101
+ await this.setUpRawEmitters();
1102
+ this.setupParsedEvents();
1103
+ }
1104
+ /**
1105
+ * Set up emitting raw worldstate data
1106
+ */
1107
+ async setUpRawEmitters() {
1108
+ this.#worldStates = {};
1109
+ for await (const locale of locales) if (!this.#locale || this.#locale === locale) this.#worldStates[locale] = new WSCache({
1110
+ language: locale,
1111
+ kuvaCache: this.#kuvaCache,
1112
+ sentientCache: this.#sentientCache,
1113
+ eventEmitter: this.#emitter
1114
+ });
1115
+ this.#wsRawCache.on("update", (dataStr) => {
1116
+ this.#emitter.emit("ws:update:raw", {
1117
+ platform: "pc",
1118
+ data: dataStr
1119
+ });
1120
+ });
1121
+ this.#emitter.on("ws:update:raw", ({ data }) => {
1122
+ logger.debug("ws:update:raw - updating locales data");
1123
+ locales.forEach((locale) => {
1124
+ if (!this.#locale || this.#locale === locale) this.#worldStates[locale].data = data;
1125
+ });
1126
+ });
1127
+ }
1128
+ /**
1129
+ * Set up listeners for the parsed worldstate updates
1130
+ */
1131
+ setupParsedEvents() {
1132
+ this.#emitter.on("ws:update:parsed", ({ language, platform, data }) => {
1133
+ const packet = {
1134
+ platform,
1135
+ worldstate: data,
1136
+ language
1137
+ };
1138
+ this.parseEvents(packet);
1139
+ });
1140
+ }
1141
+ /**
1142
+ * Parse new worldstate events
1143
+ * @param packet - Object containing worldstate, platform, and language
1144
+ */
1145
+ parseEvents({ worldstate, platform, language = "en" }) {
1146
+ const cycleStart = Date.now();
1147
+ const packets = [];
1148
+ Object.keys(worldstate).forEach((key) => {
1149
+ const wsRecord = worldstate;
1150
+ if (worldstate && wsRecord[key]) {
1151
+ const packet = parse_default({
1152
+ data: wsRecord[key],
1153
+ key,
1154
+ language,
1155
+ platform,
1156
+ cycleStart
1157
+ });
1158
+ if (Array.isArray(packet)) {
1159
+ if (packet.length) packets.push(...packet.filter((p) => p));
1160
+ } else if (packet) packets.push(packet);
1161
+ }
1162
+ });
1163
+ lastUpdated[platform][language] = Date.now();
1164
+ packets.filter((p) => !!p && !!p.id).forEach((packet) => {
1165
+ this.emit("ws:update:event", packet);
1166
+ });
1167
+ }
1168
+ /**
1169
+ * Emit an event with given id
1170
+ * @param id - Id of the event to emit
1171
+ * @param packet - Data packet to emit
1172
+ */
1173
+ emit(id, packet) {
1174
+ if (debugEvents.includes(packet.key)) logger.warn(packet.key);
1175
+ logger.debug(`ws:update:event - emitting ${packet.id}`);
1176
+ delete packet.cycleStart;
1177
+ this.#emitter.emit(id, packet);
1178
+ }
1179
+ /**
1180
+ * get a specific worldstate version
1181
+ * @param language - Locale of the worldstate
1182
+ * @returns Worldstate corresponding to provided data
1183
+ * @throws When the platform or locale aren't tracked and aren't updated
1184
+ */
1185
+ get(language = "en") {
1186
+ logger.debug(`getting worldstate ${language}...`);
1187
+ if (this.#worldStates?.[language]) return this.#worldStates?.[language]?.data;
1188
+ throw new Error(`Language (${language}) not tracked.\nEnsure that the parameters passed are correct`);
1189
+ }
1190
+ destroy() {
1191
+ this.#wsRawCache?.stop();
1192
+ this.#kuvaCache?.stop();
1193
+ this.#sentientCache?.stop();
1194
+ }
1195
+ };
1196
+ //#endregion
1197
+ //#region index.ts
1198
+ var WorldstateEmitter = class WorldstateEmitter extends node_events.default {
1199
+ #locale;
1200
+ #worldstate;
1201
+ #twitter;
1202
+ #rss;
1203
+ static async make({ locale, features } = {}) {
1204
+ const emitter = new WorldstateEmitter({ locale });
1205
+ await emitter.#init(features?.length ? features : FEATURES);
1206
+ return emitter;
1207
+ }
1208
+ /**
1209
+ * Pull in and instantiate emitters
1210
+ * @param options - Configuration options
1211
+ */
1212
+ constructor({ locale } = {}) {
1213
+ super();
1214
+ this.#locale = locale;
1215
+ }
1216
+ async #init(features) {
1217
+ if (features.includes("rss")) this.#rss = new RSS(this);
1218
+ if (features.includes("worldstate")) {
1219
+ this.#worldstate = new Worldstate(this, this.#locale);
1220
+ await this.#worldstate.init();
1221
+ }
1222
+ if (features.includes("twitter")) this.#twitter = new TwitterCache(this);
1223
+ logger.silly("hey look, i started up...");
1224
+ this.setupLogging();
1225
+ }
1226
+ /**
1227
+ * Set up internal logging
1228
+ * @private
1229
+ */
1230
+ setupLogging() {
1231
+ this.on("error", logger.error);
1232
+ this.on("rss", (body) => logger.silly(`emitted: ${body.id}`));
1233
+ this.on("ws:update:raw", (body) => logger.silly(`emitted raw: ${body.platform}`));
1234
+ this.on("ws:update:parsed", (body) => logger.silly(`emitted parsed: ${body.platform} in ${body.language}`));
1235
+ this.on("ws:update:event", (body) => logger.silly(`emitted event: ${body.id} ${body.platform} in ${body.language}`));
1236
+ this.on("tweet", (body) => logger.silly(`emitted: ${body.id}`));
1237
+ }
1238
+ /**
1239
+ * Get current rss feed items
1240
+ * @returns RSS feed items
1241
+ */
1242
+ getRss() {
1243
+ if (!this.#rss) return void 0;
1244
+ return this.#rss.feeder.list.map((i) => ({
1245
+ url: i.url,
1246
+ items: i.items
1247
+ }));
1248
+ }
1249
+ /**
1250
+ * Get a specific worldstate, defaulting to 'pc' for the platform and 'en' for the language
1251
+ * @param language - locale/language to fetch
1252
+ * @returns Requested worldstate
1253
+ */
1254
+ getWorldstate(language = "en") {
1255
+ if (!this.#worldstate) return void 0;
1256
+ return this.#worldstate?.get(language);
1257
+ }
1258
+ get debug() {
1259
+ return {
1260
+ rss: FEATURES.includes("rss") ? this.getRss() : void 0,
1261
+ worldstate: FEATURES.includes("worldstate") ? this.#worldstate?.get() : void 0,
1262
+ twitter: this.#twitter?.clientInfoValid ? this.#twitter.getData() : void 0
1263
+ };
1264
+ }
1265
+ /**
1266
+ * Get Twitter data
1267
+ * @returns Promised twitter data
1268
+ */
1269
+ async getTwitter() {
1270
+ return this.#twitter?.clientInfoValid ? this.#twitter.getData() : void 0;
1271
+ }
1272
+ destroy() {
1273
+ if (this.#rss) {
1274
+ this.#rss.destroy();
1275
+ this.#rss = void 0;
1276
+ }
1277
+ if (this.#worldstate) {
1278
+ this.#worldstate.destroy();
1279
+ this.#worldstate = void 0;
1280
+ }
1281
+ if (this.#twitter) {
1282
+ this.#twitter.dispose();
1283
+ this.#twitter = void 0;
1284
+ }
1285
+ }
1286
+ };
1287
+ //#endregion
1288
+ module.exports = WorldstateEmitter;