Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

mwn

Package Overview
Dependencies
Maintainers
1
Versions
28
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

mwn - npm Package Compare versions

Comparing version 0.6.0 to 0.7.0

src/date.js

5

package.json
{
"name": "mwn",
"version": "0.6.0",
"version": "0.7.0",
"description": "MediaWiki bot framework for NodeJS",

@@ -29,2 +29,3 @@ "main": "./src/bot.js",

"axios-cookiejar-support": "^1.0.0",
"eventsource": "^1.0.7",
"form-data": "^3.0.0",

@@ -45,2 +46,2 @@ "oauth-1.0a": "^2.2.6",

}
}
}

136

src/bot.js

@@ -51,2 +51,3 @@ /**

const Title = require('./title');
const Xdate = require('./date');
const Page = require('./page');

@@ -57,2 +58,3 @@ const Wikitext = require('./wikitext');

const File = require('./file');
const Stream = require('./eventstream');
const static_utils = require('./static_utils');

@@ -204,2 +206,7 @@

/**
* Date class
*/
this.date = Xdate(this);
/**
* Page class associated with bot instance

@@ -229,2 +236,7 @@ */

/**
* Sub-class for the EventStreams API
*/
this.stream = Stream(mwn, this);
// set up any semlog options

@@ -554,3 +566,3 @@ semlog.updateConfig(this.options.semlog || {});

log(`[W] Encountered ${response.error.code} error, waiting for ${this.options.retryPause/1000} seconds before retrying`);
return sleep(this.options.retryPause).then(() => {
return this.sleep(this.options.retryPause).then(() => {
return this.request(params, customRequestOptions);

@@ -586,2 +598,8 @@ });

if (response.warnings && !this.suppressAPIWarnings) {
for (let [key, info] of Object.entries(response.warnings)) {
log(`[W] Warning received from API: ${key}: ${info.warnings}`);
}
}
return response;

@@ -595,3 +613,3 @@

customRequestOptions.retryNumber = requestOptions.retryNumber + 1;
return sleep(this.options.retryPause).then(() => {
return this.sleep(this.options.retryPause).then(() => {
return this.request(params, customRequestOptions);

@@ -841,4 +859,8 @@ });

/**
* Reads the content / and meta-data of one (or many) pages
* Reads the content and and meta-data of one (or many) pages.
* Content from the "main" slot is copied over to every revision object
* for easier referencing (`pg.revisions[0].content` can be used instead of
* `pg.revisions[0].slots.main.content`).
*
*
* @param {string|string[]|number|number[]} titles - for multiple pages use an array

@@ -854,2 +876,3 @@ * @param {Object} [options]

rvprop: 'content|timestamp',
rvslots: 'main',
redirects: '1'

@@ -859,2 +882,9 @@ }, makeTitles(titles), options),

var data = jsons.reduce((data, json) => {
json.query.pages.forEach(pg => {
if (pg.revisions) {
pg.revisions.forEach(rev => {
Object.assign(rev, rev.slots.main);
});
}
});
return data.concat(json.query.pages);

@@ -871,2 +901,3 @@ }, []);

rvprop: 'content',
rvslots: 'main',
redirects: '1'

@@ -879,2 +910,7 @@ }, makeTitles(titles), options),

for (let pg of response.query.pages) {
if (pg.revisions) {
pg.revisions.forEach(rev => {
Object.assign(rev, rev.slots.main);
});
}
yield pg;

@@ -1691,2 +1727,86 @@ }

/**
* Query the top contributors to the article using the WikiWho API.
* This API has a throttling of 2000 requests a day.
* Supported for EN, DE, ES, EU, TR Wikipedias only
* @see https://api.wikiwho.net/
* @param {string} title
* @returns {{totalBytes: number, users: ({id: number, name: string, bytes: number, percent: number})[]}}
*/
async queryAuthors(title) {
let langcodematch = this.options.apiUrl.match(/([^/]*?)\.wikipedia\.org/);
if (!langcodematch || !langcodematch[1]) {
throw new Error('WikiWho API is not supported for bot API url. Re-check.');
}
let json;
try {
json = await this.rawRequest({
url: `https://api.wikiwho.net/${langcodematch[1]}/api/v1.0.0-beta/latest_rev_content/${encodeURIComponent(title)}/?editor=true`
});
} catch(err) {
throw new Error(err && err.response && err.response.data
&& err.response.data.Error);
}
const tokens = Object.values(json.revisions[0])[0].tokens;
let data = {
totalBytes: 0,
users: []
}, userdata = {};
for (let token of tokens) {
data.totalBytes += token.str.length;
let editor = token['editor'];
if (!userdata[editor]) {
userdata[editor] = { bytes: 0 };
}
userdata[editor].bytes += token.str.length;
if (editor.startsWith('0|')) { // IP
userdata[editor].name = editor.slice(2);
}
}
Object.entries(userdata).map(([userid, {bytes}]) => {
userdata[userid].percent = bytes / data.totalBytes;
if (userdata[userid].percent < 0.02) {
delete userdata[userid];
}
});
await this.request({
"action": "query",
"list": "users",
"ususerids": Object.keys(userdata).filter(us => !us.startsWith('0|')) // don't lookup IPs
}).then(json => {
json.query.users.forEach(us => {
userdata[String(us.userid)].name = us.name;
});
});
data.users = Object.entries(userdata).map(([userid, {bytes, name, percent}]) => {
return {
id: userid,
name: name,
bytes: bytes,
percent: percent
};
}).sort((a, b) => {
a.bytes < b.bytes ? 1 : -1;
});
return data;
}
/**
* Promisified version of setTimeout
* @param {number} duration - of sleep in milliseconds
*/
sleep(duration) {
return new Promise(resolve => {
setTimeout(resolve, duration);
});
}
}

@@ -1779,12 +1899,2 @@

/**
* Promisified version of setTimeout
* @param {number} duration - of sleep in milliseconds
*/
var sleep = function(duration) {
return new Promise(resolve => {
setTimeout(resolve, duration);
});
};
var makeTitles = function(pages) {

@@ -1791,0 +1901,0 @@ pages = Array.isArray(pages) ? pages : [ pages ];

@@ -106,3 +106,24 @@ module.exports = function(bot) {

/**
* Get list of pages transcluding this page
* @returns {Promise<String[]>}
*/
transclusions() {
return bot.continuedQuery({
"action": "query",
"prop": "transcludedin",
"titles": this.toString(),
"tiprop": "title",
"tilimit": "max"
}).then(jsons => {
var pages = jsons.reduce((pages, json) => pages.concat(json.query.pages), []);
var page = pages[0];
if (page.missing) {
return Promise.reject('missingarticle');
}
return page.transcludedin.map(pg => pg.title);
});
}
/**

@@ -206,2 +227,6 @@ * Returns list of images on the page

/**
* Get username of the last deleting admin (or null)
* @returns {Promise<string>}
*/
getDeletingAdmin() {

@@ -223,2 +248,17 @@ return bot.request({

getDescription(customOptions) {
return bot.request({
action: 'query',
prop: 'description',
titles: this.toString(),
...customOptions
}).then(data => {
var page = data.query.pages[0];
if (page.missing) {
return Promise.reject('missingarticle');
}
return data.query.pages[0].description;
});
}
/**

@@ -225,0 +265,0 @@ * Get the edit history of the page

@@ -134,6 +134,6 @@ /**

_getheaderline(header) {
if (typeof header === 'object') {
var text = `scope="col"`;
for (let [key, value] of Object.entries(header)) {
_makecell(cell, isHeader) {
if (typeof cell === 'object') {
let text = isHeader ? `scope="col"` : ``;
for (let [key, value] of Object.entries(cell)) {
if (key === 'label') {

@@ -144,6 +144,6 @@ continue;

}
text += ` | ${header.label}`;
text += ` | ${cell.label}`;
return text;
}
return header;
return cell;
}

@@ -157,5 +157,5 @@ /**

if (this.multiline) {
this.text += headers.map(e => `! ${this._getheaderline(e)} \n`).join('');
this.text += headers.map(e => `! ${this._makecell(e, true)} \n`).join('');
} else {
this.text += `! ` + headers.map(this._getheaderline).join(' !! ') + '\n';
this.text += `! ` + headers.map(e => this._makecell(e, true)).join(' !! ') + '\n';
}

@@ -176,5 +176,5 @@ }

if (this.multiline) {
this.text += fields.map(e => `| ${e} \n`).join('');
this.text += fields.map(e => `| ${this._makecell(e)} \n`).join('');
} else {
this.text += `| ` + fields.join(' || ') + '\n';
this.text += `| ` + fields.map(this._makecell).join(' || ') + '\n';
}

@@ -181,0 +181,0 @@ }

@@ -13,2 +13,5 @@ module.exports = function(bot) {

/**
* @returns {bot.page}
*/
get userpage() {

@@ -20,2 +23,5 @@ // User: namespace name will work across all MW sites

/**
* @returns {bot.page}
*/
get talkpage() {

@@ -72,2 +78,17 @@ return new bot.page('User talk:' + this.username);

/**
* Get global user info for wikis with CentralAuth
* @param {("groups"|"rights"|"merged"|"unattached"|"editcount")[]} props
*/
globalinfo(props) {
return bot.request({
"action": "query",
"meta": "globaluserinfo",
"guiuser": this.username,
"guiprop": props || ''
}).then(data => {
return data.query.globaluserinfo;
});
}
/**
* Post a message on user's talk page

@@ -74,0 +95,0 @@ * @param {string} header

@@ -5,3 +5,4 @@ module.exports = function (bot) {

* Class for some basic wikitext parsing, involving
* links, files, categories, templates and simple tables.
* links, files, categories, templates and simple tables
* and sections.
*

@@ -25,8 +26,2 @@ * For more advanced and sophisticated wikitext parsing, use

this.text = wikitext;
this.unbinder = {
counter: 0,
history: {},
prefix: '%UNIQ::' + Math.random() + '::',
postfix: '::UNIQ%'
};
}

@@ -40,8 +35,10 @@

var n = this.text.length;
let n = this.text.length;
// files can have links in captions; use a stack to handle the nesting
var stack = new Stack();
let stack = new Stack();
for (let i = 0; i < n; i++) {
if (this.text[i] === '[' && this.text[i + 1] === '[') {
stack.push({ startIdx: i });
stack.push({
startIdx: i
});
i++;

@@ -74,15 +71,119 @@ } else if (this.text[i] === ']' && this.text[i + 1] === ']' && stack.top()) {

* count: number}} config
* @config {boolean} recursive - also parse templates within subtemplates
* @config {function} namePredicate - include template in result only if the its name matches this predicate
* More efficient than templatePredicate as the template parameters aren't parsed if name didn't match.
* @config {function} templatePredicate - include template in result only if it matches this predicate
* @config {number} count - max number of templates to be parsed. If recursive is set true, note that
* templates are parsed breadth-first, not depth-first.
* @config {boolean} recursive - also parse templates within subtemplates. The other
* config parameters (namePredicate, templatePredicate, count) are *not* compatible
* with recursive mode. Expect unexpected results if used.
* @config {function} namePredicate - include template in result only if the its name
* matches this predicate. More efficient than templatePredicate as the template parameters
* aren't parsed if name didn't match.
* @config {function} templatePredicate - include template in result only if it matches
* this predicate
* @config {number} count - max number of templates to be parsed.
* @returns {Template[]}
*/
parseTemplates(config) {
return this.templates = parseTemplates(this.text, config);
return this.templates = Wikitext.parseTemplates(this.text, config);
}
// parseTemplates() and processTemplateText() are adapted from
// https://en.wikipedia.org/wiki/MediaWiki:Gadget-libExtraUtil.js written by Evad37
// which was in turn adapted from https://en.wikipedia.org/wiki/User:SD0001/parseAllTemplates.js
// written by me. (cc-by-sa/GFDL)
/**
* @inheritdoc
*/
static parseTemplates(wikitext, config) {
config = config || {
recursive: false,
namePredicate: null,
templatePredicate: null,
count: null
};
const result = [];
const n = wikitext.length;
// number of unclosed braces
let numUnclosed = 0;
// are we inside a comment, or between nowiki tags, or in a {{{parameter}}}?
let inComment = false;
let inNowiki = false;
let inParameter = false;
let startIdx, endIdx;
for (let i = 0; i < n; i++) {
if (!inComment && !inNowiki && !inParameter) {
if (wikitext[i] === '{' && wikitext[i + 1] === '{' && wikitext[i + 2] === '{' && wikitext[i + 3] !== '{') {
inParameter = true;
i += 2;
} else if (wikitext[i] === '{' && wikitext[i + 1] === '{') {
if (numUnclosed === 0) {
startIdx = i + 2;
}
numUnclosed += 2;
i++;
} else if (wikitext[i] === '}' && wikitext[i + 1] === '}') {
if (numUnclosed === 2) {
endIdx = i;
let templateWikitext = wikitext.slice(startIdx, endIdx); // without braces
let processed = processTemplateText(templateWikitext, config.namePredicate, config.templatePredicate);
if (processed) {
result.push(processed);
}
if (config.count && result.length === config.count) {
return result;
}
}
numUnclosed -= 2;
i++;
} else if (wikitext[i] === '|' && numUnclosed > 2) {
// swap out pipes in nested templates with \x01 character
wikitext = strReplaceAt(wikitext, i, '\x01');
} else if (/^<!--/.test(wikitext.slice(i, i + 4))) {
inComment = true;
i += 3;
} else if (/^<nowiki ?>/.test(wikitext.slice(i, i + 9))) {
inNowiki = true;
i += 7;
}
} else { // we are in a comment or nowiki or {{{parameter}}}
if (wikitext[i] === '|') {
// swap out pipes with \x01 character
wikitext = strReplaceAt(wikitext, i, '\x01');
} else if (/^-->/.test(wikitext.slice(i, i + 3))) {
inComment = false;
i += 2;
} else if (/^<\/nowiki ?>/.test(wikitext.slice(i, i + 10))) {
inNowiki = false;
i += 8;
} else if (wikitext[i] === '}' && wikitext[i + 1] === '}' && wikitext[i + 2] === '}') {
inParameter = false;
i += 2;
}
}
}
if (config.recursive) {
let subtemplates = result.map(template => {
return template.wikitext.slice(2, -2);
}).filter(templateWikitext => {
return /\{\{.*\}\}/s.test(templateWikitext);
}).map(templateWikitext => {
return Wikitext.parseTemplates(templateWikitext, config);
});
return result.concat.apply(result, subtemplates);
}
return result;
}
/**
* Remove a template, link, file or category from the text

@@ -102,3 +203,3 @@ * CAUTION: If an entity with the very same wikitext exists earlier in the text,

*
* eg. let u = new bot.wikitext("Hello world <!-- world --> world");
* eg. let u = new bot.wikitext("Hello world <!-- world --> world");
* u.unbind('<!--','-->');

@@ -116,2 +217,10 @@ * u.content = u.content.replace(/world/g, 'earth');

unbind(prefix, postfix) {
if (!this.unbinder) {
this.unbinder = {
counter: 0,
history: {},
prefix: '%UNIQ::' + Math.random() + '::',
postfix: '::UNIQ%'
};
}
let re = new RegExp(prefix + '([\\s\\S]*?)' + postfix, 'g');

@@ -125,3 +234,3 @@ this.text = this.text.replace(re, match => {

}
/**

@@ -133,3 +242,2 @@ * Rebind after unbinding.

let content = this.text;
content.self = this;
for (let [current, replacement] of Object.entries(this.unbinder.history)) {

@@ -179,17 +287,17 @@ content = content.replace(current, replacement);

// number of unclosed brackets
let tlevel = 0, llevel = 0;
let tlevel = 0,
llevel = 0;
let n = text.length;
for (let i = 0; i < n; i++) {
if (text[i] === '{' && text[i+1] === '{') {
if (text[i] === '{' && text[i + 1] === '{') {
tlevel++;
i++;
} else if (text[i] === '[' && text[i+1] === '[') {
} else if (text[i] === '[' && text[i + 1] === '[') {
llevel++;
i++;
} else if (text[i] === '}' && text[i+1] === '}') {
} else if (text[i] === '}' && text[i + 1] === '}') {
tlevel--;
i++;
} else if (text[i] === ']' && text[i+1] === ']') {
} else if (text[i] === ']' && text[i + 1] === ']') {
llevel--;

@@ -229,3 +337,3 @@ i++;

rows.forEach((row, idx) => {
let cells = row.split(/^\|/m).slice(1); // slice(1) removes the emptiness or the row styles if present
let cells = row.split(/^\|/m).slice(1); // slice(1) removes the emptiness or the row styles if present

@@ -253,6 +361,7 @@ if (cells.length === 1) { // non-multilined

/**
* Parse sections from wikitext
* @param {string} text
* CAUTION: section header syntax in comments, nowiki tags,
* pre, source or syntaxhighlight tags can lead to wrong results.
* You're advised to run unbind() first.
* @returns {{level: number, header: string, index: number, content: string}[]} array of

@@ -263,11 +372,18 @@ * section objects. Each section object has the level, header, index (of beginning) and content.

*/
parseSections() {
return this.sections = Wikitext.parseSections(this.text);
}
// XXX: fix jsdocs
/**
* @inheritdoc
*/
static parseSections(text) {
const rgx = /^(=+)(.*?)\1/mg;
let sections = [
{
level: 1,
header: null,
index: 0
}
];
let sections = [{
level: 1,
header: null,
index: 0
}];
let match;

@@ -298,6 +414,7 @@ while (match = rgx.exec(text)) { // eslint-disable-line no-cond-assign

}
var processLink = function (self, startIdx, endIdx) {
var linktext = self.text.slice(startIdx, endIdx + 1);
var [target, displaytext] = linktext.slice(2, -2).split('|');
var noSortkey = false;
const processLink = function (self, startIdx, endIdx) {
let linktext = self.text.slice(startIdx, endIdx + 1);
let [target, displaytext] = linktext.slice(2, -2).split('|');
let noSortkey = false;
if (!displaytext) {

@@ -307,3 +424,3 @@ displaytext = target[0] === ':' ? target.slice(1) : target;

}
var title = bot.title.newFromText(target);
let title = bot.title.newFromText(target);
if (!title) {

@@ -369,11 +486,12 @@ return;

return this.parameters.find(p => {
return p.name == paramName;
return p.name == paramName; // == is intentional
});
}
getValue(paramName) {
var param = this.getParam(paramName);
let param = this.getParam(paramName);
return param ? param.value : null;
}
setName(name) {
this.name = name.trim();
name = name.trim();
this.name = name[0] ? (name[0].toUpperCase() + name.slice(1)) : name;
this.nameTitle = bot.title.newFromText(name, 10);

@@ -383,104 +501,3 @@ }

// parseTemplates() and processTemplateText() are adapted from
// https://en.wikipedia.org/wiki/MediaWiki:Gadget-libExtraUtil.js written by Evad37
// which was in turn adapted from https://en.wikipedia.org/wiki/User:SD0001/parseAllTemplates.js
// written by me. (cc-by-sa/GFDL)
/**
* @inheritdoc
*/
const parseTemplates = function (wikitext, config) {
config = config || {
recursive: false,
namePredicate: null,
templatePredicate: null,
count: null
};
const result = [];
const n = wikitext.length;
// number of unclosed braces
var numUnclosed = 0;
// are we inside a comment, or between nowiki tags, or in a {{{parameter}}}?
var inComment = false;
var inNowiki = false;
var inParameter = false;
var startIdx, endIdx;
for (var i = 0; i < n; i++) {
if (!inComment && !inNowiki && !inParameter) {
if (wikitext[i] === '{' && wikitext[i + 1] === '{' && wikitext[i + 2] === '{' && wikitext[i + 3] !== '{') {
inParameter = true;
i += 2;
} else if (wikitext[i] === '{' && wikitext[i + 1] === '{') {
if (numUnclosed === 0) {
startIdx = i + 2;
}
numUnclosed += 2;
i++;
} else if (wikitext[i] === '}' && wikitext[i + 1] === '}') {
if (numUnclosed === 2) {
endIdx = i;
var templateWikitext = wikitext.slice(startIdx, endIdx); // without braces
var processed = processTemplateText(templateWikitext, config.namePredicate, config.templatePredicate);
if (processed) {
result.push(processed);
}
if (config.count && result.length === config.count) {
return result;
}
}
numUnclosed -= 2;
i++;
} else if (wikitext[i] === '|' && numUnclosed > 2) {
// swap out pipes in nested templates with \1 character
wikitext = strReplaceAt(wikitext, i, '\1');
} else if (/^<!--/.test(wikitext.slice(i, i + 4))) {
inComment = true;
i += 3;
} else if (/^<nowiki ?>/.test(wikitext.slice(i, i + 9))) {
inNowiki = true;
i += 7;
}
} else { // we are in a comment or nowiki or {{{parameter}}}
if (wikitext[i] === '|') {
// swap out pipes with \1 character
wikitext = strReplaceAt(wikitext, i, '\1');
} else if (/^-->/.test(wikitext.slice(i, i + 3))) {
inComment = false;
i += 2;
} else if (/^<\/nowiki ?>/.test(wikitext.slice(i, i + 10))) {
inNowiki = false;
i += 8;
} else if (wikitext[i] === '}' && wikitext[i + 1] === '}' && wikitext[i + 2] === '}') {
inParameter = false;
i += 2;
}
}
}
if (config.recursive) {
var subtemplates = result.map(template => {
return template.wikitext.slice(2, -2);
}).filter(templateWikitext => {
return /\{\{.*\}\}/s.test(templateWikitext);
}).map(templateWikitext => {
return parseTemplates(templateWikitext, config);
});
return result.concat.apply(result, subtemplates);
}
return result;
};
/**
* @param {string} text - template wikitext without braces, with the pipes in

@@ -492,17 +509,19 @@ * nested templates replaced by \x01

const template = new Template('{{' + text.replace(/\1/g, '|') + '}}');
// eslint-disable-next-line no-control-regex
const template = new Template('{{' + text.replace(/\x01/g, '|') + '}}');
// swap out pipe in links with \1 control character
// swap out pipe in links with \x01 control character
// [[File: ]] can have multiple pipes, so might need multiple passes
while (/(\[\[[^\]]*?)\|(.*?\]\])/g.test(text)) {
text = text.replace(/(\[\[[^\]]*?)\|(.*?\]\])/g, '$1\1$2');
text = text.replace(/(\[\[[^\]]*?)\|(.*?\]\])/g, '$1\x01$2');
}
const [name, ...parameterChunks] = text.split('|').map(chunk => {
// change '\1' control characters back to pipes
return chunk.replace(/\1/g, '|');
// change '\x01' control characters back to pipes
// eslint-disable-next-line no-control-regex
return chunk.replace(/\x01/g, '|');
});
template.setName(name);
if (namePredicate && !namePredicate(template.name)) {

@@ -512,12 +531,12 @@ return null;

var unnamedIdx = 1;
let unnamedIdx = 1;
parameterChunks.forEach(function (chunk) {
var indexOfEqualTo = chunk.indexOf('=');
var indexOfOpenBraces = chunk.indexOf('{{');
let indexOfEqualTo = chunk.indexOf('=');
let indexOfOpenBraces = chunk.indexOf('{{');
var isWithoutEquals = !chunk.includes('=');
var hasBracesBeforeEquals = chunk.includes('{{') && indexOfOpenBraces < indexOfEqualTo;
var isUnnamedParam = (isWithoutEquals || hasBracesBeforeEquals);
let isWithoutEquals = !chunk.includes('=');
let hasBracesBeforeEquals = chunk.includes('{{') && indexOfOpenBraces < indexOfEqualTo;
let isUnnamedParam = (isWithoutEquals || hasBracesBeforeEquals);
var pName, pNum, pVal;
let pName, pNum, pVal;
if (isUnnamedParam) {

@@ -551,2 +570,2 @@ // Get the next number not already used by either an unnamed parameter,

};
};
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc