Created
October 15, 2020 18:25
-
-
Save siddharthvp/31553bb9b648256153f5dc558878734c to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* 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