diff --git a/package-lock.json b/package-lock.json index edc8f93..beed299 100644 --- a/package-lock.json +++ b/package-lock.json @@ -892,6 +892,14 @@ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true }, + "eventsource": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.0.7.tgz", + "integrity": "sha512-4Ln17+vVT0k8aWq+t/bF5arcS3EpT9gYtW66EPacdj/mAFevznsnyoHLPy2BA8gbIQeIHoPsvwmfBftfcG//BQ==", + "requires": { + "original": "^1.0.0" + } + }, "external-editor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", @@ -1803,6 +1811,14 @@ "word-wrap": "~1.2.3" } }, + "original": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/original/-/original-1.0.2.tgz", + "integrity": "sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg==", + "requires": { + "url-parse": "^1.4.3" + } + }, "os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", @@ -1942,6 +1958,11 @@ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" }, + "querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" + }, "read-pkg": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", @@ -1990,6 +2011,11 @@ "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", "dev": true }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=" + }, "resolve": { "version": "1.15.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.15.1.tgz", @@ -2392,6 +2418,15 @@ "punycode": "^2.1.0" } }, + "url-parse": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.7.tgz", + "integrity": "sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg==", + "requires": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "v8-compile-cache": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz", diff --git a/package.json b/package.json index b7ea3aa..a72bec1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mwn", - "version": "0.6.0", + "version": "0.7.0", "description": "MediaWiki bot framework for NodeJS", "main": "./src/bot.js", "scripts": { @@ -26,6 +26,7 @@ "dependencies": { "axios": "^0.19.2", "axios-cookiejar-support": "^1.0.0", + "eventsource": "^1.0.7", "form-data": "^3.0.0", "oauth-1.0a": "^2.2.6", "semlog": "^0.6.10", @@ -41,4 +42,4 @@ "eslint-plugin-standard": "^4.0.1", "mocha": "^7.1.1" } -} \ No newline at end of file +} diff --git a/src/bot.js b/src/bot.js index a22a122..0413f1d 100644 --- a/src/bot.js +++ b/src/bot.js @@ -54,6 +54,7 @@ const Wikitext = require('./wikitext'); const User = require('./user'); const Category = require('./category'); const File = require('./file'); +const Stream = require('./eventstream'); const static_utils = require('./static_utils'); class mwn { @@ -229,6 +230,11 @@ class mwn { */ this.file = File(this); + /** + * Sub-class for the EventStreams API + */ + this.stream = Stream(mwn, this); + // set up any semlog options semlog.updateConfig(this.options.semlog || {}); } diff --git a/src/eventstream.js b/src/eventstream.js new file mode 100644 index 0000000..8ea0d90 --- /dev/null +++ b/src/eventstream.js @@ -0,0 +1,80 @@ +const EventSource = require('eventsource'); + +module.exports = function (mwn, bot) { + + class EventStream extends EventSource { + + /** + * Access the Wikimedia EventStreams API + * @see https://wikitech.wikimedia.org/wiki/Event_Platform/EventStreams + * @param {string[]} streams + * @param {Object} [config={}] + * @config {string|Date} since + * @config {Function} onopen + * @config {Function} onerror + */ + constructor(streams, config = {}) { + if (Array.isArray(streams)) { + streams = streams.join(','); + } + + let since = config.since ? `?since=${new bot.date(config.since).format('YYYYMMDDHHmmss')}` : ''; + super(`https://stream.wikimedia.org/v2/stream/${streams}${since}`, { + headers: { + 'User-Agent': bot.userAgent + } + }); + + this.onopen = config.onopen || function () { + mwn.log(`[S] Opened eventsource connection for ${streams} stream(s)`); + }; + this.onerror = config.onerror || function (err) { + mwn.log(`[W] event source encountered error: ${err}`); + }; + + } + + /** + * Register a function to trigger for every message data from the source. + * @param {Function} action + * @param {Function | Object} [filter={}] + */ + addListener(action, filter = {}) { + let filterer = typeof filter === 'function' ? + filter : + function(data) { + for (let key of Object.keys(filter)) { + if (data[key] !== filter[key]) { + return false; + } + } + return true; + }; + + this.onmessage = function(event) { + let data = JSON.parse(event.data); + if (!filterer(data)) { + return; + } + action(data); + }; + } + + /** + * Access the recentchange EventStreams API + * @see https://wikitech.wikimedia.org/wiki/Event_Platform/EventStreams + * @param {{wiki: string, type: ("edit"|"log"|"new"|"categorize"), title: string, namespace: number, + * user: string, bot: boolean, minor: boolean} | Function} filter + * @param {Function} action + */ + static recentchange(filter, action) { + let stream = new EventStream('recentchange'); + stream.addListener(filter, action); + return stream; + } + + } + + return EventStream; + +};