Content deleted Content added
[5ad0992a] CCI fixes |
v0.2.0; https://github.com/ChlodAlejandro/deputy/blob/main/CHANGELOG.md#v020---2022-10-20 |
||
Line 990:
findContributionSurveyHeadings() {
if (!DeputyCasePage.isCasePage()) {
throw new Error('Current page is not a case page. Expected subpage of '
DeputyCasePage.rootPage.getPrefixedText());
}
else {
Line 1,334 ⟶ 1,335:
Object.assign(this, revisionData);
this.row = row;
}
}
/**
* Data that constructs a raw contribution survey row.
*/
/**
* Parser for {@link ContributionSurveyRow}s.
*
* This is used directly in unit tests. Do not import unnecessary
* dependencies, as they may indirectly import the entire Deputy
* codebase outside a browser environment.
*/
class ContributionSurveyRowParser {
/**
*
* @param wikitext
*/
constructor(wikitext) {
this.wikitext = wikitext;
this.current = wikitext;
}
/**
* Parses a wikitext contribution survey row into a {@link RawContributionSurveyRow}.
* If invalid, an Error is thrown with relevant information.
*
* @return Components of a parsed contribution survey row.
*/
parse() {
var _a, _b;
this.current = this.wikitext;
const bullet = this.eatUntil(/^[^*\s]/g);
if (!bullet) {
throw new Error('dp-malformed-row');
}
const creation = this.eatExpression(/^\s*'''N'''\s*/g) != null;
const page = this.eatExpression(/\[\[([^\]|]+)(?:\|.*)?]]/g, 1);
if (!page) {
// Malformed or unparsable listing.
throw new Error('dp-undetectable-page-name');
}
let extras =
// [[Special:Diff/12345|6789]]
(_a = this.eatUntil(/^(?:'''?)?\[\[Special:Diff\/\d+/, true)) !== null && _a !== void 0 ? _a :
// {{dif|12345|6789}}
this.eatUntil(/^(?:'''?)?{{dif\|\d+/, true);
// At this point, `extras` is either a string or `null`. If it's a string,
// extras exist, and we should add them. If not, there's likely no more
// revisions to be processed here, and can assume that the rest is user comments.
const revids = [];
let diffs = null, comments, diffTemplate = '[[Special:Diff/$1|($2)]]';
if (extras) {
const starting = this.current;
let diff = true;
while (diff) {
diff =
// [[Special:Diff/12345|6789]]
(_b = this.eatExpression(/(?:'''?)?\[\[Special:Diff\/(\d+)(?:\|[^\]]*)?]](?:'''?)?/g, 1)) !== null && _b !== void 0 ? _b :
// {{dif|12345|6789}}
this.eatExpression(/(?:'''?)?{{dif\|(\d+)\|[^}]+}}(?:'''?)?/g, 1);
if (diff != null) {
revids.push(+diff);
}
}
// All diff links removed. Get diff of starting and current to get entire diff part.
diffs = starting.slice(0, starting.length - this.current.length);
// Dawkeye support.
if ((diffs !== null && diffs !== void 0 ? diffs : '').includes('{{dif')) {
diffTemplate = '$3{{dif|$1|$2}}$3';
}
// Comments should be empty, but just in case we do come across comments.
comments = this.isEmpty() ? null : this.eatRemaining();
}
else {
// Try to grab extras. This is done by detecting any form of parentheses and
// matching them, including any possible included colon. If that doesn't work,
// try pulling out just the colon.
const maybeExtras = this.eatExpression(/\s*(?::\s*)?\(.+?\)(?:\s*:)?\s*/) || this.eatExpression(/\s*:\s*/g);
if (maybeExtras) {
extras = maybeExtras;
}
// Only comments probably remain. Eat out whitespaces and the rest is a comment.
extras = (extras || '') + (this.eatUntil(/^\S/g, true) || '');
if (extras === '') {
extras = null;
}
comments = this.getCurrentLength() > 0 ? this.eatRemaining() : null;
}
// "{bullet}{creation}[[{page}]]{extras}{diffs}{comments}"
return {
type: (extras || comments || diffs) == null ? 'pageonly' : 'detailed',
bullet,
creation,
page,
extras,
diffs,
comments,
revids,
diffTemplate
};
}
/**
* Returns `true` if the working string is empty.
*
* @return `true` if the length of `current` is zero. `false` if otherwise.
*/
isEmpty() {
return this.current.length === 0;
}
/**
* @return the length of the working string.
*/
getCurrentLength() {
return this.current.length;
}
/**
* Views the next character to {@link ContributionSurveyRowParser#eat}.
*
* @return The first character of the working string.
*/
peek() {
return this.current[0];
}
/**
* Pops the first character off the working string and returns it.
*
* @return First character of the working string, pre-mutation.
*/
eat() {
const first = this.current[0];
this.current = this.current.slice(1);
return first;
}
/**
* Continue eating from the string until a string or regular expression
* is matched. Unlike {@link eatExpression}, passed regular expressions
* will not be re-wrapped with `^(?:)`. These must be added on your own if
* you wish to match the start of the string.
*
* @param pattern The string or regular expression to match.
* @param noFinish If set to `true`, `null` will be returned instead if the
* pattern is never matched. The working string will be reset to its original
* state if this occurs. This prevents the function from being too greedy.
* @return The consumed characters.
*/
eatUntil(pattern, noFinish) {
const starting = this.current;
let consumed = '';
while (this.current.length > 0) {
if (typeof pattern === 'string') {
if (this.current.startsWith(pattern)) {
return consumed;
}
}
else {
if (cloneRegex$1(pattern).test(this.current)) {
return consumed;
}
}
consumed += this.eat();
}
if (noFinish && this.current.length === 0) {
// We finished the string! Reset.
this.current = starting;
return null;
}
else {
return consumed;
}
}
/**
* Eats a given expression from the start of the working string. If the working
* string does not contain the given expression, `null` is returned (and not a
* blank string). Only eats once, so any expression must be greedy if different
* behavior is expected.
*
* The regular expression passed into this function is automatically re-wrapped
* with `^(?:<source>)`. Avoid adding these expressions on your own.
*
* @param pattern The pattern to match.
* @param n The capture group to return (returns the entire string (`0`) by default)
* @return The consumed characters.
*/
eatExpression(pattern, n = 0) {
const expression = new RegExp(`^(?:${pattern.source})`,
// Ban global and multiline, useless since this only matches once and to
// ensure that the reading remains 'flat'.
pattern.flags.replace(/[gm]/g, ''));
const match = expression.exec(this.current);
if (match) {
this.current = this.current.slice(match[0].length);
return match[n];
}
else {
return null;
}
}
/**
* Consumes the rest of the working string.
*
* @return The remaining characters in the working string.
*/
eatRemaining() {
const remaining = this.current;
this.current = '';
return remaining;
}
}
Line 1,365 ⟶ 1,572:
*/
constructor(casePage, wikitext) {
this.data = new ContributionSurveyRowParser(wikitext).parse();
this.casePage = casePage;
this.wikitext = wikitext;
this.title = new mw.Title(
this.extras =
this.comment =
this.status = this.originalStatus =
ContributionSurveyRowStatus.Unfinished :
ContributionSurveyRow.identifyCommentStatus(
if (ContributionSurveyRow.commentMatchRegex[this.status] != null) {
if (cloneRegex$1((ContributionSurveyRow.commentMatchRegex)[this.status], { pre: '^' }).test(this.comment)) {
Line 1,385 ⟶ 1,592:
}
}
}
/**
Line 1,433 ⟶ 1,631:
return this.diffs;
}
const revisionData = new Map();
const
// Load revision information
for
}
}
}
if (toCache.length > 0) { }
}
Line 1,506 ⟶ 1,690:
}
}
ContributionSurveyRow.Parser = ContributionSurveyRowParser;
/**
* A set of regular expressions that will match a specific contribution survey row
Line 1,677 ⟶ 1,854:
const userContribsPage = new mw.Title('Special:Contributions/' + user);
return h_1("span", { class: "history-user" },
h_1("a", { class: "mw-userlink", target: "_blank", rel: "noopener", href: mw.format(mw.config.get('wgArticlePath'), userPage.getPrefixedDb()), title: userPage.getPrefixedText() }, userPage.
" ",
h_1("span", { class: "mw-usertoollinks mw-changeslist-links" },
Line 1,725 ⟶ 1,902:
* A specific revision for a section row.
*/
class DeputyContributionSurveyRevision extends
/**
* @param revision
Line 1,821 ⟶ 1,998:
this.completedCheckbox.on('change', (checked) => {
var _a, _b, _c;
this.
detail: {
checked: checked,
revision: this.revision
}
}));
window.deputy.comms.send({
type: 'revisionStatusUpdate',
Line 1,959 ⟶ 2,141:
const parser = window.deputy.session.rootSession.parser;
// Use DiscussionTools to identify the user and timestamp.
try { parsedComment = (_b = (_a = parser.parse(props.originalElement)) === null || _a === void 0 ? void 0 : _a.commentItems) === null || _b === void 0 ? void 0 : _b[0]; }
catch (e) {
console.warn('Failed to parse user signature.', e);
}
if (!parsedComment) {
// See if the Deputy trace exists.
Line 2,426 ⟶ 2,614:
}
const finished = (_a = this.wasFinished) !== null && _a !== void 0 ? _a : false;
// "* "
let result =
if (
result += "'''N''' ";
}
// [[:Example]]
result += `[[
// "{bullet}{creation}[[{page}]]{extras}{diffs}{comments}"
if (this.row.extras) {
result += `
}
const unfinishedDiffs = (_c = (_b = this.revisions) === null || _b === void 0 ? void 0 : _b.filter((v) => !v.completed)) !== null && _c !== void 0 ? _c : [];
if (unfinishedDiffs.length > 0) {
result += unfinishedDiffs.map((v) => {
return
}).join('');
}
Line 2,657 ⟶ 2,843:
for (const revision of diffs.values()) {
const revisionUIEl = new DeputyContributionSurveyRevision(revision, this);
revisionUIEl.
// Recheck options first to avoid "Unfinished" being selected when done.
this.onUpdate();
Line 2,835 ⟶ 3,021:
this.element = h_1(DeputyLoadingDots, null);
this.rootElement = h_1("div", { class: "dp-cs-row" }, this.element);
return this.rootElement;
}
Line 3,152 ⟶ 3,337:
* modifying it manually.
*/
var deputyVersion = /* v */ '0.
/**
Line 3,256 ⟶ 3,441:
final.push(obj);
}
else
final.push(obj.wikitext);
}
}
Line 3,407 ⟶ 3,589:
const line = wikitextLines[i];
let rowElement;
const csr = new ContributionSurveyRow(this.casePage, line);
rowElement = new DeputyContributionSurveyRow(csr, rowElements[csr.title.getPrefixedText()], line, this);
}
// Only trigger on actual bulleted lists.
if (line.startsWith('*')) {
console.warn('Could not parse row.', line, e);
// For debugging and tests.
mw.hook('deputy.errors.cciRowParse').fire({ line, error: e });
}
rowElement = line;
}
Line 3,490 ⟶ 3,678:
return false;
});
});
}
/**
* Makes all rows of this section being loading data.
*
* @return A Promise that resolves when all rows have finished loading data.
*/
loadData() {
return __awaiter(this, void 0, void 0, function* () {
// For debugging and tests.
if (window.deputy.NO_ROW_LOADING !== true) {
yield Promise.all(this.rows.map(row => row.loadData()));
}
});
}
Line 3,502 ⟶ 3,703:
});
this.closeButton = new OO.ui.ButtonWidget({
label: mw.msg('deputy.
});
this.reviewButton = new OO.ui.ButtonWidget({
Line 3,831 ⟶ 4,032:
static startSession(section, _casePage) {
return __awaiter(this, void 0, void 0, function* () {
const
// Save session to storage
const casePage = _casePage !== null && _casePage !== void 0 ? _casePage : yield DeputyCasePage.build();
const session = yield this.setSession({
casePageId: casePage.pageId,
caseSections:
});
const rootSession = window.deputy.session.rootSession =
Line 3,941 ⟶ 4,142:
yield this.closeSession();
}
mw.hook('deputy.load.cci.root').fire();
res();
}));
Line 4,041 ⟶ 4,243:
yield casePage.addActiveSection(sectionName);
heading.insertAdjacentElement('afterend', el.render());
yield el.loadData();
mw.hook('deputy.load.cci.session').fire();
return true;
});
Line 4,940 ⟶ 5,144:
}
}
/**
* Sleep for an specified amount of time.
*
* @param ms Milliseconds to sleep for.
*/
function sleep(ms) {
return __awaiter(this, void 0, void 0, function* () {
return new Promise((res) => {
setTimeout(res, ms);
});
});
}
/**
* Handles requests that might get hit by a rate limit. Wraps around
* `fetch` and ensures that all users of the Requester only request
* a single time per 100 ms on top of the time it takes to load
* previous requests. Also runs on four "threads", allowing at
* least a certain level of asynchronicity.
*
* Particularly used when a multitude of requests have a chance to
* DoS a service.
*/
class Requester {
/**
* Processes things in the fetchQueue.
*/
static processFetch() {
return __awaiter(this, void 0, void 0, function* () {
if (Requester.fetchActive >= Requester.maxThreads) {
return;
}
Requester.fetchActive++;
const next = Requester.fetchQueue.shift();
if (next) {
const data =
// eslint-disable-next-line prefer-spread
yield fetch.apply(null, next[1])
.then((res) => {
// Return false for survivable cases. In this case, we'll re-queue
// the request.
if (res.status === 429 || res.status === 502) {
return res.status;
}
else {
return res;
}
}, next[0][1]);
if (data instanceof Response) {
next[0][0](data);
}
else if (typeof data === 'number') {
Requester.fetchQueue.push(next);
}
}
yield sleep(Requester.minTime);
Requester.fetchActive--;
setTimeout(Requester.processFetch, 0);
});
}
}
/**
* Maximum number of requests to be processed simultaneously.
*/
Requester.maxThreads = 4;
/**
* Minimum amount of milliseconds to wait between each request.
*/
Requester.minTime = 100;
/**
* Requests to be performed. Takes tuples containing a resolve-reject pair and arguments
* to be passed into the fetch function.
*/
Requester.fetchQueue = [];
/**
* Number of requests currently being processed. Must be lower than
* {@link maxThreads}.
*/
Requester.fetchActive = 0;
Requester.fetch = (...args) => {
let res, rej;
const fakePromise = new Promise((_res, _rej) => {
res = _res;
rej = _rej;
});
Requester.fetchQueue.push([[res, rej], args]);
setTimeout(Requester.processFetch, 0);
return fakePromise;
};
/**
Line 4,955 ⟶ 5,249:
return __awaiter(this, void 0, void 0, function* () {
// TODO: Make logout API request
yield window.deputy.storage.setKV('api-token', null);
});
}
Line 4,979 ⟶ 5,273:
getExpandedRevisionData(revisions) {
return __awaiter(this, void 0, void 0, function* () {
return Requester.fetch(`https://zoomiebot.toolforge.org/bot/api/deputy/v1/revisions/${mw.config.get('wgWikiID')}`, {
method: 'POST',
headers: {
Line 10,846 ⟶ 11,140:
*/
set(v, throwOnInvalid = false) {
if (this.locked) {
console.warn('Attempted to modify locked setting.');
return;
}
if (this.allowedValues) {
const keys = Array.isArray(this.allowedValues) ?
Line 10,882 ⟶ 11,180:
load(raw) {
return (this.value = this.deserialize(raw));
}
/**
* Prevents the value of the setting from being changed. Used for debugging.
*/
lock() {
this.locked = true;
}
/**
* Allows the value of the setting to be changed. Used for debugging.
*/
unlock() {
this.locked = false;
}
}
Line 12,580 ⟶ 12,890:
"deputy.session.otherActive.button": "Stop session",
"deputy.session.add": "Start working on this section",
"deputy.session.section.close": "
"deputy.session.section.closeComments": "
"deputy.session.section.closeHelp": "Your signature will automatically be included.",
"deputy.session.section.closeWarn": "You have unsaved changes. Close the section without saving?",
"deputy.session.section.stop": "Stop session",
"deputy.session.section.saved": "Section saved",
"deputy.session.section.failed": "Failed to save section",
Line 14,635 ⟶ 14,946:
}
return this._windowManager;
}
/**
* Initialize Deputy.
*/
static init() {
return __awaiter(this, void 0, void 0, function* () {
Deputy.instance = new Deputy();
window.deputy = Deputy.instance;
return window.deputy.init();
});
}
/**
Line 14,700 ⟶ 15,021:
}
}
mw.loader.using([
'mediawiki.api',
'mediawiki.jqueryMsg',
'mediawiki.Title',
'mediawiki.util',
Line 14,715 ⟶ 15,031:
Recents.save();
performHacks();
});
|