From cfd7b6568bc7b6978b8301825441373c4540d39b Mon Sep 17 00:00:00 2001 From: Siddharth VP Date: Sat, 23 Nov 2024 03:05:59 +0530 Subject: [PATCH 1/4] rollback: support older fluff preferences (#2059) Continue honoring the confirmOnFluff and confirmOnMobileFluff preferences (renamed in #2056) which are in use by 900+ users. When a user changes any of their Twinkle preferences, these two preferences if in use will get automatically renamed. When this has occurred for most users, support for them can be dropped. --- modules/twinklerollback.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/modules/twinklerollback.js b/modules/twinklerollback.js index 2db2f06c4..33b7fef67 100644 --- a/modules/twinklerollback.js +++ b/modules/twinklerollback.js @@ -673,9 +673,12 @@ Twinkle.rollback.callbacks = { break; } - if ((Twinkle.getPref('confirmOnRollback') || - // Mobile user agent taken from [[en:MediaWiki:Gadget-confirmationRollback-mobile.js]] - (Twinkle.getPref('confirmOnMobileRollback') && /Android|webOS|iPhone|iPad|iPod|BlackBerry|Mobile|Opera Mini/i.test(navigator.userAgent))) && + // Preferences confirmOnRollback and confirmOnMobileRollback were formerly named + // confirmOnFluff and confirmOnMobileFluff, respectively. Check both variations for now. + if ((Twinkle.getPref('confirmOnRollback') || Twinkle.getPref('confirmOnFluff') || + ((Twinkle.getPref('confirmOnMobileRollback') || Twinkle.getPref('confirmOnMobileFluff')) && + // Mobile user agent taken from [[en:MediaWiki:Gadget-confirmationRollback-mobile.js]] + /Android|webOS|iPhone|iPad|iPod|BlackBerry|Mobile|Opera Mini/i.test(navigator.userAgent))) && !userHasAlreadyConfirmedAction && !confirm('Reverting page: are you sure?')) { statelem.error('Aborted by user.'); return; From 61ff53632c825dc75c6f2d4d4a4dd010e5480b69 Mon Sep 17 00:00:00 2001 From: Tollens Date: Fri, 22 Nov 2024 16:06:32 -0700 Subject: [PATCH 2/4] arv: refactor AIV report generation (#2060) * extract aiv report creation to methods * fix lint error * use more concise method name --- modules/twinklearv.js | 130 ++++++++++++++++++++++++------------------ 1 file changed, 74 insertions(+), 56 deletions(-) diff --git a/modules/twinklearv.js b/modules/twinklearv.js index 7a9a147b1..ff8898e1f 100644 --- a/modules/twinklearv.js +++ b/modules/twinklearv.js @@ -473,72 +473,28 @@ Twinkle.arv.callback.changeCategory = function (e) { Twinkle.arv.callback.evaluate = function(e) { var form = e.target; var reason = ''; + var input = Morebits.quickForm.getInputData(form); + var comment = ''; if (form.reason) { comment = form.reason.value; } var uid = form.uid.value; - var types; - switch (form.category.value) { + + switch (input.category) { // Report user for vandalism case 'aiv': /* falls through */ default: - types = form.getChecked('arvtype'); - if (!types.length && comment === '') { + reason = Twinkle.arv.callback.getAivReasonWikitext(input); + + if (reason === null) { alert('You must specify some reason'); return; } - types = types.map(function(v) { - switch (v) { - case 'final': - return 'vandalism after final warning'; - case 'postblock': - return 'vandalism after recent release of block'; - case 'vandalonly': - return 'actions evidently indicate a vandalism-only account'; - case 'promoonly': - return 'account is being used only for promotional purposes'; - case 'spambot': - return 'account is evidently a spambot or a compromised account'; - default: - return 'unknown reason'; - } - }).join('; '); - - if (form.page.value !== '') { - // Allow links to redirects, files, and categories - reason = 'On {{No redirect|:' + form.page.value + '}}'; - if (form.badid.value !== '') { - reason += ' ({{diff|' + form.page.value + '|' + form.badid.value + '|' + form.goodid.value + '|diff}})'; - } - reason += ':'; - } - - if (types) { - reason += ' ' + types; - } - - if (comment !== '') { - var reasonEndsInPunctuationOrBlank = /([.?!;:]|^)$/.test(reason); - reason += reasonEndsInPunctuationOrBlank ? '' : '.'; - var reasonIsBlank = reason === ''; - reason += reasonIsBlank ? '' : ' '; - reason += comment; - } - - reason = reason.trim(); - var reasonEndsInPunctuation = /[.?!;]$/.test(reason); - if (!reasonEndsInPunctuation) { - reason += '.'; - } - - reason += ' ~~~~'; - reason = reason.replace(/\r?\n/g, '\n*:'); // indent newlines - Morebits.simpleWindow.setButtonsEnabled(false); Morebits.status.init(form); @@ -554,7 +510,7 @@ Twinkle.arv.callback.evaluate = function(e) { var $aivLink = 'WP:AIV'; // check if user has already been reported - if (new RegExp('\\{\\{\\s*(?:(?:[Ii][Pp])?[Vv]andal|[Uu]serlinks)\\s*\\|\\s*(?:1=)?\\s*' + Morebits.string.escapeRegExp(uid) + '\\s*\\}\\}').test(text)) { + if (new RegExp('\\{\\{\\s*(?:(?:[Ii][Pp])?[Vv]andal|[Uu]serlinks)\\s*\\|\\s*(?:1=)?\\s*' + Morebits.string.escapeRegExp(input.uid) + '\\s*\\}\\}').test(text)) { aivPage.getStatusElement().error('Report already present, will not add a new one'); Morebits.status.printUserText(reason, 'The comments you typed are provided below, in case you wish to manually post them under the existing report for this user at ' + $aivLink + ':'); return; @@ -566,8 +522,8 @@ Twinkle.arv.callback.evaluate = function(e) { var tb2Text = tb2Page.getPageText(); var tb2statelem = tb2Page.getStatusElement(); - if (new RegExp('\\{\\{\\s*(?:(?:[Ii][Pp])?[Vv]andal|[Uu]serlinks)\\s*\\|\\s*(?:1=)?\\s*' + Morebits.string.escapeRegExp(uid) + '\\s*\\}\\}').test(tb2Text)) { - if (confirm('The user ' + uid + ' has already been reported by a bot. Do you wish to make the report anyway?')) { + if (new RegExp('\\{\\{\\s*(?:(?:[Ii][Pp])?[Vv]andal|[Uu]serlinks)\\s*\\|\\s*(?:1=)?\\s*' + Morebits.string.escapeRegExp(input.uid) + '\\s*\\}\\}').test(tb2Text)) { + if (confirm('The user ' + input.uid + ' has already been reported by a bot. Do you wish to make the report anyway?')) { tb2statelem.info('Proceeded despite bot report'); } else { tb2statelem.error('Report from a bot is already present, stopping'); @@ -579,9 +535,9 @@ Twinkle.arv.callback.evaluate = function(e) { } aivPage.getStatusElement().status('Adding new report...'); - aivPage.setEditSummary('Reporting [[Special:Contributions/' + uid + '|' + uid + ']].'); + aivPage.setEditSummary('Reporting [[Special:Contributions/' + input.uid + '|' + input.uid + ']].'); aivPage.setChangeTags(Twinkle.changeTags); - aivPage.setAppendText('\n*{{vandal|' + (/=/.test(uid) ? '1=' : '') + uid + '}} – ' + reason); + aivPage.setAppendText(Twinkle.arv.callback.buildAivReport(input)); aivPage.append(); }); }); @@ -812,6 +768,68 @@ Twinkle.arv.callback.evaluate = function(e) { } }; +Twinkle.arv.callback.getAivReasonWikitext = function(input) { + var text = ''; + var type = input.arvtype; + + if (!type.length && input.reason === '') { + return null; + } + + type = type.map(function(v) { + switch (v) { + case 'final': + return 'vandalism after final warning'; + case 'postblock': + return 'vandalism after recent release of block'; + case 'vandalonly': + return 'actions evidently indicate a vandalism-only account'; + case 'promoonly': + return 'account is being used only for promotional purposes'; + case 'spambot': + return 'account is evidently a spambot or a compromised account'; + default: + return 'unknown reason'; + } + }).join('; '); + + if (input.page !== '') { + // Allow links to redirects, files, and categories + text = 'On {{No redirect|:' + input.page + '}}'; + if (input.badid !== '') { + text += ' ({{diff|' + input.page + '|' + input.badid + '|' + input.goodid + '|diff}})'; + } + text += ':'; + } + + if (type) { + text += ' ' + type; + } + + if (input.reason !== '') { + var textEndsInPunctuationOrBlank = /([.?!;:]|^)$/.test(text); + text += textEndsInPunctuationOrBlank ? '' : '.'; + var textIsBlank = text === ''; + text += textIsBlank ? '' : ' '; + text += input.reason; + } + + text = text.trim(); + var textEndsInPunctuation = /[.?!;]$/.test(text); + if (!textEndsInPunctuation) { + text += '.'; + } + + text += ' ~~~~'; + text = text.replace(/\r?\n/g, '\n*:'); // indent newlines + + return text; +}; + +Twinkle.arv.callback.buildAivReport = function(input) { + return '\n*{{vandal|' + (/=/.test(input.uid) ? '1=' : '') + input.uid + '}} – ' + Twinkle.arv.callback.getAivReasonWikitext(input); +}; + Twinkle.arv.processSock = function(params) { Morebits.wiki.addCheckpoint(); // prevent notification events from causing an erronous "action completed" From de88face05f0832629eafd768752e92a048b0746 Mon Sep 17 00:00:00 2001 From: NovemLinguae <79697282+NovemLinguae@users.noreply.github.com> Date: Sat, 23 Nov 2024 08:05:43 -0800 Subject: [PATCH 3/4] config: don't ignore old fluff preferences (#2061) * rollback: don't delete user preferences - fixes an issue where if a user still had the old confirmOnFluff or confirmOnRollback preferences, and visited the preferences page and saved, their preferences were ignored and silently deleted - refactor complicated conditional in twinklerollback.js module - reverts #2059 because we accomplish the same thing by putting the code in twinkle.js * un-extract variable * fix wrong variable bug Co-authored-by: Siddharth VP * fix semicolon bug --------- Co-authored-by: Siddharth VP --- modules/twinklerollback.js | 19 ++++++++++++------- twinkle.js | 9 +++++++++ 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/modules/twinklerollback.js b/modules/twinklerollback.js index 33b7fef67..ef7ce0530 100644 --- a/modules/twinklerollback.js +++ b/modules/twinklerollback.js @@ -673,13 +673,18 @@ Twinkle.rollback.callbacks = { break; } - // Preferences confirmOnRollback and confirmOnMobileRollback were formerly named - // confirmOnFluff and confirmOnMobileFluff, respectively. Check both variations for now. - if ((Twinkle.getPref('confirmOnRollback') || Twinkle.getPref('confirmOnFluff') || - ((Twinkle.getPref('confirmOnMobileRollback') || Twinkle.getPref('confirmOnMobileFluff')) && - // Mobile user agent taken from [[en:MediaWiki:Gadget-confirmationRollback-mobile.js]] - /Android|webOS|iPhone|iPad|iPod|BlackBerry|Mobile|Opera Mini/i.test(navigator.userAgent))) && - !userHasAlreadyConfirmedAction && !confirm('Reverting page: are you sure?')) { + const needToDisplayConfirmation = + ( + Twinkle.getPref('confirmOnRollback') || + ( + Twinkle.getPref('confirmOnMobileRollback') && + // Mobile user agent taken from [[en:MediaWiki:Gadget-confirmationRollback-mobile.js]] + /Android|webOS|iPhone|iPad|iPod|BlackBerry|Mobile|Opera Mini/i.test(navigator.userAgent) + ) + ) && + !userHasAlreadyConfirmedAction; + + if (needToDisplayConfirmation && !confirm('Reverting page: are you sure?')) { statelem.error('Aborted by user.'); return; } diff --git a/twinkle.js b/twinkle.js index ed0363b6b..47e65cd73 100644 --- a/twinkle.js +++ b/twinkle.js @@ -179,6 +179,7 @@ Twinkle.getPref = function twinkleGetPref(name) { if (typeof Twinkle.prefs === 'object' && Twinkle.prefs[name] !== undefined) { return Twinkle.prefs[name]; } + // Old preferences format, used before twinkleoptions.js was a thing if (typeof window.TwinkleConfig === 'object' && window.TwinkleConfig[name] !== undefined) { return window.TwinkleConfig[name]; @@ -186,6 +187,14 @@ Twinkle.getPref = function twinkleGetPref(name) { if (typeof window.FriendlyConfig === 'object' && window.FriendlyConfig[name] !== undefined) { return window.FriendlyConfig[name]; } + + // Backwards compatibility code because we renamed confirmOnFluff to confirmOnRollback, and confirmOnMobileFluff to confirmOnMobileRollback + if (name === 'confirmOnRollback' && Twinkle.prefs.confirmOnFluff !== undefined) { + return Twinkle.prefs.confirmOnFluff; + } else if (name === 'confirmOnMobileRollback' && Twinkle.prefs.confirmOnMobileFluff !== undefined) { + return Twinkle.prefs.confirmOnMobileFluff; + } + return Twinkle.defaultConfig[name]; }; From b3c4e3f13a19aca32760bbcd84b18a3afb7b2b7c Mon Sep 17 00:00:00 2001 From: Tollens Date: Sat, 23 Nov 2024 09:15:05 -0700 Subject: [PATCH 4/4] refactor UAA report generation logic (#2062) --- modules/twinklearv.js | 73 +++++++++++++++++++++---------------------- 1 file changed, 36 insertions(+), 37 deletions(-) diff --git a/modules/twinklearv.js b/modules/twinklearv.js index ff8898e1f..ea9a56b42 100644 --- a/modules/twinklearv.js +++ b/modules/twinklearv.js @@ -475,12 +475,7 @@ Twinkle.arv.callback.evaluate = function(e) { var reason = ''; var input = Morebits.quickForm.getInputData(form); - var comment = ''; - if (form.reason) { - comment = form.reason.value; - } var uid = form.uid.value; - var types; switch (input.category) { @@ -545,37 +540,9 @@ Twinkle.arv.callback.evaluate = function(e) { // Report inappropriate username case 'username': - types = form.getChecked('arvtype').map(Morebits.string.toLowerCaseFirstChar); - var censorUsername = types.includes('offensive'); // check if the username is marked offensive - - // generate human-readable string, e.g. "misleading and promotional username" - if (types.length <= 2) { - types = types.join(' and '); - } else { - types = [ types.slice(0, -1).join(', '), types.slice(-1) ].join(' and '); - } - - // a or an? - var adjective = 'a'; - if (/[aeiouwyh]/.test(types[0] || '')) { // non 100% correct, but whatever, including 'h' for Cockney - adjective = 'an'; - } + var censorUsername = input.arvtype.includes('offensive'); // check if the username is marked offensive - // generate wikicode to place on [[WP:UAA]] page - reason = '*{{user-uaa|1=' + uid + '}} – '; - if (types.length) { - reason += 'Violation of the username policy as ' + adjective + ' ' + types + ' username. '; - } - if (comment !== '') { - reason += Morebits.string.toUpperCaseFirstChar(comment); - var endsInPeriod = /\.$/.test(comment); - if (!endsInPeriod) { - reason += '.'; - } - reason += ' '; - } - reason += '~~~~'; - reason = reason.replace(/\r?\n/g, '\n*:'); // indent newlines + reason = Twinkle.arv.callback.getUsernameReportWikitext(input); Morebits.simpleWindow.setButtonsEnabled(false); Morebits.status.init(form); @@ -590,14 +557,14 @@ Twinkle.arv.callback.evaluate = function(e) { var text = uaaPage.getPageText(); // check if user has already been reported - if (new RegExp('\\{\\{\\s*user-uaa\\s*\\|\\s*(1\\s*=\\s*)?' + Morebits.string.escapeRegExp(uid) + '\\s*(\\||\\})').test(text)) { + if (new RegExp('\\{\\{\\s*user-uaa\\s*\\|\\s*(1\\s*=\\s*)?' + Morebits.string.escapeRegExp(input.uid) + '\\s*(\\||\\})').test(text)) { uaaPage.getStatusElement().error('User is already listed.'); var $uaaLink = 'WP:UAA'; Morebits.status.printUserText(reason, 'The comments you typed are provided below, in case you wish to manually post them under the existing report for this user at ' + $uaaLink + ':'); return; } uaaPage.getStatusElement().status('Adding new report...'); - uaaPage.setEditSummary('Reporting ' + (censorUsername ? 'an offensive username.' : '[[Special:Contributions/' + uid + '|' + uid + ']].')); + uaaPage.setEditSummary('Reporting ' + (censorUsername ? 'an offensive username.' : '[[Special:Contributions/' + input.uid + '|' + input.uid + ']].')); uaaPage.setChangeTags(Twinkle.changeTags); // Blank newline per [[Special:Permalink/996949310#Spacing]]; see also [[WP:LISTGAP]] and [[WP:INDENTGAP]] @@ -830,6 +797,38 @@ Twinkle.arv.callback.buildAivReport = function(input) { return '\n*{{vandal|' + (/=/.test(input.uid) ? '1=' : '') + input.uid + '}} – ' + Twinkle.arv.callback.getAivReasonWikitext(input); }; +Twinkle.arv.callback.getUsernameReportWikitext = function(input) { + // generate human-readable string, e.g. "misleading and promotional username" + if (input.arvtype.length <= 2) { + input.arvtype = input.arvtype.join(' and '); + } else { + input.arvtype = [ input.arvtype.slice(0, -1).join(', '), input.arvtype.slice(-1) ].join(' and '); + } + + // a or an? + var adjective = 'a'; + if (/[aeiouwyh]/.test(input.arvtype[0] || '')) { // non 100% correct, but whatever, including 'h' for Cockney + adjective = 'an'; + } + + var text = '*{{user-uaa|1=' + input.uid + '}} – '; + if (input.arvtype.length) { + text += 'Violation of the username policy as ' + adjective + ' ' + input.arvtype + ' username. '; + } + if (input.reason !== '') { + text += Morebits.string.toUpperCaseFirstChar(input.reason); + var endsInPeriod = /\.$/.test(input.reason); + if (!endsInPeriod) { + text += '.'; + } + text += ' '; + } + text += '~~~~'; + text = text.replace(/\r?\n/g, '\n*:'); // indent newlines + + return text; +}; + Twinkle.arv.processSock = function(params) { Morebits.wiki.addCheckpoint(); // prevent notification events from causing an erronous "action completed"