diff --git a/package-lock.json b/package-lock.json
index 4f4d50365..4d94597af 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -20,25 +20,24 @@
"dev": true
},
"@babel/core": {
- "version": "7.11.0",
- "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.11.0.tgz",
- "integrity": "sha512-mkLq8nwaXmDtFmRkQ8ED/eA2CnVw4zr7dCztKalZXBvdK5EeNUAesrrwUqjQEzFgomJssayzB0aqlOsP1vGLqg==",
- "dev": true,
- "requires": {
- "@babel/code-frame": "^7.10.4",
- "@babel/generator": "^7.11.0",
- "@babel/helper-module-transforms": "^7.11.0",
- "@babel/helpers": "^7.10.4",
- "@babel/parser": "^7.11.0",
- "@babel/template": "^7.10.4",
- "@babel/traverse": "^7.11.0",
- "@babel/types": "^7.11.0",
+ "version": "7.12.13",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.12.13.tgz",
+ "integrity": "sha512-BQKE9kXkPlXHPeqissfxo0lySWJcYdEP0hdtJOH/iJfDdhOCcgtNCjftCJg3qqauB4h+lz2N6ixM++b9DN1Tcw==",
+ "dev": true,
+ "requires": {
+ "@babel/code-frame": "^7.12.13",
+ "@babel/generator": "^7.12.13",
+ "@babel/helper-module-transforms": "^7.12.13",
+ "@babel/helpers": "^7.12.13",
+ "@babel/parser": "^7.12.13",
+ "@babel/template": "^7.12.13",
+ "@babel/traverse": "^7.12.13",
+ "@babel/types": "^7.12.13",
"convert-source-map": "^1.7.0",
"debug": "^4.1.0",
"gensync": "^1.0.0-beta.1",
"json5": "^2.1.2",
"lodash": "^4.17.19",
- "resolve": "^1.3.2",
"semver": "^5.4.1",
"source-map": "^0.5.0"
},
@@ -51,12 +50,6 @@
"requires": {
"minimist": "^1.2.5"
}
- },
- "semver": {
- "version": "5.7.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
- "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
- "dev": true
}
}
},
@@ -71,20 +64,10 @@
"semver": "^6.3.0"
},
"dependencies": {
- "eslint-scope": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.0.tgz",
- "integrity": "sha512-iiGRvtxWqgtx5m8EyQUJihBloE4EnYeGE/bz1wSPwJE6tZuJUtHlhqDM4Xj2ukE8Dyy1+HCZ4hE0fzIVMzb58w==",
- "dev": true,
- "requires": {
- "esrecurse": "^4.1.0",
- "estraverse": "^4.1.1"
- }
- },
- "eslint-visitor-keys": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz",
- "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==",
+ "semver": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"dev": true
}
}
@@ -129,14 +112,6 @@
"@babel/helper-validator-option": "^7.12.11",
"browserslist": "^4.14.5",
"semver": "^5.5.0"
- },
- "dependencies": {
- "semver": {
- "version": "5.7.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
- "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
- "dev": true
- }
}
},
"@babel/helper-create-class-features-plugin": {
@@ -977,14 +952,6 @@
"@babel/types": "^7.12.13",
"core-js-compat": "^3.8.0",
"semver": "^5.5.0"
- },
- "dependencies": {
- "semver": {
- "version": "5.7.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
- "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
- "dev": true
- }
}
},
"@babel/preset-modules": {
@@ -2064,7 +2031,7 @@
}
},
"erlpack": {
- "version": "github:discordapp/erlpack#c514d36ec81a7a61ef90b75df261025ab046574d",
+ "version": "github:discordapp/erlpack#e27db8f82892bdb9b28a0547cc394d68b5d2242d",
"from": "github:discordapp/erlpack",
"requires": {
"bindings": "^1.5.0",
@@ -2226,6 +2193,22 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
+ "eslint-scope": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
+ "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
+ "dev": true,
+ "requires": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^4.1.1"
+ }
+ },
+ "eslint-visitor-keys": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz",
+ "integrity": "sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==",
+ "dev": true
+ },
"globals": {
"version": "12.4.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz",
@@ -2387,32 +2370,13 @@
}
},
"eslint-scope": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
- "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.0.tgz",
+ "integrity": "sha512-iiGRvtxWqgtx5m8EyQUJihBloE4EnYeGE/bz1wSPwJE6tZuJUtHlhqDM4Xj2ukE8Dyy1+HCZ4hE0fzIVMzb58w==",
"dev": true,
"requires": {
- "esrecurse": "^4.3.0",
+ "esrecurse": "^4.1.0",
"estraverse": "^4.1.1"
- },
- "dependencies": {
- "esrecurse": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
- "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
- "dev": true,
- "requires": {
- "estraverse": "^5.2.0"
- },
- "dependencies": {
- "estraverse": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz",
- "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==",
- "dev": true
- }
- }
- }
}
},
"eslint-utils": {
@@ -2422,20 +2386,12 @@
"dev": true,
"requires": {
"eslint-visitor-keys": "^1.1.0"
- },
- "dependencies": {
- "eslint-visitor-keys": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz",
- "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==",
- "dev": true
- }
}
},
"eslint-visitor-keys": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz",
- "integrity": "sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==",
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz",
+ "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==",
"dev": true
},
"espree": {
@@ -2447,14 +2403,6 @@
"acorn": "^7.4.0",
"acorn-jsx": "^5.3.1",
"eslint-visitor-keys": "^1.3.0"
- },
- "dependencies": {
- "eslint-visitor-keys": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz",
- "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==",
- "dev": true
- }
}
},
"esprima": {
@@ -2481,12 +2429,20 @@
}
},
"esrecurse": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz",
- "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==",
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
"dev": true,
"requires": {
- "estraverse": "^4.1.0"
+ "estraverse": "^5.2.0"
+ },
+ "dependencies": {
+ "estraverse": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz",
+ "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==",
+ "dev": true
+ }
}
},
"estraverse": {
@@ -3808,9 +3764,9 @@
"integrity": "sha1-VjsZx8HeiS4Jv8Ty/DDjwn8JUrk="
},
"semver": {
- "version": "6.3.0",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
- "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
"dev": true
},
"seq-queue": {
diff --git a/package.json b/package.json
index b8886d540..5fb610c27 100644
--- a/package.json
+++ b/package.json
@@ -72,7 +72,7 @@
"zlib-sync": "^0.1.7"
},
"devDependencies": {
- "@babel/core": "^7.11.0",
+ "@babel/core": "^7.12.13",
"@babel/eslint-parser": "^7.12.13",
"@babel/plugin-proposal-class-properties": "^7.12.13",
"@babel/plugin-proposal-private-methods": "^7.12.13",
diff --git a/src/CommonFunctions.js b/src/CommonFunctions.js
index bf1430d27..3213b0168 100644
--- a/src/CommonFunctions.js
+++ b/src/CommonFunctions.js
@@ -15,7 +15,7 @@ const welcomes = require('./resources/welcomes.json');
const {
eventTypes, rewardTypes, opts, fissures, syndicates, twitter, conclave, deals, clantech,
- resources, nightwave, twitch,
+ resources, nightwave, twitch
} = require('./resources/trackables.json');
const rssFeeds = require('./resources/rssFeeds');
@@ -379,7 +379,7 @@ const chunkify = ({
if (checkTitle) {
// strip the last title if it starts with a title
if (string.endsWith('**')) {
- const endTitle = string.matches(/\*\*(.*)\*\*\s*$/g)[1];
+ const endTitle = (string.match(/\*\*(.*)\*\*\s*$/g)[1] || '');
string = string.replace(/\*\*(.*)\*\*\s*$/g, ''); // eslint-disable-line no-param-reassign
breakIndex -= endTitle.length;
}
@@ -411,6 +411,7 @@ const markdinate = htmlString => htmlString
.replace(/<\/li>\s*
/gm, '\n') // clean up breaks between list items
.replace(/\n/gm, '- ') // strip list items to bullets, replace later with emoji
.replace(/ipsnoembed="false" /gm, '') // manually replace ipsnoembed, it causes issues given location
+ .replace(/ipsnoembed="true" /gm, '') // manually replace ipsnoembed, it causes issues given location
.replace(/(.*)<\/a>/gm, '[$2]($1)')
.replace(/&/gm, '&') // replace ampersand entity... it looks weird with some titles
.replace(/<\/li>/gm, '') // strip li end tags
diff --git a/src/Logger.js b/src/Logger.js
index 75c9dc099..a9d06083c 100644
--- a/src/Logger.js
+++ b/src/Logger.js
@@ -26,7 +26,7 @@ const levels = {
SILLY: 'grey',
DEBUG: 'brightYellow',
INFO: 'blue',
- WARN: 'orange',
+ WARN: 'brightRed',
ERROR: 'red',
FATAL: 'magenta',
};
@@ -40,6 +40,8 @@ const contexts = {
TWITCH: 'magenta',
WS: 'cyan',
DB: 'blue',
+ TwitchApi: 'magenta',
+ TM: 'yellow',
};
/**
@@ -79,12 +81,13 @@ Object.keys(levels).forEach((level) => {
if (Sentry) {
Sentry.captureException(message);
}
- if (errorHook) {
+ if (errorHook && !l.logLevel === 'DEBUG') {
// filter out api errors, they're largely unhelpful and unrecoverable
if (message.stack && message.stack.startsWith('DiscordAPIError')) return;
errorHook.send(new ErrorEmbed(message));
} else {
console.error(simple);
+ console.error(message);
}
}
};
diff --git a/src/bot.js b/src/bot.js
index 04c3ac6e5..a4c5a9ce8 100644
--- a/src/bot.js
+++ b/src/bot.js
@@ -2,6 +2,8 @@
const { Client, WebhookClient } = require('discord.js');
+// const Feeder = require('rss-feed-emitter');
+
const WorldStateClient = require('./resources/WorldStateClient');
const CommandManager = require('./CommandManager');
const EventHandler = require('./EventHandler');
@@ -11,6 +13,8 @@ const MessageManager = require('./settings/MessageManager');
const Database = require('./settings/Database');
const logger = require('./Logger');
+// const feeds = require('./resources/rssFeeds');
+
const unlog = ['WS_CONNECTION_TIMEOUT'];
/**
@@ -150,6 +154,8 @@ class Genesis {
this.clusterId = process.env.CLUSTER_ID || 0;
// this.tracker = new Tracker(this);
+ // this.feeder; // for debugging
+
if (process.env.CONTROL_WH_ID) {
this.controlHook = new WebhookClient(process.env.CONTROL_WH_ID, process.env.CONTROL_WH_TOKEN);
}
@@ -203,6 +209,16 @@ class Genesis {
try {
await this.client.login(this.token);
this.logger.debug('Logged in with token.');
+
+ /* For Debugging:
+ this.feeder = new Feeder({
+ userAgent: `RSS Feed Emitter | ${this.client.user.username}`,
+ skipFirstLoad: true,
+ });
+ feeds.forEach((feed) => {
+ this.feeder.add({ url: feed.url, refresh: 900000 });
+ });
+ */
} catch (err) {
const type = ((err && err.toString()) || '').replace(/Error \[(.*)\]: .*/ig, '$1');
if (!unlog.includes(type)) {
diff --git a/src/commands/Ondemand/GetRSSUpdates.js b/src/commands/Ondemand/GetRSSUpdates.js
index 8777175d9..feb7042d0 100644
--- a/src/commands/Ondemand/GetRSSUpdates.js
+++ b/src/commands/Ondemand/GetRSSUpdates.js
@@ -16,7 +16,7 @@ class GetRSSUpdates extends Command {
async run(message, ctx) {
const feedUrl = feeds.filter(f => f.key === `forum.updates.${ctx.platform}`)[0].url;
- const matchingFeeds = this.bot.feedNotifier.feeder.list.filter(f => f.url === feedUrl);
+ const matchingFeeds = this.bot.feeder.list.filter(f => f.url === feedUrl);
const updates = matchingFeeds && matchingFeeds[0]
&& matchingFeeds[0].items && matchingFeeds[0].items[0]
? [...matchingFeeds[0].items]
diff --git a/src/embeds/RSSEmbed.js b/src/embeds/RSSEmbed.js
index 4e7f6dc15..afc077d75 100644
--- a/src/embeds/RSSEmbed.js
+++ b/src/embeds/RSSEmbed.js
@@ -3,8 +3,7 @@
const BaseEmbed = require('./BaseEmbed.js');
const { chunkify, markdinate } = require('../CommonFunctions');
-
-const escapeReserved = str => str.replace(/[\(\)\?\+\.]/ig, '\\$1');
+const logger = require('../Logger');
/**
* Generates daily deal embeds
@@ -18,8 +17,8 @@ class RSSEmbed extends BaseEmbed {
constructor(feedItem, feed) {
super();
// clean up description, falling back to an empty string
- let strippedDesc = markdinate(escapeReserved((feedItem.description || '\u200B')
- .replace(/\<\\?string\>/ig, '')));
+ let strippedDesc = markdinate((feedItem.description || '\u200B')
+ .replace(/\<\\?string\>/ig, ''));
const firstLine = strippedDesc.split('\n')[0].replace(/\*\*/g, '');
if (feedItem.title.includes(firstLine)) {
@@ -28,21 +27,26 @@ class RSSEmbed extends BaseEmbed {
strippedDesc = tokens.join('\n');
}
- const chunks = chunkify({
- string: strippedDesc,
- maxLength: 1000,
- breakChar: '\n',
- checkTitle: true,
- });
+ try {
+ const chunks = chunkify({
+ string: strippedDesc,
+ maxLength: 1000,
+ breakChar: '\n',
+ checkTitle: true,
+ });
- if (chunks) {
- [strippedDesc] = chunks;
- // strip the last title if it starts with a title
- if (strippedDesc.endsWith('**')) {
- strippedDesc = strippedDesc.replace(/\*\*(.*)\*\*$/g, '');
- }
+ if (chunks) {
+ [strippedDesc] = chunks;
+ // strip the last title if it starts with a title
+ if (strippedDesc.endsWith('**')) {
+ strippedDesc = strippedDesc.replace(/\*\*(.*)\*\*$/g, '');
+ }
- this.description = strippedDesc;
+ this.description = strippedDesc;
+ }
+ } catch (e) {
+ logger.error(e);
+ logger.debug(strippedDesc, 'WS');
}
this.url = feedItem.link;
diff --git a/src/embeds/TwitchEmbed.js b/src/embeds/TwitchEmbed.js
index 73ffe251a..ed5c2345b 100644
--- a/src/embeds/TwitchEmbed.js
+++ b/src/embeds/TwitchEmbed.js
@@ -7,21 +7,40 @@ const BaseEmbed = require('./BaseEmbed.js');
*/
class TwitchEmbed extends BaseEmbed {
/**
- * @param {HelixStream} streamData - a stream result from twitch api
- * @param {HelixUser} userData - a user result from twitch api
+ * @param {Object} streamData - a stream result from twitch api
*/
- constructor(streamData, userData) {
+ constructor(streamData) {
super();
this.title = streamData.title;
- if (userData) {
+ this.url = `https://www.twitch.tv/${streamData.user_login}`;
+
+ this.image = {
+ url: streamData.thumbnail_url
+ .replace('{width}', '1280')
+ .replace('{height}', '720'),
+ };
+
+ this.color = 6570405;
+ this.footer = {
+ text: 'Live @',
+ icon_url: 'https://i.imgur.com/urcKWLO.png',
+ };
+
+ if (streamData.user) {
this.author = {
- name: streamData.userDisplayName,
- icon_url: userData.profilePictureUrl,
+ name: streamData.user.display_name,
+ icon_url: streamData.user.profile_image_url,
};
- this.image = {
- url: streamData.thumbnailUrl.replace('{width}', '580').replace('{height}', '326'),
+
+ this.description = streamData.user.description;
+ }
+
+ if (streamData.game) {
+ this.thumbnail = {
+ url: streamData.game.box_art_url
+ .replace('{width}', '288')
+ .replace('{height}', '384'),
};
- this.url = `https://www.twitch.tv/${userData.name}`;
}
}
}
diff --git a/src/notifications/Broadcaster.js b/src/notifications/Broadcaster.js
index 1e27dbd89..0ba4890f6 100644
--- a/src/notifications/Broadcaster.js
+++ b/src/notifications/Broadcaster.js
@@ -57,7 +57,7 @@ class Broadcaster {
? this.workerCache.getKey(`${type}:${platform}`)
: await this.settings.getAgnosticNotifications(type, platform, items);
if (!channels.length) {
- logger.debug(`No channels on ${platform} tracking ${type}... continuing`);
+ logger.silly(`No channels on ${platform} tracking ${type}... continuing`, 'WS');
return;
}
diff --git a/src/notifications/TwitchNotifier.js b/src/notifications/TwitchNotifier.js
deleted file mode 100644
index ef79544fa..000000000
--- a/src/notifications/TwitchNotifier.js
+++ /dev/null
@@ -1,116 +0,0 @@
-'use strict';
-
-const { ApiClient } = require('twitch');
-const WebHookListener = require('twitch-webhooks').default;
-
-const TwitchEmbed = require('../embeds/TwitchEmbed');
-const Broadcaster = require('./Broadcaster');
-const { platforms } = require('../CommonFunctions');
-const logger = require('../Logger');
-
-require('colors');
-
-/**
- * Watches for Twitch go-lives and broadcasts them
- */
-class TwitchNotifier {
- constructor({
- client, settings, messageManager, workerCache,
- }) {
- this.broadcaster = new Broadcaster({
- client,
- settings,
- messageManager,
- workerCache,
- });
-
- this.lastStartedAtTime = null;
-
- if (process.env.TWITCH_CLIENT_ID && process.env.TWITCH_CLIENT_SECRET) {
- const id = process.env.TWITCH_CLIENT_ID;
- const secret = process.env.TWITCH_CLIENT_SECRET;
- this.client = ApiClient.withClientCredentials(id, secret);
- } else {
- logger.debug('[Twitch] Cannot initialize Twitch Notifier... invalid credentials');
- }
-
- this.subs = {};
- }
-
- async start() {
- if (!this.client) return;
-
- try {
- this.listener = await WebHookListener.create(this.client, {
- host: process.env.TWITCH_HOST || 'localhost',
- port: 8090,
- reverseProxy: { port: 443, ssl: true },
- });
-
- this.listener.listen();
-
- await this.listenToStreams();
- } catch (e) {
- logger.error(`initialzation error: ${e.message}`);
- return;
- }
- logger.info(`[${'Twitch'.purple}] Ready`);
- }
-
- /**
- * Set up subscription for twitch user id
- * @param {string} sub twitch user id
- * @returns {Promise}
- */
- async subscribe(sub) {
- const user = await this.client.helix.users.getUserByName(sub);
-
- const subscription = this.listener.subscribeToStreamChanges(user.id, async (stream) => {
- if (stream) {
- const twitchEmbed = new TwitchEmbed(stream, user);
- let id = `${sub}.live`;
- // add warframe type filtering for ids...
- if (sub === 'warframe') {
- if (stream.title.includes('Home Time') || stream.title.includes('Prime Time')) {
- id = `${sub}.primetime.live`;
- } else if (stream.title.includes('Devstream')) {
- id = `${sub}.devstream.live`;
- } else {
- id = `${sub}.other.live`;
- }
- }
-
- // broadcast it!
- platforms.forEach((platform) => {
- this.broadcaster.broadcast(twitchEmbed, platform, id);
- });
- }
- });
- this.subs[sub] = subscription;
- logger.debug(`[Twitch] listening for '${sub}' stream changes...`);
- }
-
- /**
- * Set up listening to streams
- */
- async listenToStreams() {
- if (!(this.client && this.listener)) return;
-
- const subs = require('../resources/twitch.json');
-
- const subPromises = [];
-
- subs.forEach((sub) => {
- subPromises.push(this.subscribe(sub));
- });
-
- await Promise.all(subPromises);
-
- process.on('exit', () => {
- Object.entries(subs).forEach(subscription => subscription.stop());
- logger.debug('[Twitch] Subscriptions unregistered...');
- });
- }
-}
-
-module.exports = TwitchNotifier;
diff --git a/src/notifications/Worker.js b/src/notifications/Worker.js
index 7858ffa4f..659eef51f 100644
--- a/src/notifications/Worker.js
+++ b/src/notifications/Worker.js
@@ -6,14 +6,14 @@ require('colors');
const Notifier = require('./Notifier');
const FeedsNotifier = require('./FeedsNotifier');
-const TwitchNotifier = require('./TwitchNotifier');
+const TwitchNotifier = require('./twitch/TwitchNotifier');
const WorldStateCache = require('../WorldStateCache');
const MessageManager = require('../settings/MessageManager');
const Rest = require('../tools/RESTWrapper');
const Database = require('../settings/Database');
-const { logger, platforms } = require('./NotifierUtils');
+const { logger } = require('./NotifierUtils');
const { games } = require('../CommonFunctions');
const cachedEvents = require('../resources/cachedEvents');
@@ -109,6 +109,7 @@ class Worker {
deps.client = rest;
deps.worldStates = this.worldStates;
deps.timeout = timeout;
+ deps.activePlatforms = activePlatforms;
await rest.init();
await deps.settings.init();
@@ -133,10 +134,10 @@ class Worker {
await this.notifier.start();
- if (logger.isLoggable('debug')) {
+ if (logger.isLoggable('DEBUG')) {
rest.controlMessage({
embeds: [{
- description: `Worker ready on ${platforms}`,
+ description: `Worker ready on ${activePlatforms}`,
color: 0x2B90EC,
}],
});
diff --git a/src/notifications/twitch/TwitchClient.js b/src/notifications/twitch/TwitchClient.js
new file mode 100644
index 000000000..4217ec86a
--- /dev/null
+++ b/src/notifications/twitch/TwitchClient.js
@@ -0,0 +1,194 @@
+'use strict';
+
+require('colors');
+const fetch = require('node-fetch');
+const Cache = require('flat-cache');
+const Job = require('cron').CronJob;
+
+const logger = require('../../Logger');
+
+const urls = {
+ helix: 'https://api.twitch.tv/helix',
+ token: 'https://id.twitch.tv/oauth2/token',
+};
+
+const forceHydrate = (process.argv[2] || '').includes('--hydrate');
+
+/**
+ * Twitch Helix API helper ("New Twitch API").
+ *
+ * All credit to https://github.com/roydejong/timbot for composition and structure
+ */
+class TwitchClient {
+ static #tokenCache = Cache.load('accessToken',
+ require('path').resolve('.cache'));
+
+ static get accessToken() {
+ return this.#tokenCache.getKey('token');
+ }
+
+ /**
+ * Sets the access token in the cache
+ * @param {string} token twitch access token to cache
+ */
+ static set #accessToken (token) {
+ this.#tokenCache.setKey('token', token);
+ this.#tokenCache.save(true);
+ }
+
+ static get refreshToken() {
+ return this.#tokenCache.getKey('refresh');
+ }
+
+ /**
+ * Sets the refresh token in the cache
+ * @param {string} token twitch refresh token to cache
+ */
+ static set #refreshToken (token) {
+ this.#tokenCache.setKey('refresh', token);
+ this.#tokenCache.save(true);
+ }
+
+ static get requestOptions() {
+ return {
+ baseURL: urls.helix,
+ headers: {
+ 'Client-ID': process.env.TWITCH_CLIENT_ID,
+ Authorization: `Bearer ${this.accessToken}`,
+ },
+ };
+ }
+
+ /**
+ * Handle twitch API errors
+ * @param {Error} err error reply
+ */
+ static handleApiError(err) {
+ const res = err.response || { };
+
+ if (res.data && res.data.message) {
+ logger.error(`API request failed with Helix error:\n${res.data.message}\n(${res.data.error}/${res.data.status})`, 'TwitchApi');
+ } else {
+ logger.error(`API request failed with error: ${err.message || err}`, 'TwitchApi');
+ }
+ }
+
+ /**
+ * Get Data from the Twitch API
+ * @param {string} path path to request
+ * @param {string} params query params
+ * @private
+ * @static
+ * @returns {Promise>}
+ */
+ static async #apiGet (path, params) {
+ if (!this.accessToken) await this.hydrateToken();
+ if (!this.accessToken && !this.refreshToken) return [];
+
+ const url = `${this.requestOptions.baseURL}/${path}?${params}`;
+ try {
+ const res = await fetch(url, {
+ headers: {
+ 'Client-ID': process.env.TWITCH_CLIENT_ID,
+ Authorization: `Bearer ${this.accessToken}`,
+ },
+ });
+
+ if (res.ok) {
+ const data = await res.json();
+ return data.data || [];
+ }
+ logger.error(res.statusText);
+ return [];
+ } catch (e) {
+ this.handleApiError(e);
+ return [];
+ }
+ }
+
+ /**
+ * Hydrate cache with a new token.
+ * @type {Array}
+ */
+ static async hydrateToken() {
+ if (!this.refreshToken || forceHydrate) {
+ if (this.refreshToken && this.refreshToken.includes('Invalid refresh token')) {
+ this.#tokenCache.removeKey('refresh');
+ }
+
+ const atParams = [
+ { key: 'client_id', val: process.env.TWITCH_CLIENT_ID },
+ { key: 'client_secret', val: process.env.TWITCH_CLIENT_SECRET },
+ { key: 'grant_type', val: 'client_credentials' },
+ ];
+
+ const res = await fetch(`${urls.token}?${atParams
+ .map(({ key, val }) => `${key}=${val}`).join('&')}`, { method: 'POST' });
+
+ if (res.ok) {
+ const initAccessToken = (await res.json()).access_token;
+ this.#refreshToken = initAccessToken;
+ this.#accessToken = initAccessToken;
+ return;
+ }
+ logger.error(`error refreshing refresh token: ${res.statusText}`, 'TWITCH');
+ }
+
+ const params = [
+ { key: 'client_id', val: process.env.TWITCH_CLIENT_ID },
+ { key: 'client_secret', val: process.env.TWITCH_CLIENT_SECRET },
+ { key: 'grant_type', val: 'refresh_token' },
+ { key: 'refresh_token', val: this.refreshToken },
+ ];
+ const url = `${urls.token}?${params.map(({ key, val }) => `${key}=${val}`).join('&')}`;
+
+ try {
+ const res = await fetch(url, { method: 'post' });
+
+ if (res.ok) {
+ const token = (await res.json()).access_token;
+ logger.info(token, 'TWITCH');
+ this.#accessToken = token;
+ } else {
+ logger.error(`error retrieving refreshing token: ${res.statusText}`, 'TWITCH');
+ logger.debug(url);
+ }
+ } catch (e) {
+ logger.error(e, 'TWITCH');
+ }
+ }
+
+ static #refreshJob = new Job('0 0 */3 * * *', this.hydrateToken.bind(this), null, true);
+
+ /**
+ * Fetch stream data
+ * @param {Array} channels list of channels to fetch
+ * @returns {Promise>} array of stream data
+ */
+ static async fetchStreams(channels) {
+ const params = channels.map(c => `user_login=${c}`).join('&');
+ return TwitchClient.#apiGet('streams', params);
+ }
+
+ /**
+ * Fetch Twitch Users
+ * @param {Array} channelNames list of channels to fetch
+ * @returns {Promise>} array of stream data
+ */
+ static async fetchUsers(channelNames) {
+ const params = channelNames.map(c => `login=${c}`).join('&');
+ return TwitchClient.#apiGet('users', params);
+ }
+
+ /**
+ * Get Game data per gameId
+ * @param {Array} gameIds Twitch game identifiers
+ * @returns {Promise>} array of game data
+ */
+ static async fetchGames(gameIds) {
+ const params = gameIds.map(c => `id=${c}`).join('&');
+ return TwitchClient.#apiGet('games', params);
+ }
+}
+
+module.exports = TwitchClient;
diff --git a/src/notifications/twitch/TwitchMonitor.js b/src/notifications/twitch/TwitchMonitor.js
new file mode 100644
index 000000000..70a2caa81
--- /dev/null
+++ b/src/notifications/twitch/TwitchMonitor.js
@@ -0,0 +1,289 @@
+'use strict';
+
+const moment = require('moment');
+const Cache = require('flat-cache');
+require('colors');
+const EventEmitter = require('events');
+
+const TwitchApi = require('./TwitchClient');
+const logger = require('../../Logger');
+const channels = require('../../resources/twitch.json');
+
+const haveEqualValues = (a, b) => {
+ if (a.length !== b.length) {
+ return false;
+ }
+
+ a.sort();
+ b.sort();
+
+ for (let i = 0; i < a.length; i += 1) {
+ if (a[i] !== b[i]) {
+ return false;
+ }
+ }
+
+ return true;
+};
+
+const refreshRate = Number.parseInt(process.env.TWITCH_REFRESH || 60000, 10);
+
+const forceHydrate = (process.argv[2] || '').includes('--hydrate');
+
+class TwitchMonitor extends EventEmitter {
+ #userDb;
+
+ #gameDb;
+
+ #statesDb;
+
+ #pendingUserRefresh = true;
+
+ #pendingGameRefresh;
+
+ #watchingGameIds;
+
+ #lastUserRefresh;
+
+ #lastGameRefresh;
+
+ #userData;
+
+ #gameData;
+
+ #activeStreams;
+
+ #streamData;
+
+ #channelLiveCallbacks;
+
+ #channelOfflineCallbacks;
+
+ static #MIN_POLL_INTERVAL_MS = 30000;
+
+ constructor() {
+ super();
+ this.#userDb = Cache.load('users');
+ this.#gameDb = Cache.load('games');
+ this.#statesDb = Cache.load('states') || {};
+ this.#pendingUserRefresh = false;
+ this.#pendingGameRefresh = false;
+ this.#watchingGameIds = [];
+ this.#lastUserRefresh = this.#userDb.getKey('last-update');
+ this.#lastGameRefresh = moment();
+ this.#userData = this.#userDb.getKey('user-list') || {};
+ this.#gameData = this.#gameDb.getKey('game-list') || {};
+ this.#activeStreams = [];
+ this.#streamData = this.#statesDb.getKey('all') || {};
+ this.#channelLiveCallbacks = [];
+ this.#channelOfflineCallbacks = [];
+ }
+
+ async start() {
+ if (!channels.length) {
+ logger.warn('No channels configured', 'TM');
+ return;
+ }
+
+ if (forceHydrate) {
+ await TwitchApi.hydrateToken();
+ }
+
+ // Configure polling interval
+ let checkIntervalMs = refreshRate;
+ if (Number.isNaN(checkIntervalMs) || checkIntervalMs < TwitchMonitor.MIN_POLL_INTERVAL_MS) {
+ // Enforce minimum poll interval to help avoid rate limits
+ checkIntervalMs = TwitchMonitor.MIN_POLL_INTERVAL_MS;
+ }
+ setInterval(() => {
+ this.refresh('Periodic refresh');
+ }, checkIntervalMs + 1000);
+
+ // Immediate refresh after startup
+ setTimeout(() => {
+ this.refresh('Initial refresh after start-up');
+ }, 1000);
+
+ // Ready!
+ logger.debug(`(${checkIntervalMs}ms interval) Configured stream status polling for channels: ${channels.join(', ')}`, 'TM');
+ }
+
+ async refresh(reason) {
+ const now = moment();
+ logger.silly(`Refreshing now (${reason || 'No reason'})`, 'TM');
+
+ // Refresh all users periodically
+ if (this.#lastUserRefresh === null || now.diff(moment(this.#lastUserRefresh), 'minutes') >= 10) {
+ this.#pendingUserRefresh = true;
+ try {
+ this.#handleUserList(await TwitchApi.fetchUsers(channels));
+ } catch (err) {
+ logger.warn(`Error in users refresh: ${err.message || err}`, 'TM');
+ } finally {
+ if (this.#pendingUserRefresh) {
+ this.#pendingUserRefresh = false;
+ this.refresh('Got Twitch users, need to get streams');
+ }
+ }
+ }
+
+ // Refresh all games if needed
+ if (this.#pendingGameRefresh) {
+ try {
+ if (this.#watchingGameIds.length) {
+ this.#handleGameList(await TwitchApi.fetchGames(this.#watchingGameIds));
+ }
+ } catch (err) {
+ logger.warn(`Error in games refresh ${err.message || err}`, 'TM');
+ } finally {
+ if (this.#pendingGameRefresh) {
+ this.#pendingGameRefresh = false;
+ }
+ }
+ }
+
+ // Refresh all streams
+ try {
+ await this.#handleStreamList(await TwitchApi.fetchStreams(channels));
+ } catch (err) {
+ logger.warn(`Error in streams refresh: ${err.message || err}`, 'TM');
+ logger.error(err, 'TM');
+ }
+ }
+
+ #handleUserList (users) {
+ const namesSeen = [];
+
+ users.forEach((user) => {
+ const prevUserData = this.#userData[user.id] || { };
+ this.#userData[user.id] = { ...prevUserData, ...user };
+
+ namesSeen.push(user.display_name);
+ });
+
+ if (namesSeen.length) {
+ logger.silly(`Updated user info: ${namesSeen.join(', ')}`, 'TM');
+ }
+
+ this.#lastUserRefresh = moment();
+
+ this.#userDb.setKey('last-update', this.#lastUserRefresh);
+ this.#userDb.setKey('user-list', this.#userData);
+ this.#userDb.save(true);
+ }
+
+ #handleGameList (games) {
+ const gotGameNames = [];
+
+ games.forEach((game) => {
+ const gameId = game.id;
+
+ const prevGameData = this.#gameData[gameId] || { };
+ this.#gameData[gameId] = { ...prevGameData, ...game };
+
+ gotGameNames.push(`${game.id} → ${game.name}`);
+ });
+
+ if (gotGameNames.length) {
+ logger.silly(`Updated game info: ${gotGameNames.join(', ')}`, 'TM');
+ }
+
+ this.#lastGameRefresh = moment();
+
+ this.#gameDb.setKey('last-update', this.#lastGameRefresh);
+ this.#gameDb.setKey('game-list', this.#gameData);
+ this.#gameDb.save(true);
+ }
+
+ async #handleStreamList (streams) {
+ // Index channel data & build list of stream IDs now online
+ const nextOnlineList = [];
+ const nextGameIdList = [];
+
+ for (const stream of streams) {
+ const channelName = stream.user_name.toLowerCase();
+
+ if (stream.type === 'live') {
+ nextOnlineList.push(channelName);
+ }
+
+ // logger.debug(`${channelName} last seen as ${this.#statesDb.getKey(channelName)}`);
+
+ if (typeof this.#statesDb.getKey(channelName) === 'undefined') {
+ this.#statesDb.setKey(channelName, stream.type);
+ this.#statesDb.save(true);
+ }
+
+ const userDataBase = this.#userData[stream.user_id] || { };
+ const prevStreamData = this.#streamData[channelName] || { };
+
+ this.#streamData[channelName] = { ...userDataBase, ...prevStreamData, ...stream };
+ this.#streamData[channelName].game = (stream.game_id
+ && this.#gameData[stream.game_id]) || null;
+ this.#streamData[channelName].user = userDataBase;
+
+ if (this.#statesDb.getKey(channelName) !== 'live' && stream.type === 'live') {
+ // logger.debug(`${channelName} has gone online`);
+ this.#handleChannelLiveUpdate(this.#streamData[channelName], true);
+ }
+
+ if (stream.game_id) {
+ nextGameIdList.push(stream.game_id);
+ }
+
+ this.#statesDb.setKey(channelName, stream.type);
+ this.#statesDb.save(true);
+ }
+ this.#statesDb.setKey('all', this.#streamData);
+
+ for (const channel of channels) {
+ if (!streams.find(s => s.user_login === channel)
+ && this.#statesDb.getKey(channel) === 'live') {
+ // logger.debug(`${channel} has gone offline`);
+ this.#handleChannelLiveUpdate(
+ this.#streamData[channel] || { type: 'offline', user_login: channel }, false,
+ );
+ this.#statesDb.setKey(channel, 'offline');
+ }
+ }
+ this.#statesDb.save(true); // save after all are updated
+
+ if (!haveEqualValues(this.#watchingGameIds, nextGameIdList)) {
+ // We need to refresh game info
+ this.#watchingGameIds = nextGameIdList;
+ this.#pendingGameRefresh = true;
+ this.refresh('Need to request game data');
+ }
+ }
+
+ #handleChannelLiveUpdate (streamData, isOnline) {
+ let success = true;
+
+ try {
+ if (isOnline) {
+ this.emit('live', streamData);
+ } else {
+ this.emit('offline', streamData);
+ }
+ } catch (err) {
+ logger.warn('Event emit failed.', 'TM');
+ success = false;
+ }
+ return success;
+ }
+
+ async #spotLoadGame (gameId) {
+ this.#handleGameList(await TwitchApi.fetchGames([gameId]));
+ return this.#gameData[gameId];
+ }
+
+ async #spotLoadStream (channel) {
+ return this.#handleStreamList(await TwitchApi.fetchStreams([channel]));
+ }
+
+ async #spotLoadUser (user) {
+ return this.#handleUserList(await TwitchApi.fetchUsers([user]));
+ }
+}
+
+module.exports = TwitchMonitor;
diff --git a/src/notifications/twitch/TwitchNotifier.js b/src/notifications/twitch/TwitchNotifier.js
new file mode 100644
index 000000000..d6cfe96d3
--- /dev/null
+++ b/src/notifications/twitch/TwitchNotifier.js
@@ -0,0 +1,70 @@
+'use strict';
+
+const TwitchEmbed = require('../../embeds/TwitchEmbed');
+const TwitchMonitor = require('./TwitchMonitor');
+
+const Broadcaster = require('../Broadcaster');
+const logger = require('../../Logger');
+
+require('colors');
+
+/**
+ * Watches for Twitch go-lives and broadcasts them
+ */
+class TwitchNotifier {
+ #monitor;
+
+ #broadcaster;
+
+ #activePlatforms;
+
+ constructor({
+ client, settings, messageManager, workerCache, activePlatforms,
+ }) {
+ this.#broadcaster = new Broadcaster({
+ client,
+ settings,
+ messageManager,
+ workerCache,
+ });
+ this.#monitor = new TwitchMonitor();
+ this.#activePlatforms = activePlatforms;
+
+ this.enabled = true;
+ }
+
+ start() {
+ try {
+ this.#monitor.start();
+ } catch (e) {
+ logger.error(`initialzation error: ${e.message}`, 'TWITCH');
+ return;
+ }
+ logger.info('Ready', 'Twitch');
+
+ this.#monitor.on('live', (streamData) => {
+ if (this.enabled) {
+ const embed = new TwitchEmbed(streamData);
+ let id = `${streamData.user_login}.live`;
+ // add warframe type filtering for ids...
+ if (streamData.user_login === 'warframe') {
+ if (streamData.title.includes('Devstream')) {
+ id = `${streamData.user_login}.devstream.live`;
+ } else if (streamData.title.includes('Home Time')
+ || streamData.title.includes('Prime Time')
+ || streamData.title.includes('Working From Home')
+ || streamData.title.includes('Community Stream')) {
+ id = `${streamData.user_login}.primetime.live`;
+ } else {
+ id = `${streamData.user_login}.other.live`;
+ }
+ }
+ for (const platform of this.#activePlatforms) {
+ this.#broadcaster.broadcast(embed, platform, id);
+ }
+ }
+ });
+ }
+}
+
+module.exports = TwitchNotifier;
diff --git a/src/resources/trackables.json b/src/resources/trackables.json
index 27d9fb7bb..ef5d34ecb 100644
--- a/src/resources/trackables.json
+++ b/src/resources/trackables.json
@@ -178,6 +178,10 @@
"twitch": [
"warframe.devstream.live",
"warframe.primetime.live",
- "warframe.other.live"
+ "warframe.other.live",
+ "sherpa.live",
+ "joeyzerotv.live",
+ "de_rebecca.live",
+ "de_steve.live"
]
}
diff --git a/src/resources/twitch.json b/src/resources/twitch.json
index 984135e45..6e7405382 100644
--- a/src/resources/twitch.json
+++ b/src/resources/twitch.json
@@ -1,3 +1,3 @@
[
- "warframe"
+ "warframe", "sherpa", "joeyzerotv", "de_rebecca", "de_steve"
]
\ No newline at end of file