worldstate-emitter 2.2.13 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,12 +1,14 @@
1
1
  {
2
2
  "name": "worldstate-emitter",
3
- "version": "2.2.13",
4
- "description": "Event emitter for worldstate & other warframe events",
3
+ "version": "2.3.0",
4
+ "description": "Event emitter for Warframe worldstate & other events - TypeScript support included",
5
5
  "keywords": [
6
6
  "warframe",
7
7
  "worldstate",
8
8
  "event",
9
- "emitter"
9
+ "emitter",
10
+ "typescript",
11
+ "types"
10
12
  ],
11
13
  "homepage": "https://wfcd.github.io/worldstate-emitter/",
12
14
  "bugs": {
@@ -20,50 +22,73 @@
20
22
  "author": "tobiah <tobiah@protonmail.com>",
21
23
  "type": "module",
22
24
  "exports": {
23
- ".": "./index.js"
25
+ ".": {
26
+ "types": "./dist/index.d.mts",
27
+ "import": "./dist/index.mjs"
28
+ }
24
29
  },
25
- "main": "index.js",
30
+ "main": "./dist/index.mjs",
31
+ "types": "./dist/index.d.mts",
26
32
  "directories": {
27
33
  "test": "test/specs"
28
34
  },
35
+ "files": [
36
+ "dist/",
37
+ "LICENSE"
38
+ ],
29
39
  "scripts": {
30
- "build:docs": "jsdoc -c jsdoc-config.json -d docs",
31
- "coverage": "npm test && npm run report && npx coveralls < coverage/lcov.info",
32
- "dev": "nodemon test/tester.js",
33
- "lint": "eslint .",
34
- "lint:fix": "eslint . --fix",
35
- "prepare": "husky && npx install-peerdeps @wfcd/eslint-config@latest -S",
40
+ "build": "tsdown",
41
+ "build:docs": "typedoc",
42
+ "precoverage": "npm run build",
43
+ "coverage": "c8 mocha --config .mocharc.integration.yml && npm run report && npx coveralls < coverage/lcov.info",
44
+ "dev": "nodemon --exec tsx test/tester.ts",
45
+ "lint": "biome check",
46
+ "lint:fix": "biome check --write",
47
+ "prepare": "husky",
48
+ "prepublishOnly": "npm run build",
36
49
  "report": "c8 report --reporter=text-lcov > coverage/lcov.info",
37
- "pretest": "npm i --no-save warframe-worldstate-data@^3 warframe-worldstate-parser@^5",
38
- "test": "c8 mocha"
50
+ "pretest": "npm i --no-save warframe-worldstate-data@^3 warframe-worldstate-parser@^5 && npm run build",
51
+ "test": "LOG_LEVEL=silent mocha",
52
+ "test:all": "npm test && npm run test:integration",
53
+ "test:integration": "LOG_LEVEL=silent mocha --config .mocharc.integration.yml"
39
54
  },
40
55
  "dependencies": {
41
56
  "cron": "^4.3.4",
42
57
  "rss-feed-emitter": "^3.2.4",
58
+ "sanitize-html": "^2.17.0",
43
59
  "twitter": "^1.7.1"
44
60
  },
45
61
  "devDependencies": {
62
+ "@biomejs/biome": "^2.3.11",
46
63
  "@commitlint/cli": "^20.1.0",
47
64
  "@commitlint/config-conventional": "^20.0.0",
48
- "@wfcd/eslint-config": "latest",
65
+ "@types/chai": "^5.2.3",
66
+ "@types/mocha": "^10.0.10",
67
+ "@types/node": "^20.19.28",
68
+ "@types/sanitize-html": "^2.16.0",
69
+ "@types/sinon": "^21.0.0",
70
+ "@types/twitter": "^1.7.4",
49
71
  "c8": "^10.1.3",
50
72
  "chai": "^6.2.1",
51
73
  "husky": "^9.1.7",
52
- "install-peerdeps": "^3.0.7",
53
74
  "lint-staged": "^16.2.7",
54
75
  "mocha": "^11.7.5",
55
76
  "nodemon": "^3.1.11",
56
- "prettier": "^3.7.2"
77
+ "sinon": "^21.0.1",
78
+ "tsdown": "^0.19.0",
79
+ "tsx": "^4.21.0",
80
+ "typedoc": "^0.28.15",
81
+ "typescript": "^5.9.3"
57
82
  },
58
83
  "peerDependencies": {
59
84
  "warframe-worldstate-data": "^3.x",
60
- "warframe-worldstate-parser": "^5.x"
85
+ "warframe-worldstate-parser": "^5.2.14"
61
86
  },
62
87
  "optionalDependencies": {
63
88
  "winston": "^3.18.3"
64
89
  },
65
90
  "engines": {
66
- "node": ">= 18.19.0"
91
+ "node": ">=20.10.0"
67
92
  },
68
93
  "publishConfig": {
69
94
  "provenance": true
package/.babelrc.json DELETED
@@ -1,4 +0,0 @@
1
- {
2
- "plugins": ["@babel/plugin-transform-class-properties", "@babel/plugin-transform-private-methods"],
3
- "presets": ["@babel/preset-env"]
4
- }
package/.commitlintrc.yml DELETED
@@ -1,5 +0,0 @@
1
- extends:
2
- - '@commitlint/config-conventional'
3
- rules:
4
- body-max-line-length:
5
- - 0
package/.eslintignore DELETED
@@ -1,5 +0,0 @@
1
- .github/**
2
- docs/**
3
- resources/**
4
- !resources/config.js
5
- types/**
package/.eslintrc.yml DELETED
@@ -1,9 +0,0 @@
1
- parser: '@babel/eslint-parser'
2
- parserOptions:
3
- ecmaFeatures:
4
- modules: true
5
- ecmaVersion: 6
6
- sourceType: module
7
- extends: '@wfcd/eslint-config/esm'
8
- rules:
9
- no-underscore-dangle: 'off'
package/.husky/commit-msg DELETED
@@ -1 +0,0 @@
1
- npx --no -- commitlint --edit $1
package/.husky/pre-commit DELETED
@@ -1 +0,0 @@
1
- npx lint-staged
package/.lintstagedrc.yml DELETED
@@ -1,9 +0,0 @@
1
- '*.js':
2
- - eslint --cache --fix
3
- - npm test
4
- package*.json:
5
- - prettier --write
6
- - npm dedupe
7
- - npx sort-package-json
8
- '*.{json,yml,yaml}':
9
- - prettier --write
package/.mocharc.yml DELETED
@@ -1,3 +0,0 @@
1
- exit: true
2
- spec: test/specs/**/*.spec.js
3
- timeout: 250000
package/.nvmrc DELETED
@@ -1 +0,0 @@
1
- lts/jod
package/.nycrc.yml DELETED
@@ -1,4 +0,0 @@
1
- reporter:
2
- - lcov
3
- - text
4
- skip-full: true
package/.prettierrc DELETED
@@ -1 +0,0 @@
1
- "@wfcd/eslint-config/prettier"
package/.releaserc.yml DELETED
@@ -1 +0,0 @@
1
- branches: master
package/SECURITY.md DELETED
@@ -1,17 +0,0 @@
1
- # Security Policy
2
-
3
- ## Supported Versions
4
-
5
- All versions are supported, but it's recommended to stay on the always current version.
6
- There may be known vulnerabilities in development dependencies that are avoidable.
7
-
8
- | Version | Supported |
9
- | ------- | ------------------ |
10
- | 1.x+ | :white_check_mark: |
11
- | < 1.0 | :x:
12
-
13
- ## Reporting a Vulnerability
14
-
15
- Vulnerabilities from version updates should have automated PRs from dependabot.
16
-
17
- If there is a vulnerability in the code itself, please submit an issue (or also a PR if you have the solution), mentioning the vulnerability.
package/handlers/RSS.js DELETED
@@ -1,66 +0,0 @@
1
- import RssFeedEmitter from 'rss-feed-emitter';
2
-
3
- import feeds from '../resources/rssFeeds.json' with { type: 'json' };
4
- import { logger } from '../utilities/index.js';
5
-
6
- /**
7
- * RSS Emitter, leverages [rss-feed-emitter](https://npmjs.org/package/rss-feed-emitter)
8
- */
9
- export default class RSS {
10
- /**
11
- * Set up emitting events for warframe forum entries
12
- * @param {EventEmitter} eventEmitter Emitter to send events from
13
- */
14
- constructor(eventEmitter) {
15
- this.logger = logger;
16
- this.emitter = eventEmitter;
17
- this.feeder = new RssFeedEmitter({
18
- userAgent: 'WFCD Feed Notifier',
19
- skipFirstLoad: true,
20
- });
21
-
22
- feeds.forEach((feed) => {
23
- this.feeder.add({ url: feed.url, timeout: 30000 });
24
- });
25
- this.logger.debug('RSS Feed active');
26
-
27
- this.start = Date.now();
28
- this.feeder.on('error', this.logger.error.bind(this.logger));
29
- this.feeder.on('new-item', this.handleNew.bind(this));
30
- }
31
-
32
- handleNew(item) {
33
- try {
34
- if (Object.keys(item.image).length) {
35
- this.logger.debug(`Image: ${JSON.stringify(item.image)}`);
36
- }
37
- if (new Date(item.pubDate).getTime() <= this.start) return;
38
-
39
- const feed = feeds.filter((feedEntry) => feedEntry.url === item.meta.link)[0];
40
- let firstImg = ((item.description || '').match(/<img.*src="(.*)".*>/i) || [])[1];
41
- if (!firstImg) {
42
- firstImg = feed.defaultAttach;
43
- } else if (firstImg.startsWith('//')) {
44
- firstImg = firstImg.replace('//', 'https://');
45
- }
46
-
47
- const rssSummary = {
48
- body: (item.description || '\u200B').replace(/<(?:.|\n)*?>/gm, '').replace(/\n\n+\s*/gm, '\n\n'),
49
- url: item.link,
50
- timestamp: item.pubDate,
51
- description: item.meta.description,
52
- author: feed.author || {
53
- name: 'Warframe Forums',
54
- url: item['rss:link']['#'],
55
- icon_url: 'https://i.imgur.com/hE2jdpv.png',
56
- },
57
- title: item.title,
58
- image: firstImg,
59
- id: feed.key,
60
- };
61
- this.emitter.emit('rss', rssSummary);
62
- } catch (error) {
63
- this.logger.error(error);
64
- }
65
- }
66
- }
@@ -1,169 +0,0 @@
1
- import Twitter from 'twitter';
2
-
3
- import toWatch from '../resources/tweeters.json' with { type: 'json' };
4
- import { logger } from '../utilities/index.js';
5
- import { twiClientInfo, TWITTER_TIMEOUT } from '../utilities/env.js';
6
-
7
- const determineTweetType = (tweet) => {
8
- if (tweet.in_reply_to_status_id) {
9
- return 'reply';
10
- }
11
- if (tweet.quoted_status_id) {
12
- return 'quote';
13
- }
14
- if (tweet.retweeted_status) {
15
- return 'retweet';
16
- }
17
- return 'tweet';
18
- };
19
-
20
- const parseAuthor = (tweet) => ({
21
- name: tweet.user.name,
22
- handle: tweet.user.screen_name,
23
- url: `https://twitter.com/${tweet.user.screen_name}`,
24
- avatar: `${tweet.user.profile_image_url.replace('_normal.jpg', '.jpg')}`,
25
- });
26
-
27
- const parseQuoted = (tweet, type) =>
28
- tweet[type]
29
- ? {
30
- text: tweet[type].full_text,
31
- author: {
32
- name: tweet[type].user.name,
33
- handle: tweet[type].user.screen_name,
34
- },
35
- }
36
- : undefined;
37
-
38
- const parseTweet = (tweets, watchable) => {
39
- const [tweet] = tweets;
40
- const type = determineTweetType(tweet);
41
- return {
42
- id: `twitter.${watchable.plain}.${type}`,
43
- uniqueId: String(tweets[0].id_str),
44
- text: tweet.full_text,
45
- url: `https://twitter.com/${tweet.user.screen_name}/status/${tweet.id_str}`,
46
- mediaUrl: tweet.entities.media ? tweet.entities.media[0].media_url : undefined,
47
- isReply: typeof tweet.in_reply_to_status_id !== 'undefined',
48
- author: parseAuthor(tweet),
49
- quote: parseQuoted(tweet, 'quoted_status'),
50
- retweet: parseQuoted(tweet, 'retweeted_status'),
51
- createdAt: new Date(tweet.created_at),
52
- };
53
- };
54
-
55
- /**
56
- * Twitter event handler
57
- */
58
- export default class TwitterCache {
59
- /**
60
- * Create a new Twitter self-updating cache
61
- * @param {EventEmitter} eventEmitter emitter to push new tweets to
62
- */
63
- constructor(eventEmitter) {
64
- this.emitter = eventEmitter;
65
- this.timeout = TWITTER_TIMEOUT;
66
- this.clientInfoValid = twiClientInfo.consumer_key && twiClientInfo.consumer_secret && twiClientInfo.bearer_token;
67
- this.initClient(twiClientInfo);
68
- }
69
-
70
- initClient(clientInfo) {
71
- try {
72
- if (this.clientInfoValid) {
73
- this.client = new Twitter(clientInfo);
74
-
75
- // don't attempt anything else if authentication fails
76
- this.toWatch = toWatch;
77
- this.currentData = undefined;
78
- this.lastUpdated = Date.now() - 60000;
79
- this.updateInterval = setInterval(() => this.update(), this.timeout);
80
- this.update();
81
- } else {
82
- logger.warn(`Twitter client not initialized... invalid token: ${clientInfo.bearer_token}`);
83
- }
84
- } catch (err) {
85
- this.client = undefined;
86
- this.clientInfoValid = false;
87
- logger.error(err);
88
- }
89
- }
90
-
91
- /**
92
- * Force the cache to update
93
- * @returns {Promise} the currently updating promise.
94
- */
95
- async update() {
96
- if (!this.clientInfoValid) return undefined;
97
-
98
- if (!this.toWatch) {
99
- logger.verbose('Not processing twitter, no data to watch.');
100
- return undefined;
101
- }
102
-
103
- if (!this.client) {
104
- logger.verbose('Not processing twitter, no client to connect.');
105
- return undefined;
106
- }
107
-
108
- this.updating = this.getParseableData();
109
-
110
- return this.updating;
111
- }
112
-
113
- /**
114
- * Get data able to be parsed from twitter.
115
- * @returns {Promise.<Array.<Object>>} Tweets
116
- */
117
- async getParseableData() {
118
- logger.silly('Starting Twitter update...');
119
- const parsedData = [];
120
- try {
121
- await Promise.all(
122
- this.toWatch.map(async (watchable) => {
123
- const tweets = await this.client.get('statuses/user_timeline', {
124
- screen_name: watchable.acc_name,
125
- tweet_mode: 'extended',
126
- count: 1,
127
- });
128
- const tweet = parseTweet(tweets, watchable);
129
- parsedData.push(tweet);
130
-
131
- if (tweet.createdAt.getTime() > this.lastUpdated) {
132
- this.emitter.emit('tweet', tweet);
133
- }
134
- })
135
- );
136
- } catch (error) {
137
- this.onError(error);
138
- }
139
- this.lastUpdated = Date.now();
140
- return parsedData;
141
- }
142
-
143
- /**
144
- * Handle errors that arise while fetching data from twitter
145
- * @param {Error} error twitter error
146
- */
147
- onError(error) {
148
- if (error[0] && error[0].code === 32) {
149
- this.clientInfoValid = false;
150
- logger.info('wiping twitter client data, could not authenticate...');
151
- } else {
152
- logger.debug(JSON.stringify(error));
153
- }
154
- }
155
-
156
- /**
157
- * Get the current data or a promise with the current data
158
- * @returns {Promise.<Object> | Object} either the current data
159
- * if it's not updating, or the promise returning the new data
160
- */
161
- async getData() {
162
- if (!this.clientInfoValid) return undefined;
163
-
164
- if (this.updating) {
165
- return this.updating;
166
- }
167
- return this.currentData;
168
- }
169
- }
@@ -1,154 +0,0 @@
1
- import wsData from 'warframe-worldstate-data';
2
-
3
- import WSCache from '../utilities/WSCache.js';
4
- import { logger, lastUpdated } from '../utilities/index.js';
5
- import Cache from '../utilities/Cache.js';
6
- import { sentientUrl, kuvaUrl, worldstateUrl, externalCron, worldstateCron } from '../resources/config.js';
7
-
8
- import parseNew from './events/parse.js';
9
-
10
- const { locales } = wsData;
11
- const debugEvents = ['arbitration', 'kuva', 'nightwave'];
12
-
13
- /**
14
- * Handler for worldstate data
15
- */
16
- export default class Worldstate {
17
- #emitter;
18
- #locale;
19
- #worldStates = {};
20
- #wsRawCache;
21
- #kuvaCache;
22
- #sentientCache;
23
-
24
- /**
25
- * Set up listening for specific platform and locale if provided.
26
- * @param {EventEmitter} eventEmitter Emitter to push new worldstate events to
27
- * @param {string} locale Locale (actually just language) to watch
28
- */
29
- constructor(eventEmitter, locale) {
30
- this.#emitter = eventEmitter;
31
- this.#locale = locale;
32
- logger.debug('starting up worldstate listener...');
33
- if (locale) {
34
- logger.debug(`only listening for ${locale}...`);
35
- }
36
- }
37
-
38
- async init() {
39
- this.#wsRawCache = await Cache.make(worldstateUrl, worldstateCron);
40
- this.#kuvaCache = await Cache.make(kuvaUrl, externalCron);
41
- this.#sentientCache = await Cache.make(sentientUrl, externalCron);
42
-
43
- await this.setUpRawEmitters();
44
- this.setupParsedEvents();
45
- }
46
-
47
- /**
48
- * Set up emitting raw worldstate data
49
- */
50
- async setUpRawEmitters() {
51
- this.#worldStates = {};
52
-
53
- // eslint-disable-next-line no-restricted-syntax
54
- for await (const locale of locales) {
55
- if (!this.#locale || this.#locale === locale) {
56
- this.#worldStates[locale] = new WSCache({
57
- language: locale,
58
- kuvaCache: this.#kuvaCache,
59
- sentientCache: this.#sentientCache,
60
- eventEmitter: this.#emitter,
61
- });
62
- }
63
- }
64
-
65
- /* listen for the raw cache updates so we can emit them from the super emitter */
66
- this.#wsRawCache.on('update', (dataStr) => {
67
- this.#emitter.emit('ws:update:raw', { platform: 'pc', data: dataStr });
68
- });
69
-
70
- /* when the raw emits happen, parse them and store them on parsed worldstate caches */
71
- this.#emitter.on('ws:update:raw', ({ data }) => {
72
- logger.debug('ws:update:raw - updating locales data');
73
- locales.forEach((locale) => {
74
- if (!this.#locale || this.#locale === locale) {
75
- this.#worldStates[locale].data = data;
76
- }
77
- });
78
- });
79
- }
80
-
81
- /**
82
- * Set up listeners for the parsed worldstate updates
83
- */
84
- setupParsedEvents() {
85
- this.#emitter.on('ws:update:parsed', ({ language, platform, data }) => {
86
- const packet = { platform, worldstate: data, language };
87
- this.parseEvents(packet);
88
- });
89
- }
90
-
91
- /**
92
- * Parse new worldstate events
93
- * @param {Object} worldstate worldstate to find packets from
94
- * @param {string} platform platform the worldstate corresponds to
95
- * @param {string} [language='en'] language of the worldstate (defaults to 'en')
96
- */
97
- parseEvents({ worldstate, platform, language = 'en' }) {
98
- const cycleStart = Date.now();
99
- const packets = [];
100
- Object.keys(worldstate).forEach(async (key) => {
101
- if (worldstate && worldstate[key]) {
102
- const packet = parseNew({
103
- data: worldstate[key],
104
- key,
105
- language,
106
- platform,
107
- cycleStart,
108
- });
109
-
110
- if (Array.isArray(packet)) {
111
- if (packet.length) {
112
- packets.push(...packet.filter((p) => p && p));
113
- }
114
- } else if (packet) {
115
- packets.push(packet);
116
- }
117
- }
118
- });
119
-
120
- lastUpdated[platform][language] = Date.now();
121
- packets
122
- .filter((p) => p && p.id && packets)
123
- .forEach((packet) => {
124
- this.emit('ws:update:event', packet);
125
- });
126
- }
127
-
128
- /**
129
- * Emit an event with given id
130
- * @param {string} id Id of the event to emit
131
- * @param {Object} packet Data packet to emit
132
- */
133
- emit(id, packet) {
134
- if (debugEvents.includes(packet.key)) logger.warn(packet.key);
135
-
136
- logger.debug(`ws:update:event - emitting ${packet.id}`);
137
- delete packet.cycleStart;
138
- this.#emitter.emit(id, packet);
139
- }
140
-
141
- /**
142
- * get a specific worldstate version
143
- * @param {string} [language='en'] Locale of the worldsttate
144
- * @returns {Object} Worldstate corresponding to provided data
145
- * @throws {Error} when the platform or locale aren't tracked and aren't updated
146
- */
147
- get(language = 'en') {
148
- logger.debug(`getting worldstate ${language}...`);
149
- if (this.#worldStates?.[language]) {
150
- return this.#worldStates?.[language]?.data;
151
- }
152
- throw new Error(`Language (${language}) not tracked.\nEnsure that the parameters passed are correct`);
153
- }
154
- }
@@ -1,27 +0,0 @@
1
- import { logger } from '../../utilities/index.js';
2
-
3
- import checkOverrides from './checkOverrides.js';
4
- import objectLike from './objectLike.js';
5
-
6
- /**
7
- * arrayLike are all just arrays of objectLike
8
- * @param {Object} deps dependencies for processing
9
- * @param {Object[]} packets packets to emit
10
- * @returns {Object|Object[]} object(s) to emit from arrayLike processing
11
- */
12
- export default (deps, packets) => {
13
- try {
14
- deps.data.forEach((arrayItem) => {
15
- const k = checkOverrides(deps.key, arrayItem);
16
- packets.push(
17
- objectLike(arrayItem, {
18
- ...deps,
19
- id: k,
20
- })
21
- );
22
- });
23
- return packets;
24
- } catch (err) {
25
- logger.error(err);
26
- }
27
- };
@@ -1,17 +0,0 @@
1
- import * as eKeyOverrides from './eKeyOverrides.js';
2
-
3
- /**
4
- * Find overrides for the provided key
5
- * @param {string} key worldsate field to find overrides
6
- * @param {Object} data data corresponding to the key from provided worldstate
7
- * @returns {string} overrided key
8
- */
9
- export default (key, data) => {
10
- if (typeof eKeyOverrides[key] === 'string') {
11
- return eKeyOverrides[key];
12
- }
13
- if (typeof eKeyOverrides[key] === 'function') {
14
- return eKeyOverrides[key](data);
15
- }
16
- return key;
17
- };
@@ -1,38 +0,0 @@
1
- import { between, lastUpdated, fromNow } from '../../utilities/index.js';
2
-
3
- /**
4
- * @typedef {Object} CycleLike
5
- * @property {string} state
6
- * @property {string} key
7
- * @property {Date} activation
8
- */
9
-
10
- /**
11
- * CylceData parser
12
- * @param {CycleLike} cycleData data for parsing all cycles like this
13
- * @param {Deps} deps dependencies for processing
14
- * @returns {Object[]}
15
- */
16
- export default (cycleData, deps) => {
17
- const packet = {
18
- ...deps,
19
- data: cycleData,
20
- id: `${deps.key.replace('Cycle', '')}.${cycleData.state}`,
21
- };
22
-
23
- const last = new Date(lastUpdated[deps.platform][deps.language]);
24
- const activation = new Date(cycleData.activation);
25
- const start = new Date(deps.cycleStart);
26
-
27
- const packets = [];
28
- if (between(last, activation, start)) {
29
- packets.push(packet);
30
- }
31
-
32
- const timePacket = {
33
- ...packet,
34
- id: `${packet.id}.${Math.round(fromNow(deps.data.expiry) / 60000)}`,
35
- };
36
- packets.push(timePacket);
37
- return packets;
38
- };