commit a07755e32287ec6fa89dfeb0c25ca6933481a388
Author: Agastya Chandrakant <acagastya@outlook.com>
Date: Wed, 30 Sep 2020 16:10:11 +0530
initial commit
Diffstat:
A | .gitignore | | | 1 | + |
A | LICENSE | | | 29 | +++++++++++++++++++++++++++++ |
A | config.js | | | 12 | ++++++++++++ |
A | index.js | | | 144 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | package.json | | | 23 | +++++++++++++++++++++++ |
A | utils.js | | | 139 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | yarn.lock | | | 86 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
7 files changed, 434 insertions(+), 0 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1 @@
+node_modules/
diff --git a/LICENSE b/LICENSE
@@ -0,0 +1,29 @@
+BSD 3-Clause License
+
+Copyright (c) 2020, Agastya
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/config.js b/config.js
@@ -0,0 +1,12 @@
+module.exports = {
+ channels: ["#wikinews", "#wikinews-en", "#wikinewsie-group"],
+ ircBotName: "enwnbot",
+ ircServer: "irc.freenode.net",
+ RCAPI: "https://stream.wikimedia.org/v2/stream/recentchange",
+ RQAPI:
+ "https://en.wikinews.org/w/api.php?action=query&list=categorymembers&cmtitle=Category:Review&format=json&cmsort=timestamp&cmprop=timestamp|ids|title",
+ URAPI:
+ "https://en.wikinews.org/w/api.php?action=query&list=categorymembers&cmtitle=Category:Under%20review&format=json&cmsort=timestamp&cmprop=title|timestamp",
+ URL: "https://en.wikinews.org/w/index.php?title=",
+ wiki: "enwikinews"
+};
diff --git a/index.js b/index.js
@@ -0,0 +1,144 @@
+const ES = require("eventsource");
+
+const irc = require("irc");
+
+const moment = require("moment-timezone");
+
+const {
+ channels,
+ ircBotName,
+ ircServer,
+ RCAPI,
+ RQAPI,
+ URAPI
+} = require("./config");
+
+const {
+ fetchData,
+ fullUrl,
+ getFullLink,
+ getFullTemplate,
+ linkRegex,
+ streamError,
+ streamMessage,
+ templateRegex,
+ thanksRegex
+} = require("./utils");
+
+const ircClient = new irc.Client(ircServer, ircBotName, { channels });
+
+console.log("Connecting to the event stream...");
+
+const eventSource = new ES(RCAPI);
+
+eventSource.onopen = function(event) {
+ console.log("--- Opened connection.");
+};
+
+eventSource.onerror = streamError;
+
+eventSource.onmessage = function(event) {
+ let msg = streamMessage(event);
+ if (msg) channels.forEach(channel => ircClient.say(channel, msg));
+};
+
+ircClient.addListener("error", function(message) {
+ console.log("error: ", message);
+});
+
+ircClient.addListener("pm", function(sender, msg) {
+ if(msg == "KILL" && ['pizero', 'pizero|afk', 'acagastya'].indexOf(sender) >= 0) process.abort();
+ ircClient.say(sender, "I am a bot.");
+});
+
+ircClient.addListener("message", groupChat);
+
+function groupChat(sender, channel, msg) {
+ if (thanksRegex.test(msg))
+ ircClient.say(channel, `You are welcome, ${sender}.`);
+ if (msg.includes(`${ircBotName} !RQ`)) announceRQ(sender, channel);
+ if (msg.includes(`${ircBotName} !UR`)) announceUR(sender, channel);
+
+ const links = msg.match(linkRegex);
+ const templates = msg.match(templateRegex);
+
+ if (!msg.endsWith("--ignore") && links) {
+ const nonEmptyLinks = links.filter(el => el.length > 4);
+ const fullLinks = nonEmptyLinks.map(getFullLink);
+ if (fullLinks.length) sayUrls(false, fullLinks, channel);
+ }
+
+ if (!msg.endsWith("--ignore") && templates) {
+ const nonEmptyTemplates = templates.filter(el => el.length > 4);
+ const fullLinks = nonEmptyTemplates.map(getFullTemplate);
+ if (fullLinks.length) sayUrls(false, fullLinks, channel);
+ }
+}
+
+async function announceRQ(sender, channel) {
+ const data = await fetchData(RQAPI);
+
+ if (data.error)
+ ircClient.say(
+ channel,
+ `Error occurred, ${sender}. Try this instead: "[[CAT:REV]]"`
+ );
+ else {
+ const { list } = data;
+ if (!list.length)
+ ircClient.say(channel, `Review queue is empty, ${sender}.`);
+ else {
+ ircClient.say(
+ channel,
+ `${list.length} articles to review, ${sender}. They are:`
+ );
+ const titles = list.map(({ title }) => title);
+ const times = list.map(({ timestamp }) => moment().to(moment(timestamp)));
+ const urls = titles.map(fullUrl);
+ sayUrls(true, urls, channel, titles, times);
+ }
+ }
+}
+
+async function announceUR(sender, channel) {
+ const data = await fetchData(URAPI);
+
+ if (data.error)
+ ircClient.say(
+ channel,
+ `Error occurred, ${sender}. Try this instead: "[[CAT:Under Review]]"`
+ );
+ else {
+ const { list } = data;
+ if (!list.length)
+ ircClient.say(channel, `No articles are under review, ${sender}.`);
+ else {
+ client.say(
+ channel,
+ `${list.length} articles are under review, ${sender}. They are:`
+ );
+ const titles = list.map(({ title }) => title);
+ const times = list.map(({ timestamp }) => moment().to(moment(timestamp)));
+ const urls = titles.map(fullUrl);
+ sayUrls(true, urls, channel, titles, times);
+ }
+ }
+}
+
+function sayUrls(
+ review = false,
+ urlList,
+ channel,
+ titles = [],
+ times = [],
+ pending = []
+) {
+ urlList.forEach((url, idx) => {
+ let msg = url;
+ if (review) msg += " submitted for review";
+ if (times.length) msg += ` *${times[idx]}*`;
+ if (titles.length) msg += ` -- ${titles[idx]}`;
+ if (pending[idx]) msg += " *under review*";
+ ircClient.say(channel, msg);
+ });
+}
diff --git a/package.json b/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "enwnbot",
+ "version": "0.0.1",
+ "description": "Does what gpy does",
+ "main": "index.js",
+ "repository": "https://github.com/acagasyta/enwnbot2",
+ "author": {
+ "name": "Agastya",
+ "email": "me@hanabi.in",
+ "url": "https://hanabi.in"
+ },
+ "license": "BSD-3-Clause",
+ "private": true,
+ "dependencies": {
+ "eventsource": "^1.0.7",
+ "irc": "^0.5.2",
+ "moment-timezone": "^0.5.31",
+ "node-fetch": "^2.6.1"
+ },
+ "scripts": {
+ "start": "node index.js"
+ }
+}
diff --git a/utils.js b/utils.js
@@ -0,0 +1,139 @@
+const fetch = require("node-fetch");
+
+const { ircBotName, URL, wiki } = require("./config");
+
+async function fetchData(URI) {
+ const res = {};
+ try {
+ const data = await fetch(URI);
+ const parsed = await data.json();
+ res.list = parsed.query.categorymembers;
+ } catch (error) {
+ res.error = true;
+ console.warn("Error in fetchData:", error);
+ }
+ return res;
+}
+
+function fullUrl(title = "") {
+ const fixedTitle = title.replace(/\?/g, "%3F"); // fix the title for '?'
+ let [main, anchor] = fixedTitle.split("#");
+ main = main.replace(/ /g, "%20");
+ if (anchor) anchor = anchor.replace(/ /g, "_");
+ else main = main.replace(/%20/g, "_");
+ let final = main;
+ if (anchor) final += `%23${anchor}`;
+ return `${URL}${final}`;
+}
+
+function getCategory(title = "") {
+ return title.replace(/^Category:/, "");
+}
+
+function getFullLink(link) {
+ const len = link.length;
+ const trimmed = link.substr(2, len - 4);
+ const finalUrl = fullUrl(trimmed);
+ return finalUrl;
+}
+
+function getFullTemplate(template) {
+ const len = template.length;
+ const word = template
+ .substr(2, len - 4)
+ .split("|")[0]
+ .replace(/ /g, "%20")
+ .replace(/\?/g, "%3F");
+ return `${URL}Template:${word}`;
+}
+
+const linkRegex = /\[{2}(.*?)\]{2}/g;
+
+function streamError(event) {
+ const knownError = { type: "error" };
+ const knownErrStr = JSON.stringify(knownError);
+ const eventStr = JSON.stringify(event);
+ if (eventStr != knownErrStr) {
+ ircClient.say("acagastya", eventStr + "\n --- error");
+ console.error("--- Encountered error", event);
+ }
+}
+
+function streamMessage(event) {
+ let msg = "";
+ const change = JSON.parse(event.data);
+ const { comment, title, type, user } = change;
+ if (change.wiki == wiki && type == "categirize") {
+ const category = getCategory(title);
+ const pageRegex = /\[\[:(.*)\]\]/;
+ const page = comment.match(pageRegex)[1];
+ switch (category) {
+ case "Editing": {
+ if (comment.includes("added"))
+ msg = `Attention! ${user} is now {{editing}} [[${page}]].`;
+ else msg = `[[${page}]] is no longer under {{editing}}.`;
+ break;
+ }
+ case "Developing": {
+ if (comment.includes("added"))
+ msg = `${user} may be working on [[${page}]].`;
+ else msg = `${user} has removed [[${page}]] from {{develop}}.`;
+ break;
+ }
+ case "Review": {
+ if (comment.includes("added"))
+ msg = `${user} has submitted [[${page}]] for review.`;
+ else msg = `${user} has removed [[${page}]] from review.`;
+ break;
+ }
+ case "Under review": {
+ if (comment.includes("added"))
+ msg = `${user} is reviewing [[${page}]].`;
+ else msg = `${user} is no longer reviewing [[${page}]].`;
+ break;
+ }
+ case "Peer reviewed/Not ready": {
+ const failedArticleRegex = /^\[\[:Talk:(.*)\]\]/;
+ const failedArticle = comment.match(failedArticleRegex)[1];
+ msg = `${user} failed the ${failedArticle} article.`;
+ break;
+ }
+ case "Abandoned": {
+ if (comment.includes("added"))
+ msg = `${user} has marked [[${page}]] as abandoned.`;
+ break;
+ }
+ case "Published": {
+ if (comment.includes("added"))
+ msg = `${user} just published [[${page}]].`;
+ break;
+ }
+ case "Archived": {
+ if (comment.includes("added"))
+ msg = `${user} has archived [[${page}]].`;
+ else msg = `${user} has unarchived [[${page}]].`;
+ break;
+ }
+ }
+ return msg;
+ }
+}
+
+const templateRegex = /\{{2}(.*?)\}{2}/g;
+
+const thanksRegex = new RegExp(
+ `(thanks?|thank you|thankyou),? ${ircBotName}`,
+ "i"
+);
+
+module.exports = {
+ fetchData,
+ fullUrl,
+ getFullLink,
+ getFullTemplate,
+ linkRegex,
+ streamError,
+ streamMessage,
+ templateRegex,
+ thanksRegex
+};
diff --git a/yarn.lock b/yarn.lock
@@ -0,0 +1,86 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+eventsource@^1.0.7:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-1.0.7.tgz#8fbc72c93fcd34088090bc0a4e64f4b5cee6d8d0"
+ integrity sha512-4Ln17+vVT0k8aWq+t/bF5arcS3EpT9gYtW66EPacdj/mAFevznsnyoHLPy2BA8gbIQeIHoPsvwmfBftfcG//BQ==
+ dependencies:
+ original "^1.0.0"
+
+iconv@~2.2.1:
+ version "2.2.3"
+ resolved "https://registry.yarnpkg.com/iconv/-/iconv-2.2.3.tgz#e084d60eeb7d73da7f0a9c096e4c8abe090bfaed"
+ integrity sha1-4ITWDut9c9p/CpwJbkyKvgkL+u0=
+ dependencies:
+ nan "^2.3.5"
+
+irc-colors@^1.1.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/irc-colors/-/irc-colors-1.5.0.tgz#08834c01ead88b0fd88386a5f2af8f2b0bb963fb"
+ integrity sha512-HtszKchBQTcqw1DC09uD7i7vvMayHGM1OCo6AHt5pkgZEyo99ClhHTMJdf+Ezc9ovuNNxcH89QfyclGthjZJOw==
+
+irc@^0.5.2:
+ version "0.5.2"
+ resolved "https://registry.yarnpkg.com/irc/-/irc-0.5.2.tgz#3714f4768365a96d0b2f776bc91166beb2464bbc"
+ integrity sha1-NxT0doNlqW0LL3dryRFmvrJGS7w=
+ dependencies:
+ irc-colors "^1.1.0"
+ optionalDependencies:
+ iconv "~2.2.1"
+ node-icu-charset-detector "~0.2.0"
+
+moment-timezone@^0.5.31:
+ version "0.5.31"
+ resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.31.tgz#9c40d8c5026f0c7ab46eda3d63e49c155148de05"
+ integrity sha512-+GgHNg8xRhMXfEbv81iDtrVeTcWt0kWmTEY1XQK14dICTXnWJnT0dxdlPspwqF3keKMVPXwayEsk1DI0AA/jdA==
+ dependencies:
+ moment ">= 2.9.0"
+
+"moment@>= 2.9.0":
+ version "2.29.0"
+ resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.0.tgz#fcbef955844d91deb55438613ddcec56e86a3425"
+ integrity sha512-z6IJ5HXYiuxvFTI6eiQ9dm77uE0gyy1yXNApVHqTcnIKfY9tIwEjlzsZ6u1LQXvVgKeTnv9Xm7NDvJ7lso3MtA==
+
+nan@^2.3.3, nan@^2.3.5:
+ version "2.14.1"
+ resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01"
+ integrity sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==
+
+node-fetch@^2.6.1:
+ version "2.6.1"
+ resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
+ integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
+
+node-icu-charset-detector@~0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/node-icu-charset-detector/-/node-icu-charset-detector-0.2.0.tgz#c2320da374ddcb671fc54cb4a0e041e156ffd639"
+ integrity sha1-wjINo3Tdy2cfxUy0oOBB4Vb/1jk=
+ dependencies:
+ nan "^2.3.3"
+
+original@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/original/-/original-1.0.2.tgz#e442a61cffe1c5fd20a65f3261c26663b303f25f"
+ integrity sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg==
+ dependencies:
+ url-parse "^1.4.3"
+
+querystringify@^2.1.1:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6"
+ integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==
+
+requires-port@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
+ integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=
+
+url-parse@^1.4.3:
+ version "1.4.7"
+ resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.7.tgz#a8a83535e8c00a316e403a5db4ac1b9b853ae278"
+ integrity sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg==
+ dependencies:
+ querystringify "^2.1.1"
+ requires-port "^1.0.0"