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.d.ts +56 -0
- package/dist/index.js +1288 -0
- package/dist/index.mjs +2 -23
- package/package.json +14 -6
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;
|