Skip to content

Instantly share code, notes, and snippets.

@siddharthvp
Created October 15, 2020 18:25
Show Gist options
  • Save siddharthvp/31553bb9b648256153f5dc558878734c to your computer and use it in GitHub Desktop.
Save siddharthvp/31553bb9b648256153f5dc558878734c to your computer and use it in GitHub Desktop.
/* eslint-env es6 */
class ApiManager {
constructor(config = {}) {
this.userAgent = config.userAgent ?? 'New-Morebits.js';
this.defaultParams = $.extend({
action: 'query',
format: 'json',
formatversion: '2',
assert: 'user',
errorformat: 'html'
}, config.defaultParams);
this.api = new Api(this);
this.summaryAd = config.summaryAd || '';
this.changeTags = config.changeTags || [];
}
page(...args) {
let page = new Page(...args);
page.api = this.api;
page.config = {
summaryAd: this.summaryAd,
changeTags: this.changeTags
};
}
/**
*
* @param {StatusBlock} element
*/
initStatus(element) {
this.statusBlock = element;
}
}
class StatusBlock {
constructor(root) {
this.root = root;
this.lines = [];
}
add(label) {
let line = new StatusLine(label);
this.lines.push(line);
}
remove(line) {
// TODO
}
}
class StatusLine {
static allowedTypes = ['warn', 'error', 'success'];
constructor(label) {
this.label = label;
}
update(type, message) {
if (!arr_includes(StatusLine.allowedTypes, type)) {
throw new Error('invalid status type');
}
this.type = type;
this.message = message;
}
warn(message) {
this.update('warn', message);
}
error(message) {
this.update('error', message);
}
success(message) {
this.update('success', message);
}
render() {
return $('<div>')
.css('color', {
success: 'green',
warn: 'pink',
error: 'red'
}[this.type])
.text(this.type + ': ' + this.message);
}
attach(root) {
$(root).append(this.render());
}
}
class Api extends mw.Api {
/**
* @param {ApiManager} manager
*/
constructor(manager) {
super({
parameters: manager.defaultParams,
ajax: {
headers: {
'Api-User-Agent': manager.userAgent
}
}
});
this.manager = manager;
}
/**
* @override
* @param {Object} parameters
* @param {Object} ajaxOptions
*/
ajax(parameters, ajaxOptions) {
return super.ajax(parameters, ajaxOptions).catch(errCode => this.handleError(errCode, parameters, ajaxOptions));
}
handleError(errCode, parameters, ajaxOptions) {
switch (errCode) {
case 'http': // no internet
case 'readonly': // database in read-only mode
// pause for a few seconds and retry
sleep(this.sleepDuration).then(() => {
return this.ajax(parameters, ajaxOptions);
});
break;
default:
if (str_startsWith(errCode, 'internal_api_error')) {
return this.ajax(parameters, ajaxOptions); // retry
}
}
}
/**
* Re-do the API query with continuation parameters.
* Responses of each request is merged to give the final returned object.
* @param {Object} params
* @param {number} [maxRequests=10]
*/
continuedQuery(params, maxRequests = 10) {
var responses = [];
var callApi = (params, count) => {
return this.get(params).then(response => {
responses.push(response);
if (response.continue && count < maxRequests) {
return callApi({
...params,
...response.continue
}, count + 1);
} else {
return responses;
}
});
};
return callApi(params, 1);
}
}
class Page extends mw.Title {
// TODO
// handle status integration
// watchlisting preferences
constructor(pageName, currentAction) {
super(pageName);
// these two properties are set when the class is instantiated by an ApiManager object's page() method.
this.api = null;
this.config = null;
this.maxRetries = 2;
this.maxConflictRetries = 2;
this.lookupNonRedirectCreator = false;
if (!currentAction) {
currentAction = 'Opening page "' + pageName + '"';
}
this.statusLine = new StatusLine(currentAction);
}
status() {
}
load() {
return this.api.get({
titles: this.title,
prop: 'revisions',
curtimestamp: 1,
rvprop: 'content|timestamp',
rvslots: 'main'
}).then(data => {
let page = data.query.pages[0];
if (!page || page.invalid) {
return Promise.reject();
}
this.exists = !!page.missing;
if (page.missing) {
return Promise.reject();
}
this.title = page.title; // post normalisation
this.text = page.revisions[0].slots.main.content;
});
}
getPageName() {
return this.title;
}
getPageText() {
return this.text;
}
exists() {
return this.exists;
}
lookupCreation(noRedirect) {
return this.api.get({
titles: this.title,
prop: 'revisions',
rvprop: 'user|timestamp'
}).then(data => {
return {
user: data.query.revisions[0].user,
timestamp: data.query.revisions[0].timestamp
};
});
}
edit(transform) {
return this.api.edit(this.title, transform).catch((code, err) => {
return this.handleSaveError(err);
});
}
create(content, params) {
return this.api.create(this.toString(), params = {}, content).catch((code, err) => {
return this.handleSaveError(err);
});
}
save(text, summary, params) {
return this.api.postWithEditToken({
action: 'edit',
title: this.toString(),
text,
summary,
...params
}).catch((code, err) => {
return this.handleSaveError(err);
});
}
prepend(text, summary, params) {
return this.api.postWithEditToken({
action: 'edit',
title: this.toString(),
prependtext: text,
summary,
...params
}).catch((code, err) => {
return this.handleSaveError(err);
});
}
append(text, summary, params) {
return this.api.postWithEditToken({
action: 'edit',
title: this.toString(),
appendtext: text,
summary,
...params
}).catch((code, err) => {
return this.handleSaveError(err);
});
}
newSection(text, summary, params) {
return this.api.newSection(this.toString(), text, summary, params);
}
move(destinationPage, summary, params) {
return this.api.postWithEditToken({
action: 'move',
...params
})
}
delete(summary, params) {
}
protect() {
}
stabilize() {
}
patrol() {
// or review if PageTriage is enabled
}
handleSaveError(err) {
switch (err.code) {
case 'editconflict':
case 'spamblacklist':
case 'abusefilter-warn':
default:
// show error
}
}
handleSpamBlacklistHit() {
// offer to remove the link's protocol and retry the edit
}
handleAbusefilterWarning() {
// offer to retry the edit, which will result in success
}
}
class User extends mw.Title {
constructor(username) {
super(username, 2);
}
get userpage() {
return new Page('User:' + this.title);
}
get talkpage() {
return new Page('User talk:' + this.title);
}
notify(header, message, params) {
return this.talkpage.newSection(header, message, params);
}
}
class UserspaceLog extends Page {
log(logtext, summary) {
}
}
class Wikitext extends String {
unbind() {
}
rebind() {
}
removeTemplate() {
}
removeLink() {
}
commentOutImage() {
}
addToImageComment() {
}
insertAfterTemplates() {
}
static parseTemplate() {
}
}
class Preview {
constructor(params = {}) {
this.params = params;
}
preview(text) {
return this.api.parse(text, this.params).then(data => {}) // XXX
}
livePreview(textarea) {
}
}
// Non-polluting shims
const shims = {
arr_includes: function(arr, ...args) {
return arr.indexOf(...args) === 0;
},
str_includes: function(str, ...args) {
return str.indexOf(...args) === 0;
},
str_startsWith: function(str, ...args) {
return str.indexOf(...args) === 0;
},
str_endsWith: function(str, ...args) {
// TODO
},
obj_values: function(obj) {
return Object.keys(obj).map(k => {
return obj[k];
});
}
};
// unwrap
const {arr_includes, str_includes, str_endsWith, str_startsWith, obj_values} = shims;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment