「利用者:Dragoniez/scripts/AN Reporter.js」の版間の差分
表示
削除された内容 追加された内容
3.3.3RR: +LTA:AINO |
v4.0: ブロック状態確認機能、重複報告確認機能、ほか変更・追加点多数 |
||
2行目: | 2行目: | ||
* AN Reporter (ANR) |
* AN Reporter (ANR) |
||
* Author: Dragoniez |
* Author: Dragoniez |
||
* Version: |
* Version: 4.0 |
||
*************************************/ |
*************************************/ |
||
//<nowiki> |
//<nowiki> |
||
14行目: | 14行目: | ||
// Load CSS source for Select2 |
// Load CSS source for Select2 |
||
$('head').append('<link rel |
$('head') |
||
.append($('<link />') |
|||
.attr({ |
|||
'rel': 'stylesheet', |
|||
'href': 'https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css' |
|||
}) |
|||
); |
|||
// Run the script only if the user is autoconfirmed and the page is not an edit page |
// Run the script only if the user is autoconfirmed and the page is not an edit page |
||
if (userIsInGroup('autoconfirmed') && mw.config.get('wgAction') !== 'edit') { |
if (userIsInGroup('autoconfirmed') && mw.config.get('wgAction') !== 'edit') { |
||
// ********** UTILITY FUNCTIONS ********** |
|||
/** |
|||
* String method to get rid of the U+200E space, in addition to the function of $.trim() |
|||
* @returns {String} |
|||
*/ |
|||
String.prototype.trim2 = function() { |
|||
return this.replace(/\u200e/g, '').trim(); |
|||
}; |
|||
/** |
|||
* String method (alternative) to replace all occurences of a string with another |
|||
* (takes a replacer and a replacee as arguments) |
|||
* @returns {String} |
|||
*/ |
|||
String.prototype.replaceAll2 = function() { |
|||
let replaced = ''; |
|||
if (arguments.length %2 !== 0) { |
|||
return new Error('SyntaxError: replaceAll2 takes an even number of arguments.'); |
|||
} else { |
|||
for (let i = 0; i < arguments.length; i = i + 2) { |
|||
if (i === 0) { |
|||
replaced = this.split(arguments[i]).join(arguments[i + 1]); |
|||
} else { |
|||
replaced = replaced.split(arguments[i]).join(arguments[i + 1]); |
|||
} |
|||
} |
|||
return replaced; |
|||
} |
|||
}; |
|||
// Define the position of the '報告' button |
// Define the position of the '報告' button |
||
53行目: | 91行目: | ||
// Default CSS for Select2 |
// Default CSS for Select2 |
||
$('head').append( |
$('head').append( |
||
`<style> |
|||
.select2-selection__rendered { |
|||
padding: 1px 2px; |
|||
font-size: 1em; |
|||
line-height: normal !important; |
|||
} |
|||
.select2-results__option, .select2-results__group { |
|||
padding: 1px 8px; |
|||
font-size: ${s2fSize}; |
|||
margin: 0; |
|||
} |
|||
.select2-container, .select2-selection--single { |
|||
'</style>' |
|||
height: auto !important; |
|||
} |
|||
</style>` |
|||
); |
); |
||
// Variables |
|||
let Logids = {}; // Object to store usernames and their corresponding logids |
|||
let checkBlockStatusBeforeEdit = true; |
|||
let checkDuplicateReportsBeforeEdit = true; |
|||
// Add ANR tab |
// Add ANR tab |
||
$(mw.util.addPortletLink(btnPosition, '#', '報告', 'ca-anr', '管理者伝言板に利用者を報告', null, '#ca-move')) |
$(mw.util.addPortletLink(btnPosition, '#', '報告', 'ca-anr', '管理者伝言板に利用者を報告', null, '#ca-move')) // '報告β' when debugging |
||
.click(function(e){ |
.click(function(e){ |
||
77行目: | 123行目: | ||
// CSS |
// CSS |
||
const labelCSS = |
|||
'display: inline-block;' + |
|||
'width: 8ch;' ; |
|||
let siCSS = 'border: 1px solid #d3d3d3; border-radius: 1%; background: white; padding: 2px 4px;'; // For select and input |
|||
const marginCSS = |
|||
let btnCSS = 'color: black; font-weight: normal; border: 1px solid #d3d3d3; background: white; padding: 0.2em 0.5em; border-radius: 10%;'; // For button |
|||
'margin: 1em 0;' ; |
|||
const siCSS = // For select and input |
|||
'border: 1px solid #d3d3d3;' + |
|||
'border-radius: 1%;' + |
|||
'background-color: white;' + |
|||
'padding: 2px 4px;' ; |
|||
const btnCSS = // For button |
|||
'color: black;' + |
|||
'font-weight: normal;' + |
|||
'border: 1px solid #d3d3d3;' + |
|||
'background-color: white;' + |
|||
'padding: 0.2em 0.5em;' + |
|||
'border-radius: 10%;' ; |
|||
/* Experimental design |
|||
const lsCSS = // Eliminate gap between inline-blocks |
|||
'letter-spacing: -1em;' + |
|||
'white-space: nowrap' ; |
|||
const rlsCSS = // Reset line-spacing in child elements |
|||
'letter-spacing: normal' ; |
|||
*/ |
|||
// Page names |
// Page names |
||
98行目: | 164行目: | ||
` <select id="anr-section-options-i" style="${siCSS}">` + |
` <select id="anr-section-options-i" style="${siCSS}">` + |
||
` <option selected disabled hidden>選択してください</option>` + |
` <option selected disabled hidden>選択してください</option>` + |
||
` <option> |
` <option id="anr-section-options-i-date"></option>` + |
||
` <option>不適切な利用者名</option>` + |
` <option>不適切な利用者名</option>` + |
||
` <option>公開アカウント</option>` + |
` <option>公開アカウント</option>` + |
||
127行目: | 193行目: | ||
` <option>Asaklira系(ASA)</option>` + |
` <option>Asaklira系(ASA)</option>` + |
||
` <option>麻原英太系 (ASACOV)</option>` + |
` <option>麻原英太系 (ASACOV)</option>` + |
||
` <option>アジアンビ系 (ASIANB)</option>` + |
` <option>アジアンビ系 (ASIANB)</option>` + |
||
` <option>Bulut系 (Asperger、ASPE)</option>` + |
` <option>Bulut系 (Asperger、ASPE)</option>` + |
||
` <option>백돌系(BAEG)</option>` + |
` <option>백돌系(BAEG)</option>` + |
||
139行目: | 205行目: | ||
` <option>Ellsiemall系 (ELLS)</option>` + |
` <option>Ellsiemall系 (ELLS)</option>` + |
||
` <option>イギリス可変IP系(ENS)</option>` + |
` <option>イギリス可変IP系(ENS)</option>` + |
||
` <option>放送局・送信所の記事で荒らしを行うIP(EOHS)</option>` + |
` <option>放送局・送信所の記事で荒らしを行うIP(EOHS)</option>` + |
||
` <option>EricNeedles3系 (ERIC3)</option>` + |
` <option>EricNeedles3系 (ERIC3)</option>` + |
||
` <option>IUCNレッドリスト関連荒らし系(FRL)</option>` + |
` <option>IUCNレッドリスト関連荒らし系(FRL)</option>` + |
||
178行目: | 244行目: | ||
` <option>猛烈な勢いで赤リンクを無差別除去するアカウント群系(MOUAKA)</option>` + |
` <option>猛烈な勢いで赤リンクを無差別除去するアカウント群系(MOUAKA)</option>` + |
||
` <option>MShared系 (MShared)</option>` + |
` <option>MShared系 (MShared)</option>` + |
||
` <option>名取の納豆系(NATO)</option>` + |
` <option>名取の納豆系(NATO)</option>` + |
||
` <option>Die ndbtk系 (NDBTK)</option>` + |
` <option>Die ndbtk系 (NDBTK)</option>` + |
||
` <option>NoSaito・みそかつおにんにく系 (NMT)</option>` + |
` <option>NoSaito・みそかつおにんにく系 (NMT)</option>` + |
||
215行目: | 281行目: | ||
` </select>`; |
` </select>`; |
||
//</div> |
//</div> |
||
// Username input |
|||
let userHtml = |
|||
//<div class="anr-user-div"> |
|||
` <div id="anr-user1-input-div">` + |
|||
` <label for="anr-user1-input" style="${labelCSS}">利用者</label>` + |
|||
` <input id="anr-user1-input" style="width: 34ch; ${siCSS}">` + |
|||
` <select disabled id="anr-user1-select" style="${siCSS}">` + |
|||
` <option class="anr-opt-UNL">UNL</option>` + |
|||
` <option class="anr-opt-User2">User2</option>` + |
|||
` <option class="anr-opt-IP2">IP2</option>` + |
|||
` <option class="anr-opt-logid">logid</option>` + |
|||
` <option class="anr-opt-diff">diff</option>` + |
|||
` <option selected class="anr-opt-none">none</option>` + |
|||
` </select>` + |
|||
` </div>` + |
|||
` <div id="anr-user1-checkbox-div" style="display: none;">` + |
|||
` <label class="anr-emptylabel" style="${labelCSS}"></label>` + |
|||
` <input type="checkbox" id="anr-user1-checkbox">` + |
|||
` <label for="anr-user1-checkbox">利用者名を隠す</label>` + |
|||
` </div>` + |
|||
` <div id="anr-user1-idlink-div" style="display: none;">` + |
|||
` <label for="anr-user1-idlink" style="${labelCSS}"></label>` + |
|||
` <a id="anr-user1-idlink" href="" target="_blank"></a>` + |
|||
` </div>` + |
|||
` <div id="anr-user1-blockstatus-div" style="display: none;">` + |
|||
` <label for="anr-user1-blockstatus" style="${labelCSS}"></label>` + |
|||
` <a id="anr-user1-blockstatus" href="" target="_blank" style="color: MediumVioletRed;">ブロックあり</a>` + |
|||
` </div>`; |
|||
//</div> |
|||
// The whole html contour |
|||
let modalHtml = |
|||
`<div class="anr-modal-dialog" title="AN Reporter" style="max-height: 80vh; min-width: 54ch;">` + |
|||
` <div class="anr-modal-header">` + |
|||
` <h2>利用者を報告</h2>` + |
|||
` </div>` + |
|||
` <div class="anr-modal-body">` + |
|||
` <form>` + |
|||
` <div class="anr-target-div" style="${marginCSS}">` + |
|||
` <label for="anr-target-options" style="${labelCSS}">報告先</label>` + |
|||
` <select id="anr-target-options" style="${siCSS}">` + |
|||
` <option selected disabled hidden>選択してください</option>` + |
|||
` <option>${ANI}</option>` + |
|||
` <option>${ANS}</option>` + |
|||
` <option>${AN3RR}</option>` + |
|||
` </select>` + |
|||
` <div class="anr-target-pagelink-div" style="display: none;">` + |
|||
` <label class="anr-emptylabel" for="anr-target-pagelink" style="${labelCSS}"></label>` + |
|||
` <a id="anr-target-pagelink" href="" target="_blank">報告先を確認</a>` + |
|||
` </div>` + |
|||
` </div>` + |
|||
` <div class="anr-section-div" style="${marginCSS} display: none;">` + |
|||
// sectionsX + |
|||
` </div>` + |
|||
` <div class="anr-user-div" style="${marginCSS}">` + |
|||
userHtml + |
|||
` <div class="anr-btn-div">` + |
|||
` <button type="button" class="anr-addBtn" style="${btnCSS}">追加</button>` + |
|||
` </div>` + |
|||
` </div>` + |
|||
` <div class="anr-reason-div" style="${marginCSS}">` + |
|||
` <label for="anr-reason-text" style="${labelCSS}">理由</label>` + |
|||
` <textarea id="anr-reason-text" rows="8" style="width: 100%"></textarea>` + |
|||
` </div>` + |
|||
` <div class="anr-summary-div" style="${marginCSS}">` + |
|||
` <input id="anr-summary-checkbox" type="checkbox">` + |
|||
` <label for="anr-summary-checkbox">要約を指定</label>` + |
|||
` <textarea id="anr-summary-text" rows="3" style="width: 100%; display: none;"></textarea>` + |
|||
` </div>` + |
|||
` <div class="anr-checkbox-div" style="${marginCSS}">` + |
|||
` <input checked id="anr-blockstatus-checkbox" type="checkbox">` + |
|||
` <label for="anr-blockstatus-checkbox">報告前にブロック状態をチェック</label>` + |
|||
` <br>` + |
|||
` <input checked id="anr-duplicatereport-checkbox" type="checkbox">` + |
|||
` <label for="anr-duplicatereport-checkbox">報告前に重複報告をチェック</label>` + |
|||
` </div>` + |
|||
` </form>` + |
|||
` </div>` + |
|||
`</div>`; |
|||
// Add the frame div to the page |
|||
$('body').append(modalHtml); |
|||
// Script ad for edit summary |
|||
const scriptAd = ' ([[User:Dragoniez/AN Reporter|AN Reporter]])'; |
|||
//const scriptAd = ' ([[User:Dragoniez/AN Reporter|AN Reporter Experimental]])'; // For debugging |
|||
// Show dialog |
|||
$('.anr-modal-dialog').dialog({ |
|||
'resizable': false, |
|||
'height': 'auto', |
|||
'width': 'auto', |
|||
'modal': true, |
|||
'position': { my: 'center', at: 'top', of: window }, |
|||
'open': function(){ |
|||
// CSS |
|||
dialogCSS(); |
|||
// Show VIP list |
|||
VIPList(); |
|||
// Initialize variables |
|||
checkBlockStatusBeforeEdit = true; |
|||
checkDuplicateReportsBeforeEdit = true; |
|||
/* NOTE: The value of Logids is inherited from before reopening the dialog. |
|||
* If any bug is reported, consider resetting the variable here as well. */ |
|||
// Get the name of the user to report if it can be retrieved from the page |
|||
let username = mw.config.get('wgRelevantUserName'); // Note: This does not pick up IP ranges |
|||
// Workaround to pick up IP ranges |
|||
if (username === null && mw.config.get('wgCanonicalSpecialPageName') === 'Contributions') { |
|||
let relUsername = $('#firstHeading').text().replace('の投稿記録', ''); |
|||
if (mw.util.isIPAddress(relUsername, true)) { |
|||
username = relUsername; |
|||
} |
|||
} |
|||
// Exit function if the current user is on his/her own page or username has remained undefined or null |
|||
if (!username || username === mw.config.get('wgUserName')) { |
|||
return; |
|||
} |
|||
/* Initialize the username input and type dropdown |
|||
* Note: The function below is partially a duplicate of updateTypeDropdown(), but deliberately coded that way |
|||
* to prevent bugs and eliminate unnecessary API requests */ |
|||
async function initializeTypeDropdown() { |
|||
let inputID = '#anr-user1-input'; |
|||
let selectID = '#anr-user1-select'; |
|||
let checkboxDivID = '#anr-user1-checkbox-div'; |
|||
$(inputID).val(username); // Fill the input with the username |
|||
$(selectID).prop('disabled', false); // enable dropdown |
|||
if (mw.util.isIPAddress(username, true)) { // if IP |
|||
$(selectID).children('.anr-opt-UNL').prop('hidden', true); |
|||
$(selectID).children('.anr-opt-User2').prop('hidden', true); |
|||
$(selectID).children('.anr-opt-IP2').prop({'hidden': false, 'selected': true}); |
|||
$(selectID).children('.anr-opt-logid').prop('hidden', true); |
|||
$(selectID).children('.anr-opt-diff').prop('hidden', true); |
|||
$(selectID).children('.anr-opt-none').prop('hidden', false); |
|||
$(checkboxDivID).css('display', 'none'); // hide 'hide username' checkbox |
|||
await toggleBlockStatusLink(inputID, false, false); |
|||
} else { // if user |
|||
$(selectID).children('.anr-opt-UNL').prop({'hidden': false, 'selected': true}); |
|||
$(selectID).children('.anr-opt-User2').prop('hidden', false); |
|||
$(selectID).children('.anr-opt-IP2').prop('hidden', true); |
|||
$(selectID).children('.anr-opt-logid').prop('hidden', true); |
|||
$(selectID).children('.anr-opt-diff').prop('hidden', true); |
|||
$(selectID).children('.anr-opt-none').prop('hidden', false); |
|||
$(checkboxDivID).css('display', 'block'); // show 'hide username' checkbox |
|||
await toggleBlockStatusLink(inputID, false, false); |
|||
} |
|||
} |
|||
initializeTypeDropdown(); |
|||
}, |
|||
'buttons': [{ |
|||
'text': 'プレビュー', |
|||
'click': previewBtn |
|||
}, { |
|||
'text': '報告', |
|||
'click': reportBtn |
|||
}] |
|||
}); |
|||
// ********** EVENT HANDLERS AND FUNCTIONS ********** |
|||
// Function to change the CSS of the dialog |
|||
function dialogCSS() { |
|||
$('.ui-dialog-content, .ui-dialog-buttonpane, .ui-corner-all, .ui-draggable, .ui-resizable').css('background', '#FFF0E4'); |
|||
$('.ui-button').css({ |
|||
'color': 'black', |
|||
'background-color': 'white' |
|||
}); |
|||
$('.ui-dialog-titlebar, .ui-dialog-titlebar-close').attr('style', 'background: #FEC493 !important;'); |
|||
$('.ui-dialog').css('font-size', fSize); |
|||
} |
|||
// WP:VIP list (for copy to clipboard) |
// WP:VIP list (for copy to clipboard) |
||
async function VIPList() { |
|||
return new Promise(function(resolve, reject) { |
return new Promise(function(resolve, reject) { |
||
new mw.Api().get({ |
new mw.Api().get({ |
||
223行目: | 473行目: | ||
'page': 'Wikipedia:進行中の荒らし行為', |
'page': 'Wikipedia:進行中の荒らし行為', |
||
'prop': 'sections', |
'prop': 'sections', |
||
'formatversion': 2 |
'formatversion': '2' |
||
}).then(function(res){ |
}).then(function(res){ |
||
229行目: | 479行目: | ||
// Get VIP's names |
// Get VIP's names |
||
const sectionInfo = res.parse.sections; |
|||
const excludeList = [ |
|||
'記述について', |
|||
'注意と選択', '警告の方法', '未登録(匿名・IP)ユーザーの場合', '登録済み(ログイン)ユーザーの場合', |
|||
'急を要する二段階', |
|||
'配列', |
|||
'ブロック等の手段', |
|||
'このページに利用者名を加える', |
|||
'注意と選択', |
|||
'警告の方法', |
|||
'未登録(匿名・IP)ユーザーの場合', |
|||
'登録済み(ログイン)ユーザーの場合', |
|||
'警告中', |
|||
'関連項目' |
|||
]; |
|||
let vipList = []; |
|||
for (let i = 0; i < sectionInfo.length; i++) { |
|||
if (!isInArray(sectionInfo[i].line, excludeList) && sectionInfo[i].level === '3') { |
|||
vipList.push(`<option>${sectionInfo[i].line}</option>`); |
|||
} |
} |
||
} |
} |
||
// Show the VIP list on the dialog |
// Show the VIP list on the dialog |
||
const VIPListHtml = |
|||
'<div class="anr-viplist-div" style="width: 100%;">' + |
'<div class="anr-viplist-div" style="width: 100%;">' + |
||
' <select id="anr-viplist-select">' + |
' <select id="anr-viplist-select">' + |
||
' <option selected disabled hidden>[[Wikipedia:進行中の荒らし行為]]を検索</option>' + |
' <option selected disabled hidden>[[Wikipedia:進行中の荒らし行為]]を検索</option>' + |
||
vipList.join() + |
|||
' </select>' + |
' </select>' + |
||
` <button type="button" class="anr-viplist-btn" style="${btnCSS}">コピー</button>` + |
` <button type="button" class="anr-viplist-btn" style="${btnCSS}">コピー</button>` + |
||
263行目: | 523行目: | ||
} |
} |
||
// |
// Copy a VIP name when the 'copy' button is hit |
||
$(document).off('click', '.anr-viplist-btn').on('click', '.anr-viplist-btn', function() { |
|||
let userHtml = |
|||
const vipSelectVal = $('#anr-viplist-select').find('option').filter(':selected').text().trim(); |
|||
// <div class="anr-user-div"> |
|||
const vipLink = '[[WP:VIP#' + vipSelectVal + ']]'; |
|||
if (vipSelectVal !== '[[Wikipedia:進行中の荒らし行為]]を検索') { |
|||
` <label id="anr-user1-label" for="anr-user1-input" style="${labelCSS}">利用者</label>` + |
|||
copyToClipboard(vipLink); |
|||
} else { |
|||
alert('ドロップダウンの値が選択されていません'); |
|||
} |
|||
}); |
|||
` <option class="anr-opt-IP2">IP2</option>` + |
|||
` <option class="anr-opt-logid">logid</option>` + |
|||
` <option class="anr-opt-diff">diff</option>` + |
|||
` <option selected class="anr-opt-none">none</option>` + |
|||
` </select>` + |
|||
` </div>` + |
|||
` <div id="anr-user1-checkbox-div" style="display: none;">` + |
|||
` <label class="anr-emptylabel" style="${labelCSS}"></label>` + |
|||
` <input type="checkbox" id="anr-user1-checkbox">` + |
|||
` <label id="anr-user1-checkbox-hide" for="anr-user1-checkbox">利用者名を隠す</label>` + |
|||
` </div>` + |
|||
` <div id="anr-user1-a-div" style="display: none;">` + |
|||
` <label id="anr-user1-label" for="anr-user1-a" style="${labelCSS}"></label>` + |
|||
` <a id="anr-user1-a" href="" target="_blank"></a>` + |
|||
` </div>`; |
|||
// </div> |
|||
// |
// Function to copy a string to the clipboard |
||
function copyToClipboard(str) { |
|||
// |
try { // This should handle browser incompatibility |
||
let $temp = $('<input>'); |
|||
$('body').append($temp); // Create a temporarily hidden text field |
|||
$temp.val(str).select(); // Copy the text string into the field and select the text |
|||
document.execCommand('copy'); // Copy it to the clipboard |
|||
$temp.remove(); // Remove the text field |
|||
} |
|||
catch (err) { |
|||
` <label for="anr-target-options" style="${labelCSS}">報告先</label>` + |
|||
alert('ご利用のブラウザはこの機能に対応していません'); |
|||
console.log(err); |
|||
` <option selected disabled hidden>選択してください</option>` + |
|||
} |
|||
} |
|||
` <option>${AN3RR}</option>` + |
|||
` </select>` + |
|||
` <div class="anr-target-a-div" style="display: none;">` + |
|||
` <label class="anr-emptylabel" for="anr-target-a" style="${labelCSS}"></label>` + |
|||
` <a id="anr-target-a" href="" target="_blank">報告先を確認</a>` + |
|||
` </div>` + |
|||
` </div>` + |
|||
` <div class="anr-section-div" style="${marginCSS} display: none;">` + |
|||
// sectionsX + |
|||
` </div>` + |
|||
` <div class="anr-user-div" style="${marginCSS}">` + |
|||
userHtml + |
|||
` <div class="anr-btn-div">` + |
|||
` <button type="button" class="anr-addBtn" style="${btnCSS}">追加</button>` + |
|||
` </div>` + |
|||
` </div>` + |
|||
` <div class="anr-reason-div" style="${marginCSS}">` + |
|||
` <label for="anr-reason-text" style="${labelCSS}">理由</label>` + |
|||
` <textarea id="anr-reason-text" rows="8" style="width: 100%"></textarea>` + |
|||
` </div>` + |
|||
` <div class="anr-summary-div" style="${marginCSS}">` + |
|||
` <input id="anr-summary-checkbox" type="checkbox">` + |
|||
` <label for="anr-summary-checkbox">要約を指定</label>` + |
|||
` <textarea id="anr-summary-text" rows="3" style="width: 100%; display: none;"></textarea>` + |
|||
` </div>` + |
|||
` </form>` + |
|||
` </div>`; |
|||
// </div>` |
|||
// |
// Function to check information typed into the form |
||
function editPrep() { |
|||
$('body').append('<div class="anr-modal-dialog" title="AN Reporter" style="max-height: 80vh;"/>'); |
|||
let rqFieldsEmpty = false; |
|||
// Check if at least one username is given and get users to report (and their UserAN types) |
|||
// Create html elements inside the div |
|||
let users = []; |
|||
let types = []; |
|||
let duplicates = []; |
|||
let usernameInInput = ''; |
|||
let selectedType = ''; |
|||
let tempUsername = ''; // An escape hatch for username (for t=logid) |
|||
for (let i = 1; i < Infinity; i++) { // Loop through all inputs |
|||
const scriptAd = ' ([[User:Dragoniez/AN Reporter|AN Reporter]])'; |
|||
//const scriptAd = ' ([[User:Dragoniez/AN Reporter|AN Reporter Experimental]])'; // For debugging |
|||
if ($(`#anr-user${i}-input`).length === 0) { // if selector is not found |
|||
// Show dialog |
|||
$('.anr-modal-dialog').dialog({ |
|||
'minHeight': 50, |
|||
'minWidth': '58ch', |
|||
'width': 'auto', |
|||
'modal': true, |
|||
'position': { my: 'center', at: 'top+20%', of: window }, |
|||
'open': function(){ |
|||
// |
break; // exit for |
||
dialogCSS(); |
|||
// |
} else { // if selector is found |
||
VIPList(); |
|||
usernameInInput = $(`#anr-user${i}-input`).val().trim2(); |
|||
selectedType = $(`#anr-user${i}-select`).children('option').filter(':selected').text(); |
|||
let username = mw.config.get('wgRelevantUserName'); // Note: This does not pick up IP ranges |
|||
if (usernameInInput !== '') { // if input is not empty |
|||
// If t=logid, necessary to check if Logids has a username corresponding to the logid |
|||
if (selectedType === 'logid' && Logids[usernameInInput] !== undefined) { |
|||
// If the object has a username/logid pair, get username from the logid |
|||
tempUsername = Logids[usernameInInput]; |
|||
// If either of the username or the logid is already in the array 'users', |
|||
// and if they have yet to be listed as duplicates |
|||
if ((isInArray(usernameInInput, users) || isInArray(tempUsername, users)) && |
|||
!isInArray(usernameInInput, duplicates) && !isInArray(tempUsername, duplicates)) { |
|||
// List both the username and the logid as duplicates |
|||
duplicates.push(usernameInInput, tempUsername); |
|||
} |
|||
} else { // If logids are irrelevant |
|||
// If the username is already in the array 'users' and hasn't been listed as a duplicate |
|||
if (isInArray(usernameInInput, users) && !isInArray(usernameInInput, duplicates)) { |
|||
// List the username as a duplicate |
|||
duplicates.push(usernameInInput); |
|||
} |
|||
} |
|||
users.push(usernameInInput); // Push the username into the array |
|||
types.push(selectedType); // Push the UserAN type into the array |
|||
// Workaround to pick up IP ranges |
|||
if (username === null && mw.config.get('wgCanonicalSpecialPageName') === 'Contributions') { |
|||
let relUsername = $('#firstHeading').text().replace('の投稿記録', ''); |
|||
if (mw.util.isIPAddress(relUsername, true)) { |
|||
username = relUsername; |
|||
} |
} |
||
} |
} |
||
// Reset variables |
|||
tempUsername = ''; |
|||
} |
|||
// Get the name of the section to edit |
|||
let pageToEdit = $('#anr-target-options').children('option').filter(':selected').text(); |
|||
let sectionToEdit = '選択してください'; |
|||
const ANSOptSelected = $('#anr-section-options-s').find('option').filter(':selected').text(); |
|||
let reportToANS = false; |
|||
if (pageToEdit === ANI) { // If WP:AN/I is selected as the target page to edit |
|||
sectionToEdit = $('#anr-section-options-i').children('option').filter(':selected').text(); |
|||
// Update the target section for cases in which the date has changed since the date-dependent section was chosen |
|||
if (sectionToEdit.match(/^\d{4}年\d{1,2}月\d{1,2}日 - \d{1,2}日新規報告$/) !== null) { |
|||
sectionToEdit = getSectionI(false); |
|||
$('#anr-section-options-i-date').text(getSectionI(false)); |
|||
} |
|||
} else if (pageToEdit === ANS) { // If WP:AN/S is selected as the target page to edit |
|||
reportToANS = true; |
|||
switch(ANSOptSelected) { |
|||
case 'Iccic系 (Iccic)': |
|||
pageToEdit = Iccic; |
|||
sectionToEdit = '新規依頼'; |
|||
break; |
|||
case 'いせちか系 (ISECHIKA)': |
|||
pageToEdit = ISECHIKA; |
|||
sectionToEdit = '新規依頼'; |
|||
break; |
|||
case '影武者系(KAGE)': |
|||
pageToEdit = KAGE; |
|||
sectionToEdit = '新規依頼'; |
|||
break; |
|||
case '清島達郎系 (清島、KIYOSHIMA)': |
|||
pageToEdit = KIYOSHIMA; |
|||
sectionToEdit = '新規依頼'; |
|||
break; |
|||
case '真珠王子系(SHINJU)': |
|||
pageToEdit = SHINJU; |
|||
sectionToEdit = '新規依頼'; |
|||
break; |
|||
default: |
|||
sectionToEdit = ANSOptSelected; |
|||
} |
|||
} else if (pageToEdit === AN3RR) { // If WP:AN/3RR is selected as the target page to edit |
|||
sectionToEdit = '3RR'; |
|||
} |
|||
// Check if necessary fields are filled |
|||
if ( |
|||
pageToEdit === '選択してください' || // The page dropdown's remained 選択してください |
|||
sectionToEdit === '選択してください' || // The section dropdown's remained 選択してください |
|||
$('#anr-reason-text').val().trim2() === '' || // No reason is given |
|||
users.length === 0 // No username is given |
|||
) { |
|||
rqFieldsEmpty = true; |
|||
} |
|||
// UserAN template and the reason of the report |
|||
const UserAN = '{{UserAN|t=TYPE|USER}}'; |
|||
let reason = $('#anr-reason-text').val().trim2(); |
|||
if (reason.substring(reason.length - 4) !== '~~~~') { // If reason doesn't contain signature, add one |
|||
reason = reason + '--~~~~'; |
|||
} |
|||
// Get edit summary |
|||
const editSummarySection = '/*' + sectionToEdit + '*/'; |
|||
let editSummary = |
|||
$('#anr-summary-text').val().trim2() === '' ? |
|||
editSummarySection + genEditSummary().replace(' - ', '') + scriptAd: |
|||
editSummarySection + $('#anr-summary-text').val().trim2() + scriptAd; |
|||
// Get text to add to the page |
|||
let textToSubmit = ''; |
|||
if (users.length < 2) { // If user to report is just one |
|||
textToSubmit = '\* ' + UserAN.replaceAll2('TYPE', types[0], 'USER', users[0]) + ' - ' + reason; |
|||
} else { // If two or more |
|||
for (let i = 0; i < users.length; i++) { |
|||
textToSubmit += '\* ' + UserAN.replaceAll2('TYPE', types[i], 'USER', users[i]) + '\n'; |
|||
} |
|||
textToSubmit += ': ' + reason; |
|||
} |
|||
// Return values |
|||
return { |
|||
'users': users, |
|||
'types': types, |
|||
'duplicates': duplicates, |
|||
'pageToEdit': pageToEdit, |
|||
'sectionToEdit': sectionToEdit, |
|||
'reportToANS': reportToANS, |
|||
'rqFieldsEmpty': rqFieldsEmpty, |
|||
'editSummary': editSummary, |
|||
'textToSubmit': textToSubmit |
|||
} |
|||
} |
|||
// Function for the 'preview' button of the dialog |
|||
function previewBtn() { |
|||
// Check if the necessary fields are filled and get edit information |
|||
let ep = editPrep(); |
|||
if (ep.rqFieldsEmpty) { |
|||
alert('必須項目が入力・選択されていません'); // Show error and cancel the preview |
|||
return; |
|||
} |
|||
// If the inputs have duplicates in them |
|||
if (ep.duplicates.length !== 0) { |
|||
let confirmMsg = |
|||
'以下の利用者について、重複入力がある可能性があります。\n\n' + |
|||
ep.duplicates.join(', ') + '\n\n' + |
|||
'プレビューを続行する場合は OK を、フォームに戻る場合は Cancel を押してください'; |
|||
if (confirm(confirmMsg) === false) { // If cancelled |
|||
// If wgRelevantUserName returns null or the name of the current user, set the variable as a null string |
|||
return; |
|||
username = ''; |
|||
} |
} |
||
} |
|||
// Preview dialog contour |
|||
const ANSMisc = `<a href="${mw.util.getUrl('WP:AN/S#OTH')}" target="_blank">WP:AN/S#その他</a>`; |
|||
const previewDiv = |
|||
$('#anr-user1-checkbox-div').css('display', 'block'); // Show 'hide username' if the name obtained is a user's |
|||
'<div class="anr-preview-dialog" title="AN Reporter Preview">' + |
|||
' <div id="anr-preview-header" style="padding: 0.5em;">' + |
|||
' <p id="anr-preview-loading">' + |
|||
` プレビューを読み込み中${toggleLoadingSpinner('add')}` + |
|||
' </p>' + |
|||
' <p id="anr-preview-warning" style="display: none;">' + |
|||
' 注意1: このプレビュー上のリンクは全て新しいタブで開かれます' + |
|||
' <br>' + |
|||
` 注意2: 報告先が ${ANSMisc} の場合、このプレビューには表示されませんが「他X月X日」のヘッダーは必要に応じて自動挿入されます` + |
|||
' </p>' + |
|||
' </div>' + |
|||
' <div id="anr-preview-body" style="display: none; font-size: 1.1em; padding-top: 1em; border-top: 1px solid silver;">' + |
|||
' <div id="anr-preview-text" style="border: 1px solid silver; padding: 0.2em 0.5em; background: white;">' + |
|||
// previewHtml |
|||
' </div>' + |
|||
' <div id="anr-preview-summary" style="margin-top: 0.8em; border: 1px solid silver; padding: 0.2em 0.5em; background: white;">' + |
|||
// summaryHtml |
|||
' </div>' + |
|||
' </div>' + |
|||
'</div>'; |
|||
// Show preview dialog |
|||
$('body').append(previewDiv); |
|||
$('.anr-preview-dialog').dialog({ |
|||
'height': 'auto', |
|||
'width': $('#content').width() * 0.8, |
|||
'modal': true, |
|||
'position': { my: 'center', at: 'top', of: window }, |
|||
'open': async function(){ |
|||
// CSS |
|||
dialogCSS(); |
|||
// Convert text on the dialog to html |
|||
const parsed = await convertWikitextToHtml(ep.textToSubmit, ep.editSummary); |
|||
if (parsed) { |
|||
let previewHtml = parsed.htmltext; |
|||
let summaryHtml = parsed.htmlsummary.replaceAll2('API', ep.pageToEdit); |
|||
$('#anr-preview-text').append(previewHtml); |
|||
$('#anr-preview-summary').append(summaryHtml); |
|||
$('.autocomment a').css('color', 'gray'); // Change color of section spec in summary |
|||
$('.anr-preview-dialog a').attr('target', '_blank'); // Open all links on a new tab |
|||
$('#anr-preview-body').css('display', 'block'); |
|||
$('#anr-preview-loading').remove(); |
|||
$('#anr-preview-warning').css('display', 'inline'); |
|||
} else { |
|||
$('#anr-preview-loading').text('プレビューの読み込みに失敗しました').css('color', 'MediumVioletRed'); |
|||
setTimeout(function(){ |
|||
$('.anr-preview-dialog').dialog('close'); |
|||
}, 5000); |
|||
} |
|||
}, |
|||
'buttons': [{ |
|||
'text': '閉じる', |
|||
'click': function(){ |
|||
$(this).dialog('close'); |
|||
} |
|||
}] |
|||
}); |
|||
} |
|||
/** |
|||
* Convert wikitext to its HTML format |
|||
* @param {String} wikitext The wikitext to convert |
|||
* @param {String} wikisummary The summary to convert |
|||
* @returns {{htmltext: string, htmlsummary: string}} |
|||
*/ |
|||
async function convertWikitextToHtml(wikitext, wikisummary) { |
|||
return new Promise(function(resolve, reject) { |
|||
$.ajax({ |
|||
'url': mw.util.wikiScript('api'), |
|||
'data': { |
|||
'format': 'json', |
|||
'action': 'parse', |
|||
'text': wikitext, |
|||
'summary': wikisummary, |
|||
'contentmodel': 'wikitext', |
|||
'prop': 'text', |
|||
'disableeditsection': true |
|||
}, |
|||
'dataType': 'json', |
|||
'type': 'POST', |
|||
success: function(res) { |
|||
resolve({ |
|||
'htmltext': res.parse.text['*'], |
|||
'htmlsummary': res.parse.parsedsummary['*'] |
|||
}); |
|||
}, |
|||
error: function(err) { |
|||
console.log(err); |
|||
resolve(); |
|||
} |
|||
}); |
|||
}); |
|||
} |
|||
// Function for the 'report' button of the dialog |
|||
function reportBtn() { |
|||
// Check if the necessary fields are filled and get edit information |
|||
let ep = editPrep(); |
|||
if (ep.rqFieldsEmpty) { |
|||
alert('必須項目が入力・選択されていません'); // Show error and cancel the edit |
|||
return; |
|||
} |
|||
// If the inputs have duplicates in them |
|||
if (ep.duplicates.length !== 0) { |
|||
let confirmMsg = |
|||
'以下の利用者について、重複入力がある可能性があります。\n\n' + |
|||
ep.duplicates.join(', ') + '\n\n' + |
|||
'報告を続行する場合は OK を、フォームに戻る場合は Cancel を押してください'; |
|||
if (confirm(confirmMsg) === false) { // If cancelled |
|||
return; |
|||
} |
} |
||
} |
} |
||
'buttons': [{ |
|||
// Variables for edit |
|||
let $dialog = $('.anr-modal-dialog'); |
|||
let msgEditing = '<div class="anr-editing" />'; |
|||
let msgDone = ''; // Message to show when edit attempt is done |
|||
let wikiPagename = ep.pageToEdit + '#' + ep.sectionToEdit; // The wiki pagename for link |
|||
let editFailed = false; // Boolean value to pass to function when edit attempt is done |
|||
// Change dialog content |
|||
$dialog.find('form').css('display', 'none'); // Hide dialog content |
|||
$dialog.dialog({'buttons': [] }); // Hide the button |
|||
$dialog.append(msgEditing); |
|||
alert('必須項目が入力・選択されていません'); // Show error and cancel the preview |
|||
return; |
|||
// For debugging |
|||
//ep.pageToEdit = '利用者:Dragoniez/test'; |
|||
//ep.editSummary = 'Test edit via mediawiki API' + scriptAd; |
|||
/** |
|||
* Function to check block status before edit |
|||
* @returns {Array} [] if no one is blocked, [user1, user2...] if someone is blocked |
|||
*/ |
|||
async function preeditBlockStatusQuery() { |
|||
// Update message on the dialog |
|||
msgEditing = |
|||
`<p>報告対象者のブロック情報を取得しています${toggleLoadingSpinner('add')}</p>`; |
|||
$('.anr-editing').append(msgEditing); |
|||
// Extract users and IPs from the array |
|||
let tar = []; |
|||
for (let i = 0; i < ep.users.length; i++) { |
|||
switch(ep.types[i]) { |
|||
case 'UNL': |
|||
case 'User2': |
|||
case 'IP2': |
|||
if (!isInArray(ep.users[i], tar)) { |
|||
tar.push(ep.users[i]); |
|||
} |
|||
break; |
|||
case 'logid': |
|||
if (Logids[ep.users[i]] !== undefined) { // If the logid can be converted to a username |
|||
if (!isInArray(Logids[ep.users[i]], tar)) { |
|||
tar.push(Logids[ep.users[i]]); // Push username instead of logid |
|||
} |
|||
} |
|||
break; |
|||
case 'diff': |
|||
case 'none': |
|||
// Do nothing |
|||
} |
} |
||
} |
|||
// Check if any of the users is blocked |
|||
const blocked = await getBlocked(tar); |
|||
// If any of the users is blocked |
|||
if (blocked.length !== 0) { |
|||
// Update message on the dialog |
|||
msgEditing = |
|||
toggleLoadingSpinner('remove') + |
|||
`<p style="color: MediumVioletRed">ブロック済みの利用者を検出しました</p>`; |
|||
$('.anr-editing').append(msgEditing); |
|||
let inputVal; |
|||
let $bsLinkDiv; |
|||
let |
let $bsLink; |
||
'<div class="anr-preview-dialog" title="AN Reporter Preview">' + |
|||
' <div id="anr-preview-header" style="padding: 0.5em;">' + |
|||
' <p id="anr-preview-loading" style="font-weight: bold">' + |
|||
' プレビューを読み込み中...' + |
|||
' </p>' + |
|||
' <p id="anr-preview-warning" style="display: none;">' + |
|||
' 注意1: このプレビュー上のリンクは全て新しいタブで開かれます' + |
|||
' <br>' + |
|||
` 注意2: 報告先が ${ANSMisc} の場合、このプレビューには表示されませんが「他X月X日」のヘッダーは必要に応じて自動挿入されます` + |
|||
' </p>' + |
|||
' </div>' + |
|||
' <div id="anr-preview-body" style="display: none; font-size: 1.1em; padding-top: 1em; border-top: 1px solid silver;">' + |
|||
' <div id="anr-preview-text" style="border: 1px solid silver; padding: 0.2em 0.5em; background: white;">' + |
|||
// previewHtml |
|||
' </div>' + |
|||
' <div id="anr-preview-summary" style="margin-top: 0.8em; border: 1px solid silver; padding: 0.2em 0.5em; background: white;">' + |
|||
// summaryHtml |
|||
' </div>' + |
|||
' </div>' + |
|||
'</div>'; |
|||
// |
// Update dialog |
||
for (let i = 1; i < Infinity; i++) { // Loop through all inputs |
|||
$('.anr-preview-dialog').dialog({ |
|||
if ($(`#anr-user${i}-input`).length === 0) { // if selector is not found |
|||
'width': $('#content').width() * 0.8, |
|||
break; // exit for |
|||
'buttons': [{ |
|||
} else { // if selector is found |
|||
'click': function(){ |
|||
inputVal = $(`#anr-user${i}-input`).val().trim2(); |
|||
$bsLinkDiv = $(`#anr-user${i}-blockstatus-div`); |
|||
$bsLink = $(`#anr-user${i}-blockstatus`); |
|||
$bsLinkDiv.css('display', 'none'); // Temporarily hide the div |
|||
if (isInArray(inputVal, blocked)) { |
|||
$bsLink.attr('href', mw.util.getUrl('特別:投稿記録/' + inputVal)); |
|||
$bsLinkDiv.css('display', 'block'); |
|||
} |
} |
||
}], |
|||
'open': function(){ |
|||
} |
|||
} |
|||
} else { |
|||
new mw.Api().get({ |
|||
'action': 'parse', |
|||
'text': ep.textToSubmit, |
|||
'summary': ep.editSummary, |
|||
'contentmodel': 'wikitext', |
|||
'prop': 'text', |
|||
'disableeditsection': true, |
|||
'formatversion': 2 |
|||
}).then(function(res){ |
|||
// Update message on the dialog |
|||
msgEditing = |
|||
toggleLoadingSpinner('remove') + |
|||
`<p style="color: MediumSeaGreen">ブロック済みの利用者は検出されませんでした</p>`; |
|||
$('.anr-editing').append(msgEditing); |
|||
} |
|||
return blocked; |
|||
let summaryHtml = res.parse.parsedsummary.replace(rep, ep.pageToEdit); |
|||
$('#anr-preview-text').append(previewHtml); |
|||
$('#anr-preview-summary').append(summaryHtml); |
|||
$('.autocomment a').css('color', 'gray'); // Change color of section spec in summary |
|||
$('.anr-preview-dialog a').attr('target', '_blank'); // Open all links on a new tab |
|||
$('#anr-preview-body').css('display', 'block'); |
|||
$('#anr-preview-loading').css('display', 'none'); |
|||
$('#anr-preview-warning').css('display', 'inline'); |
|||
} |
|||
/** |
|||
* Function to check duplicate reports |
|||
$('#anr-preview-loading').text('プレビューの読み込みに失敗しました').css('color', 'red'); |
|||
* @returns {{wikitext: string, reportees: Array}} wikitext === null if sections aren't found, wikitext === '' |
|||
* if there's no duplicate, wikitext === SECTIONTEXT to fetch preview from if there's any. If SECTIONTEXT |
|||
* is returned, reportees === [reportee1, reportee2...], without logids that can be converted to usernames. |
|||
*/ |
|||
async function preeditDuplicateReportQuery() { |
|||
// Update message on the dialog |
|||
msgEditing = |
|||
`<p>重複報告情報を取得しています${toggleLoadingSpinner('add')}</p>`; |
|||
$('.anr-editing').append(msgEditing); |
|||
// Get sections and the whole wikitext of the page to which to report |
|||
async function getWikitextAndSectionInfo(){ |
|||
return new Promise(function(resolve) { |
|||
new mw.Api().get({ |
|||
'action': 'parse', |
|||
'page': ep.pageToEdit, // May need to change this parameter for debugging |
|||
'prop': 'wikitext|sections', |
|||
'formatversion': '2' |
|||
}).then(function(res){ |
|||
resolve({ |
|||
'sections': res.parse.sections, |
|||
'wikitext': res.parse.wikitext |
|||
}); |
}); |
||
} |
}); |
||
}); |
}); |
||
} |
|||
const parsed = await getWikitextAndSectionInfo(); |
|||
let sections = parsed.sections; // Array of objects |
|||
let wikitext = parsed.wikitext; // String |
|||
let sectiontitles = []; |
|||
let sectionheaders = ['']; // Array of equal-enclosed section headers (e.g. == SECTION ==) |
|||
// Note: the top section has no header, thus arr[0] = '' |
|||
// |
// Get section titles and their corresponding equal-enclosed wikitext |
||
for (let i = 0; i < sections.length; i++) { |
|||
'click': function() { |
|||
// |
// The transcluded '新規依頼' sections on WP:AN/S are duplicate names, hence shouldn't be included in the array |
||
if (sections[i].index.indexOf('T') === -1) { // If the section is not loaded from another page |
|||
if (ep.rqFieldsEmpty) { |
|||
alert('必須項目が入力・選択されていません'); // Show error and cancel the edit |
|||
return; |
|||
} |
|||
// |
sectiontitles.push(sections[i].line); // Get section titles |
||
switch(sections[i].level) { // Get equal-enclosed section headers |
|||
case '2': |
|||
sectionheaders.push('== ' + sections[i].line + ' =='); |
|||
break; |
|||
case '3': |
|||
sectionheaders.push('=== ' + sections[i].line + ' ==='); |
|||
break; |
|||
case '4': |
|||
sectionheaders.push('==== ' + sections[i].line + ' ===='); |
|||
break; |
|||
case '5': |
|||
sectionheaders.push('===== ' + sections[i].line + ' ====='); |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
$dialog.dialog('option', 'width', width); |
|||
// The sections in which to search for duplicate reports |
|||
let tarSections; |
|||
const tarSectionsI = [ |
|||
getSectionI(true), |
|||
getSectionI(false), |
|||
'不適切な利用者名', |
|||
'公開アカウント', |
|||
'公開プロキシ・ゾンビマシン・ボット・不特定多数', |
|||
'犯罪行為またはその疑いのある投稿' |
|||
]; |
|||
const tarSectionsS = [ |
|||
'著作権侵害・犯罪予告', |
|||
'名誉毀損・なりすまし・個人情報', |
|||
'妨害編集・いたずら', |
|||
'その他', |
|||
ep.sectionToEdit |
|||
]; |
|||
const tarSections3RR = ['3RR']; |
|||
const tarSectionsSubpagedLTA = ['新規依頼']; |
|||
switch(ep.pageToEdit) { |
|||
case ANI: |
|||
tarSections = tarSectionsI; |
|||
break; |
|||
case ANS: |
|||
tarSections = tarSectionsS; |
|||
break; |
|||
case AN3RR: |
|||
tarSections = tarSections3RR; |
|||
break; |
|||
case Iccic: |
|||
case ISECHIKA: |
|||
case KAGE: |
|||
case KIYOSHIMA: |
|||
case SHINJU: |
|||
tarSections = tarSectionsSubpagedLTA; |
|||
break; |
|||
default: // For debugging, corresponding to the sections on [[User:Dragoniez/test]] |
|||
tarSections = tarSectionsS; |
|||
} |
|||
// Check if the target sections exist |
|||
if (!arrayIsInArray(tarSections, sectiontitles)) { |
|||
sectionNotFound(); |
|||
return {'wikitext': null}; |
|||
} |
|||
// Separate the whole text into an array of sections |
|||
let sectionsPiped = sectiontitles.join('|').replaceAll2('(', '\\(', ')', '\\)', '.', '\\.'); |
|||
let regex = new RegExp(`={2,5}\\s*(?:${sectionsPiped})\\s*={2,5}`, 'g'); |
|||
let sectionContent = wikitext.split(regex); // Array of the content of each section, without section headers |
|||
'titles': ep.pageToEdit, |
|||
// Re-add to sectionContent the '== TITLE ==' header that's been removed by the split() above |
|||
for (let i = 0; i < sectionContent.length; i++) { |
|||
sectionContent[i] = sectionheaders[i] + sectionContent[i]; |
|||
} |
|||
// Remove irrelevant sections from the array of the content of each section (sectionContent) |
|||
if (res && res.query && res.query.pages) { // If the latest revision is successfully retrieved |
|||
sectionsPiped = tarSections.join('|').replaceAll2('(', '\\(', ')', '\\)', '.', '\\.'); |
|||
regex = new RegExp(`={2,5}\\s*(?:${sectionsPiped})\\s*={2,5}`, 'g'); |
|||
for (let i = sectionContent.length -1; i >= 0; i--) { |
|||
if (sectionContent[i].search(regex) === -1) { |
|||
sectionContent.splice(i, 1); |
|||
} |
|||
} |
|||
console.log('The contents of each section:'); |
|||
console.log(sectionContent); |
|||
// Get all the reportees in the inputs, and if user, try to get logids |
|||
let reportees = []; |
|||
let logid; |
|||
for (let i = 0; i < ep.users.length; i++) { // Loop through all the usernames typed into the inputs |
|||
let user = ep.users[i]; |
|||
let type = ep.types[i]; |
|||
' <p style="color: MediumSeaGreen">取得に成功しました</p>' + |
|||
' <p>セクション番号を取得しています</p>'; |
|||
$('.anr-editing').append(msgEditing); |
|||
if (type === 'UNL' || type === 'User2') { |
|||
'baseTS': baseTS, |
|||
'curTS': curTS |
|||
}); |
|||
if (!isInArray(user, reportees)) { // If not already in the array |
|||
if (Logids[user] !== undefined) { // If the code already knows the logid for the user |
|||
'<p style="color: MediumVioletRed">エラー: 報告先のページが存在しません</p>' + manualEdit(); |
|||
$('.anr-editing').append(msgDone); |
|||
$dialog.dialog('option', 'width', width); |
|||
editFailed = true; |
|||
editDone($dialog, editFailed, wikiPagename); |
|||
resolve(undefined); |
|||
reportees.push(user, Logids[user]); // Push the username and the logid into the array |
|||
} else { // If the code doesn't know a logid for the user |
|||
logid = await getLogid(user); // Try to fetch one from the API |
|||
if (logid !== undefined) { // If a valid logid is returned |
|||
Logids[user] = logid; |
|||
Logids[logid] = user; |
|||
reportees.push(user, logid); // Push the username and the logid into the array |
|||
} |
} |
||
} |
|||
} |
|||
}); |
|||
} |
|||
} else if (type === 'logid') { |
|||
async function getSectionNumber() { |
|||
// user = numeral, Logids[user] = username |
|||
if (Logids[user] !== undefined) { // If the logid can be converted to a username |
|||
let sectionsAPI = {}; |
|||
new mw.Api().get({ |
|||
'action': 'parse', |
|||
'page': ep.pageToEdit, |
|||
'formatversion': 2 |
|||
}).done(function(res){ |
|||
if (!isInArray(Logids[user], reportees)) { // If the corresponding username is not in the array |
|||
reportees.push(Logids[user]); // Push the username into the array |
|||
} |
|||
if (!isInArray(user, reportees)) { // If the logid is not in the array |
|||
reportees.push(user); // Push the logid into the array |
|||
} |
|||
} else { // If the logid cannot be converted to a username |
|||
for (let i = 0; i < Object.keys(res.parse.sections).length; i++) { |
|||
sectionsAPI[res.parse.sections[i].line] = res.parse.sections[i].index; |
|||
} |
|||
let sectionNum = sectionsAPI[ep.sectionToEdit]; |
|||
if (sectionNum === undefined) { // If section title in the dropdown is not found |
|||
if (!isInArray(user, reportees)) { // If the logid is not in the array |
|||
reportees.push(user); // Push the logid into the array |
|||
} |
|||
'<p>指定されたセクションが見つかりませんでした</p>' + |
|||
'<br>' + |
|||
'<p>考えられる原因:</p>' + |
|||
`<p>1. 編集先のページの節構成が変更された</p>` + |
|||
`<p>2. 通信に失敗した</p>` + |
|||
`<p>3. スクリプトのバグ</p>` + |
|||
manualEdit(); |
|||
$('.anr-editing').append(msgDone); |
|||
} |
|||
editFailed = true; |
|||
editDone($dialog, editFailed, wikiPagename); |
|||
resolve(undefined); |
|||
} else { // If other than t=UNL, t=User2, and t=logid |
|||
if (!isInArray(user, reportees)) { // If not already in the array |
|||
reportees.push(user); // Push the username into the array |
|||
} |
|||
' <p>最新版のテキストを取得しています</p>'; |
|||
$('.anr-editing').append(msgEditing); |
|||
} |
|||
logid = undefined; // Reset before the next iteration |
|||
} |
|||
} else { // If the section list retrieval fails |
|||
// Extract UserAN templates and find duplicate reports |
|||
let templates = []; |
|||
let dupTemplates = []; |
|||
let duplicateFound = false; |
|||
for (let i = sectionContent.length -1; i >= 0; i--) { // Loop through all the relevant section texts |
|||
/* Extract UserAN templates from the section text |
|||
* This returns an array of the occurrences of UserAN, e.g. [{{UserAN|t=UNL|ウィキ助}}, ...] */ |
|||
templates = findTemplates(sectionContent[i], 'useran'); |
|||
if (templates.length !== 0) { // If the section text contains at least one UserAN |
|||
for (let j = templates.length -1; j >= 0; j--) { // Loop through all the occurences of UserAN |
|||
if (stringContainsElementInArray(templates[j], reportees)) { // If there's a duplciate report |
|||
if (!isInArray(templates[j], dupTemplates)) { |
|||
dupTemplates.push(templates[j]); // List the occurence as a duplicate |
|||
} |
} |
||
duplicateFound = true; |
|||
} |
} |
||
} |
|||
if (!duplicateFound) { // Remove the section text from the array if it doesn't involve duplicate reports |
|||
sectionContent.splice(i, 1); |
|||
} |
|||
} else { // If the section text has no UserAN |
|||
sectionContent.splice(i, 1); // Remove the section text from the array |
|||
} |
} |
||
duplicateFound = false; // Reset |
|||
} |
|||
console.log('Templates that involve potential duplicate reports:'); |
|||
console.log(dupTemplates); |
|||
console.log('Section contents that involve the templates:'); |
|||
console.log(sectionContent); |
|||
// Return text and update dialog |
|||
if (sectionContent.length === 0) { // If there's no duplicate report |
|||
// Update message on the dialog |
|||
msgEditing = |
|||
`<p style="color: MediumSeaGreen">重複報告は検出されませんでした${toggleLoadingSpinner('remove')}</p>`; |
|||
$('.anr-editing').append(msgEditing); |
|||
// Return an empty string |
|||
return {'wikitext': ''}; |
|||
} else { // If there're duplicate reports |
|||
// Update message on the dialog |
|||
msgEditing = |
|||
`<p style="color: MediumVioletRed">重複報告の可能性があります${toggleLoadingSpinner('remove')}</p>`; |
|||
$('.anr-editing').append(msgEditing); |
|||
// Highlight all the duplciate UserAN occurences |
|||
sectionContent = sectionContent.join(''); // Merge the separate sections |
|||
for (let i = 0; i < dupTemplates.length; i++) { |
|||
sectionContent = sectionContent.replaceAll2(dupTemplates[i], '<span style="background-color: #FEC493">' + dupTemplates[i] + '</span>'); |
|||
} |
|||
// Get rid of logids that can be converted to usernames from the array 'reportees' |
|||
for (let i = reportees.length -1; i >= 0; i--) { |
|||
if (String(reportees[i]).match(/^\d+$/) !== null && Logids[reportees[i]] !== undefined) { |
|||
reportees.splice(i, 1); |
|||
} |
|||
} |
|||
// Return wikitext to fetch preview from |
|||
return { |
|||
'wikitext': sectionContent, |
|||
'reportees': reportees |
|||
}; |
|||
'prop': 'wikitext', |
|||
'formatversion': 2 |
|||
}).done(function(res){ |
|||
} |
|||
} |
|||
// Function to get the latest revision of the administrator's noticeboard |
|||
async function getLastestRevision() { |
|||
' <p style="color: MediumSeaGreen">取得に成功しました</p>' + |
|||
' <p>報告を試みています</p>'; |
|||
$('.anr-editing').append(msgEditing); |
|||
return new Promise(function(resolve, reject) { |
|||
let wikitextObtained = res.parse.wikitext; |
|||
let wholeTextToSubmit; |
|||
const delimiter = '<!-- ◆'; |
|||
const miscHeader = `{{bgcolor|#eee|{{Visible anchor|他${today()}}}|div}}`; |
|||
msgEditing = |
|||
`<p>最新版を取得しています${toggleLoadingSpinner('add')}</p>`; |
|||
$('.anr-editing').append(msgEditing); |
|||
new mw.Api().get({ |
|||
'action': 'query', |
|||
'titles': ep.pageToEdit, |
|||
'prop': 'revisions', |
|||
'curtimestamp': true, |
|||
'formatversion': 2 |
|||
}).done(function(res){ |
|||
if (res && res.query && res.query.pages) { // If the latest revision is successfully retrieved |
|||
if (res.query.pages[0].missing !== true) { // If the page exists |
|||
ep.textToSubmit = '; ' + miscHeader + '\n\n' + ep.textToSubmit; |
|||
} |
|||
// Get the timestamps of the latest revision and the API query |
|||
let baseTS = res.query.pages[0].revisions[0].timestamp; // The TS of the latest revision |
|||
let curTS = res.curtimestamp; // The TS of the API query |
|||
// Update message on the dialog |
|||
msgEditing = |
|||
' <p style="color: MediumSeaGreen">取得に成功しました</p>' + |
|||
` <p>セクション番号を取得しています${toggleLoadingSpinner('move')}</p>`; |
|||
$('.anr-editing').append(msgEditing); |
|||
// Return the timestamps as an object |
|||
resolve({ |
|||
'baseTS': baseTS, |
|||
'curTS': curTS |
|||
}); |
|||
} else { // If the page doesn't exist |
|||
msgDone = |
|||
'<p style="color: MediumVioletRed">報告に失敗しました</p>' + |
|||
'<p>セクション構造が改変されているため、' + |
|||
`<a href="${mw.util.getUrl('User talk:Dragoniez/AN Reporter')}" target="_blank">開発者</a>` + |
|||
'に連絡をお願いします</p>' + |
|||
manualEdit(); |
|||
$('.anr-editing').append(msgDone); |
|||
msgDone = |
|||
toggleLoadingSpinner('remove') + |
|||
'<p style="color: MediumVioletRed">エラー: 報告先のページが存在しません</p>' + |
|||
manualEdit(); |
|||
$('.anr-editing').append(msgDone); |
|||
editFailed = true; |
|||
editDone($dialog, editFailed, wikiPagename); |
|||
resolve(); |
|||
} |
|||
} else { // If revision retrieval fails |
|||
queryFailed(); |
|||
resolve(); |
|||
} |
|||
}); |
|||
}); |
|||
} |
|||
// Function to get the section number from the section title |
|||
async function getSectionNumber() { |
|||
return new Promise(function(resolve, reject) { |
|||
new mw.Api().get({ |
|||
'action': 'parse', |
|||
'page': ep.pageToEdit, |
|||
'formatversion': 2 |
|||
}).done(function(res){ |
|||
if (res && res.parse && res.parse.sections) { // If the section list is successfully retrieved |
|||
// Get the titles of all sections and their section numbers |
|||
let sectionsAPI = {}; |
|||
for (let i = 0; i < Object.keys(res.parse.sections).length; i++) { |
|||
sectionsAPI[res.parse.sections[i].line] = res.parse.sections[i].index; |
|||
} |
|||
// Return a section number if the section is found, undefined if not |
|||
let sectionNum = sectionsAPI[ep.sectionToEdit]; |
|||
if (sectionNum === undefined) { // If section title in the dropdown is not found |
|||
// Show the details of the error |
|||
sectionNotFound(); |
|||
resolve(); |
|||
} else { // If section title in the dropdown is found |
|||
// Update message |
|||
msgEditing = |
|||
' <p style="color: MediumSeaGreen">取得に成功しました</p>' + |
|||
` <p>最新版のテキストを取得しています${toggleLoadingSpinner('move')}</p>`; |
|||
$('.anr-editing').append(msgEditing); |
|||
resolve(sectionNum); |
|||
} |
|||
} else { // If the section list retrieval fails |
|||
queryFailed(); |
|||
resolve(); |
|||
} |
|||
}); |
|||
}); |
|||
} |
|||
// Function to get the text to replace with the current text in the section |
|||
async function getTextToReplace(sectionNum) { |
|||
return new Promise(function(resolve, reject) { |
|||
// Get the text of the latest revision |
|||
new mw.Api().get({ |
|||
'action': 'parse', |
|||
'page': ep.pageToEdit, |
|||
'section': sectionNum, |
|||
'prop': 'wikitext', |
|||
'formatversion': 2 |
|||
}).done(function(res){ |
|||
if (res && res.parse) { |
|||
// Update message |
|||
msgEditing = |
|||
' <p style="color: MediumSeaGreen">取得に成功しました</p>' + |
|||
` <p>報告を試みています${toggleLoadingSpinner('move')}</p>`; |
|||
$('.anr-editing').append(msgEditing); |
|||
// Get the whole text to append |
|||
let wikitextObtained = res.parse.wikitext; |
|||
let wholeTextToSubmit; |
|||
const delimiter = '<!-- ◆'; |
|||
const miscHeader = `{{bgcolor|#eee|{{Visible anchor|他${today()}}}|div}}`; |
|||
if (ep.reportToANS) { // If the target is WP:AN/S |
|||
// Add div if the target section is 'その他' but lacks div for the current date |
|||
if (ep.sectionToEdit === 'その他' && wikitextObtained.indexOf(miscHeader) === -1) { |
|||
ep.textToSubmit = '; ' + miscHeader + '\n\n' + ep.textToSubmit; |
|||
} |
} |
||
// Insert text into the right place |
|||
let wikitextSplit = wikitextObtained.split(delimiter); |
|||
if (wikitextSplit.length === 3) { |
|||
wholeTextToSubmit = |
|||
wikitextSplit[0] + delimiter + wikitextSplit[1].trim2() + '\n\n' + |
|||
ep.textToSubmit + '\n\n' + delimiter + wikitextSplit[2]; |
|||
resolve(wholeTextToSubmit); |
|||
'url': mw.util.wikiScript('api'), |
|||
'data': { |
|||
'format': 'json', |
|||
'action': 'edit', |
|||
'title': ep.pageToEdit, |
|||
'section': sectionNum, |
|||
'text': text, |
|||
'summary': ep.editSummary, |
|||
'basetimestamp': baseTS, |
|||
'starttimestamp': curTS, |
|||
'token': mw.user.tokens.get('csrfToken'), |
|||
'curtimestamp': true |
|||
}, |
|||
'dataType': 'json', |
|||
'type': 'POST', |
|||
success: function(res) { |
|||
// If the edit was successful |
|||
if (res && res.edit && res.edit.result == 'Success') { |
|||
setTimeout(async function(){ |
|||
let diffNum = await getDiffNum(res.curtimestamp); // Get diff number |
|||
// Show message |
|||
$('.anr-editing').append(`<p style="color: MediumSeaGreen">報告が完了しました</p>`); |
|||
$dialog.dialog('option', 'width', width); |
|||
editDone($dialog, editFailed, wikiPagename, diffNum); |
|||
return; |
|||
} else { // If the structure of the section has been changed |
|||
// |
// Show error and quit the procedure |
||
msgDone = |
|||
toggleLoadingSpinner('remove') + |
|||
// Show the details of the error |
|||
msgDone = |
|||
'<p style="color: MediumVioletRed">報告に失敗しました</p>' + |
'<p style="color: MediumVioletRed">報告に失敗しました</p>' + |
||
'< |
'<p>セクション構造が改変されているため、' + |
||
`<a href="${mw.util.getUrl('User talk:Dragoniez/AN Reporter')}" target="_blank">開発者</a>` + |
|||
'に連絡をお願いします</p>' + |
|||
manualEdit(); |
manualEdit(); |
||
$('.anr-editing').append(msgDone); |
$('.anr-editing').append(msgDone); |
||
$dialog.dialog('option', 'width', width); |
|||
editFailed = true; |
editFailed = true; |
||
editDone($dialog, editFailed, wikiPagename); |
editDone($dialog, editFailed, wikiPagename); |
||
resolve(); |
|||
} |
|||
} else { |
|||
/ |
} else { // If the target is WP:AN/I or WP:AN/3RR |
||
msgDone = |
|||
'<p style="color: MediumVioletRed">不明なエラーが発生しました</p>' + manualEdit(); |
|||
$('.anr-editing').append(msgDone); |
|||
wholeTextToSubmit = wikitextObtained.trim2() + '\n\n' + ep.textToSubmit; |
|||
resolve(wholeTextToSubmit); |
|||
editDone($dialog, editFailed, wikiPagename); |
|||
return; |
|||
} |
|||
} |
} |
||
}); |
|||
} else { // If wikitext retrieval fails |
|||
queryFailed(); |
|||
resolve(); |
|||
} |
|||
}); |
|||
}); |
|||
} |
|||
// Function to edit the page |
|||
function edit(sectionNum, text, baseTS, curTS) { |
|||
$.ajax({ |
|||
'url': mw.util.wikiScript('api'), |
|||
'data': { |
|||
'format': 'json', |
|||
'action': 'edit', |
|||
'title': ep.pageToEdit, |
|||
'section': sectionNum, |
|||
'text': text, |
|||
'summary': ep.editSummary, |
|||
'basetimestamp': baseTS, |
|||
'starttimestamp': curTS, |
|||
'token': mw.user.tokens.get('csrfToken'), // May comment this out for debugging |
|||
'curtimestamp': true |
|||
}, |
|||
'dataType': 'json', |
|||
'type': 'POST', |
|||
success: function(res) { |
|||
// If the edit was successful |
|||
if (res && res.edit && res.edit.result == 'Success') { |
|||
setTimeout(async function(){ |
|||
const diffNum = await getDiffNum(res.curtimestamp); // Get diff number |
|||
// Show message |
|||
toggleLoadingSpinner('remove'); |
|||
$('.anr-editing').append(`<p style="color: MediumSeaGreen">報告が完了しました</p>`); |
|||
editDone($dialog, editFailed, wikiPagename, diffNum); |
|||
return; |
|||
}, 0); |
|||
async function reportUser() { |
|||
// If the edit failed |
|||
} else if (res && res.error) { |
|||
// Show the details of the error |
|||
msgDone = |
|||
toggleLoadingSpinner('remove') + |
|||
'<p style="color: MediumVioletRed">報告に失敗しました</p>' + |
|||
'<br>' + |
|||
'<p>詳細:</p>' + |
|||
`<p>${res.error.info}</p>` + |
|||
manualEdit(); |
|||
$('.anr-editing').append(msgDone); |
|||
editFailed = true; |
|||
editDone($dialog, editFailed, wikiPagename); |
|||
return; |
return; |
||
// If unknown error occurs |
|||
} else { |
} else { |
||
baseTS = rev.baseTS; |
|||
curTS = rev.curTS; |
|||
} |
|||
// Show message |
|||
msgDone = |
|||
toggleLoadingSpinner('remove') + |
|||
'<p style="color: MediumVioletRed">不明なエラーが発生しました</p>' + |
|||
manualEdit(); |
|||
$('.anr-editing').append(msgDone); |
|||
editFailed = true; |
|||
editDone($dialog, editFailed, wikiPagename); |
|||
return; |
return; |
||
} |
} |
||
} |
|||
}); |
|||
} |
|||
// Function to execute report |
|||
async function reportUsers() { |
|||
// Check the block status of the reportees if the checkbox is checked |
|||
let blocked = []; |
|||
if (checkBlockStatusBeforeEdit) { |
|||
blocked = await preeditBlockStatusQuery(); // Query who's blocked |
|||
} |
|||
$dialog.dialog('option', 'width', width); |
|||
editFailed = true; |
|||
editDone($dialog, editFailed, wikiPagename); |
|||
} |
|||
// |
if (blocked.length !== 0) { // If any of the reportees is blocked |
||
function manualEdit() { |
|||
let meHtml = |
|||
'<br>' + |
|||
'<p>手動編集用:</p>' + |
|||
`<textarea disabled rows="4" style="width: 100%">${ep.textToSubmit}</textarea>` + |
|||
'<br>' + |
|||
'<p>要約:</p>' + |
|||
`<textarea disabled rows="2" style="width: 100%">${ep.editSummary.replace(scriptAd, '')}</textarea>`; |
|||
return meHtml; |
|||
} |
|||
// |
// Update dialog buttons |
||
$dialog.dialog({ |
|||
'buttons': [{ |
|||
'text': '続行', |
|||
'click': function(){ |
|||
}); |
$(this).dialog({'buttons': [] }); |
||
reportUsersSecondProcedure(); |
|||
} |
|||
}, { |
|||
'text': '戻る', |
|||
'click': function(){ |
|||
$('.anr-modal-dialog form').css('display', 'block'); |
|||
$('.anr-editing').remove(); |
|||
$dialog.dialog({ |
|||
'buttons': [{ |
|||
'text': 'プレビュー', |
|||
'click': previewBtn |
|||
}, { |
|||
'text': '報告', |
|||
'click': reportBtn |
|||
}] |
|||
}); |
|||
} |
|||
}, { |
|||
'text': '中止', |
|||
'click': function(){ |
|||
$(this).dialog('close'); |
|||
} |
|||
}] |
|||
}); |
|||
} else { // If no one is blocked |
|||
reportUsersSecondProcedure(); |
|||
// Check if at least one username is given and get users to report (and their UserAN types) |
|||
let users = []; |
|||
let types = []; |
|||
for (let i = 1; i < Infinity; i++) { // Loop through all inputs |
|||
if ($(`#anr-user${i}-input`).length === 0) { // if selector is not found |
|||
break; // exit for |
|||
} else { // if selector is found |
|||
if (trimA($(`#anr-user${i}-input`).val()) !== '') { // if input is not empty |
|||
users.push(trimA($(`#anr-user${i}-input`).val())); // Push the username into the array |
|||
types.push($(`#anr-user${i}-select`).children('option').filter(':selected').text()); // Push the UserAN type into the array |
|||
} |
|||
} |
} |
||
} |
} |
||
async function reportUsersSecondProcedure() { |
|||
let pageToEdit = $('#anr-target-options').children('option').filter(':selected').text(); |
|||
let sectionToEdit = '選択してください'; |
|||
let ANSOptSelected = $('#anr-section-options-s').find('option').filter(':selected').text(); |
|||
let reportToANS = false; |
|||
// Check duplicate reports if the checkbox is checked |
|||
let dr; |
|||
if (checkDuplicateReportsBeforeEdit) { |
|||
dr = await preeditDuplicateReportQuery(); |
|||
} |
|||
switch(dr.wikitext) { |
|||
case null: // Sections aren't found |
|||
return; |
|||
case undefined: // checkDuplicateReportsBeforeEdit === false |
|||
case '': // No duplicate report found |
|||
reportUsersFinalProcedure(); |
|||
return; |
|||
default: // Possible duplicate reports present |
|||
// Update dialog buttons |
|||
$dialog.dialog({ |
|||
'buttons': [{ |
|||
'text': '確認', |
|||
'click': function() { |
|||
previewDuplicateReports(dr.wikitext, dr.reportees); |
|||
} |
|||
} else if (ANSOptSelected === 'いせちか系 (ISECHIKA)') { |
|||
}, { |
|||
sectionToEdit = '新規依頼'; |
|||
'text': '続行', |
|||
'click': function(){ |
|||
$(this).dialog({'buttons': [] }); |
|||
reportUsersFinalProcedure(); |
|||
} |
|||
sectionToEdit = '新規依頼'; |
|||
}, { |
|||
pageToEdit = SHINJU; |
|||
'text': '戻る', |
|||
'click': function(){ |
|||
$('.anr-modal-dialog form').css('display', 'block'); |
|||
$('.anr-editing').remove(); |
|||
$dialog.dialog({ |
|||
'buttons': [{ |
|||
'text': 'プレビュー', |
|||
'click': previewBtn |
|||
}, { |
|||
'text': '報告', |
|||
'click': reportBtn |
|||
}] |
|||
}); |
|||
} |
|||
}, { |
|||
'text': '中止', |
|||
'click': function(){ |
|||
$(this).dialog('close'); |
|||
} |
|||
}] |
|||
}); |
|||
} |
|||
} |
|||
async function reportUsersFinalProcedure() { |
|||
const rev = await getLastestRevision(); |
|||
if (rev === undefined) { |
|||
return; |
|||
} else { |
} else { |
||
var baseTS = rev.baseTS; |
|||
var curTS = rev.curTS; |
|||
} |
} |
||
const sectionNum = await getSectionNumber(); |
|||
if (sectionNum === undefined) { |
|||
return; |
|||
} |
|||
const text = await getTextToReplace(sectionNum); |
|||
if (text === undefined) { |
|||
return; |
|||
} |
|||
edit(sectionNum, text, baseTS, curTS); |
|||
} |
} |
||
/** |
|||
* Function to preview duplicate reports on a new dialog |
|||
pageToEdit === '選択してください' || // The page dropdown's remained 選択してください |
|||
* @param {String} wikitext The wikitext to convert to html and preview |
|||
sectionToEdit === '選択してください' || // The section dropdown's remained 選択してください |
|||
* @param {Array} reportees The array of reportees, logids should be converted to usernames if possible beforehand |
|||
trimA($('#anr-reason-text').val()) === '' || // No reason is given |
|||
*/ |
|||
) { |
function previewDuplicateReports(wikitext, reportees) { |
||
// Show logids in parentheses |
|||
for (let i = 0; i < reportees.length; i++) { |
|||
if (Logids[reportees[i]]) { // If the code knows the logid for a user |
|||
reportees[i] += ' (' + Logids[reportees[i]] + ')'; // Show the logid in parentheses |
|||
} |
|||
} |
|||
// Create dialog |
|||
let duplicateReportPreviewDiv = |
|||
'<div class="anr-drpreview-dialog" title="AN Reporter Duplicate Report Preview" style="max-height: 80vh;">' + |
|||
' <div id="anr-drpreview-header" style="padding: 0.5em;">' + |
|||
' <p id="anr-drpreview-loading">' + |
|||
` 読み込み中${toggleLoadingSpinner('add')}` + |
|||
' </p>' + |
|||
' <p id="anr-drpreview-userlist" style="display: none; font-size: larger">' + |
|||
' <span style="font-weight: bold">入力された利用者名:</span>' + |
|||
' <br>' + |
|||
reportees.join(', ') + |
|||
' </p>' + |
|||
' </div>' + |
|||
' <div id="anr-drpreview-body" style="display: none; font-size: 1.1em; padding: 0.5em; border: 1px solid silver; background-color: white;">' + |
|||
// Added when the dialog is opened |
|||
' </div>' + |
|||
'</div>'; |
|||
$('body').append(duplicateReportPreviewDiv); |
|||
// Show preview dialog |
|||
$('.anr-drpreview-dialog').dialog({ |
|||
'height': 'auto', |
|||
'width': $('#content').width() * 0.8, |
|||
'modal': true, |
|||
'position': { my: 'center', at: 'top', of: window }, |
|||
'open': async function(){ |
|||
// CSS |
|||
dialogCSS(); |
|||
// Convert the wikitext to an html form |
|||
const wikitextInHtml = await convertWikitextToHtml(wikitext, ''); |
|||
if (wikitextInHtml) { |
|||
$('#anr-drpreview-body').append(wikitextInHtml.htmltext); |
|||
$('.anr-drpreview-dialog a').attr('target', '_blank'); // Open all links on a new tab |
|||
$('#anr-drpreview-body').css('display', 'block'); |
|||
$('#anr-drpreview-loading').remove(); |
|||
$('#anr-drpreview-userlist').css('display', 'inline'); |
|||
} else { |
|||
$('#anr-drpreview-loading').text('読み込みに失敗しました').css('color', 'MediumVioletRed'); |
|||
setTimeout(function(){ |
|||
$('.anr-drpreview-dialog').dialog('close'); |
|||
}, 10000); |
|||
} |
|||
}, |
|||
'buttons': [{ |
|||
'text': '閉じる', |
|||
'click': function(){ |
|||
$(this).dialog('close'); |
|||
} |
|||
}] |
|||
}); |
|||
} |
} |
||
// |
// Function to show message when edit attempt is done |
||
function queryFailed() { |
|||
msgDone = |
|||
toggleLoadingSpinner('remove') + |
|||
if (reason.substring(reason.length - 4) !== '~~~~') { // If reason doesn't contain signature, add one |
|||
'<p style="color: MediumVioletRed">取得に失敗しました</p>' + |
|||
manualEdit(); |
|||
$('.anr-editing').append(msgDone); |
|||
editFailed = true; |
|||
editDone($dialog, editFailed, wikiPagename); |
|||
} |
} |
||
// |
// Function to generate the html for the manual edit helper tab |
||
function manualEdit() { |
|||
let meHtml = |
|||
'<br>' + |
|||
'<p>手動編集用:</p>' + |
|||
`<textarea disabled rows="4" style="width: 100%">${ep.textToSubmit}</textarea>` + |
|||
'<br>' + |
|||
'<p>要約:</p>' + |
|||
`<textarea disabled rows="2" style="width: 100%">${ep.editSummary.replace(scriptAd, '')}</textarea>`; |
|||
return meHtml; |
|||
} |
|||
// Function to show error message if sections that must be there are not found |
|||
let editSummary = |
|||
function sectionNotFound() { |
|||
msgDone = |
|||
toggleLoadingSpinner('remove') + |
|||
'<p style="color: MediumVioletRed">取得に失敗しました</p>' + |
|||
'<p>指定されたセクションが見つかりませんでした</p>' + |
|||
'<br>' + |
|||
'<p>考えられる原因:</p>' + |
|||
`<p>1. 編集先のページの節構成が変更された</p>` + |
|||
`<p>2. 通信に失敗した</p>` + |
|||
`<p>3. スクリプトのバグ</p>` + |
|||
manualEdit(); |
|||
$('.anr-editing').append(msgDone); |
|||
editFailed = true; |
|||
editDone($dialog, editFailed, wikiPagename); |
|||
if (users.length < 2) { // If user to report is just one |
|||
textToSubmit = '\* ' + UserAN.replace('TYPE', types[0]).replace('USER', users[0]) + ' - ' + reason; |
|||
} else { // If two or more |
|||
for (let i = 0; i < users.length; i++) { |
|||
textToSubmit += '\* ' + UserAN.replace('TYPE', types[i]).replace('USER', users[i]) + '\n'; |
|||
} |
|||
textToSubmit += ': ' + reason; |
|||
} |
} |
||
// |
// Report the user(s) |
||
reportUsers(); |
|||
'pageToEdit': pageToEdit, |
|||
} |
|||
'reportToANS': reportToANS, |
|||
/** |
|||
* Function to extract templates from wikitext |
|||
* @param {String} text The text in which to search for templates |
|||
* @param {String} templateName [Optional] Specify the template name (case-insensitive) |
|||
* @returns {Array} An array of the extracted templates |
|||
*/ |
|||
function findTemplates(text, templateName) { |
|||
// Split the text with '{{', the head delimiter of templates |
|||
const textSplit = text.split('{{'); |
|||
let templates = []; |
|||
/* Explanation |
|||
* If the text to parse is "{{UserAN|t=none|{{Logid|123456789|{{User|EXAMPLE}}|s=}}}} - {{UserAN|USER}} is the same.", |
|||
* textSplit = ['', 'UserAN|t=none|', 'Logid|123456789|', 'User|EXAMPLE}}|s=}}}} - ', 'UserAN|USER}} is the same.'] |
|||
*/ |
|||
if (textSplit.length === 0) { // If the text has no tempalte in it |
|||
return templates; // Return an empty array |
|||
} else { // If the text has some templates in it |
|||
for (let i = 1; i < textSplit.length; i++) { // Loop through all elements (but arr[0]) in the split array |
|||
let TLCloseCnt = (textSplit[i].match(/\}\}/g) || []).length; // Get the number of '}}' in the split segment |
|||
let temp = ''; // Temporary escape hatch |
|||
switch(true) { |
|||
case TLCloseCnt === 0: // If the split segment doesn't have any '}}' |
|||
// Do nothing |
|||
break; |
|||
case TLCloseCnt === 1: // If the split segment itself is the whole of a template |
|||
temp = '{{' + textSplit[i].split('}}')[0] + '}}'; |
|||
if (!isInArray(temp, templates)) { |
|||
templates.push(temp); |
|||
} |
|||
break; |
|||
/* Explanation |
|||
* If textSplit[3] ( = 'User|EXAMPLE}}|s=}}}} - '), .split('}}') returns |
|||
* ['User|EXAMPLE', '|s=', '', ' - ']. Thus '{{' + splitArr[0] +'}}' returns '{{User|EXAMPLE}}'. |
|||
*/ |
|||
case TLCloseCnt > 1: // If templates are nested |
|||
for (let j = 0; j < TLCloseCnt; j++) { // Loop through all the nests |
|||
if (j === 0) { // The innermost template |
|||
temp = '{{' + textSplit[i].split('}}')[j] + '}}'; // Same as when TLCloseCnt === 1 |
|||
if (!isInArray(temp, templates)) { |
|||
templates.push(temp); |
|||
} |
|||
} else { // Nesting templates |
|||
temp = '{{' + textSplit[i - j] + temp + textSplit[i].split('}}')[j] + '}}'; |
|||
if (!isInArray(temp, templates)) { |
|||
templates.push(temp); |
|||
} |
|||
/* Explanation |
|||
* Assume that i is 3 and j is 1. Then the 'temp' variable has the value '{{User|EXAMPLE}}' |
|||
* because this is the value returned when j is 0. We want to nest this inside its parent |
|||
* template, and the left segment of the parent template is in textSplit[2] (thus 3 - 1). |
|||
* Now we just want to add the child template (stored in 'temp'), then what remains is the |
|||
* right segment of the parent template, and this is just the splitArr[1] of textSplit[3] |
|||
* (see also the explanation for when TLCloseCnt === 1.) If there are more templates, |
|||
* looping this process will do. |
|||
*/ |
|||
} |
|||
} |
|||
break; |
|||
default: |
|||
// Do nothing |
|||
} |
|||
} |
|||
//console.log('Templates in the section:'); |
|||
//console.log(templates); |
|||
// Check if the optional parameter is specified |
|||
if (templateName !== undefined && templates.length !== 0) { |
|||
let templateRegExp = new RegExp(templateName, 'i'); // Template names are case-insensitive |
|||
for (let i = templates.length -1; i >= 0; i--) { |
|||
if (templates[i].search(templateRegExp) === -1) { // If the array element doesn't contain the template name |
|||
templates.splice(i, 1); // Remove the element |
|||
} |
|||
} |
|||
} |
|||
//console.log('UserAN occurrences in the section:'); |
|||
//console.log(templates); |
|||
return templates; |
|||
} |
} |
||
} |
} |
||
/ |
/** |
||
* Function to add/remove/move a loading spinner |
|||
function dialogCSS() { |
|||
* @param {string} action - 'add', 'remove', or 'move' |
|||
$('.ui-dialog-content, .ui-dialog-buttonpane, .ui-corner-all, .ui-draggable, .ui-resizable').css('background', '#FFF0E4'); |
|||
*/ |
|||
function toggleLoadingSpinner(action) { |
|||
'color': 'black', |
|||
'background-color': 'white' |
|||
const img = '<img ' + |
|||
'class="anr-loading-spinner" ' + |
|||
$('.ui-dialog-titlebar, .ui-dialog-titlebar-close').attr('style', 'background: #FEC493 !important;'); |
|||
'src="https://upload.wikimedia.org/wikipedia/commons/4/42/Loading.gif" ' + |
|||
$('.ui-dialog').css('font-size', fSize); |
|||
'style=' + |
|||
'vertical-align: middle; ' + |
|||
'max-height: 100%; ' + |
|||
'border: 0;" ' + |
|||
'/>'; |
|||
switch(action) { |
|||
case 'add': |
|||
return img; |
|||
case 'remove': |
|||
$('.anr-loading-spinner').remove(); |
|||
return ''; |
|||
case 'move': |
|||
$('.anr-loading-spinner').remove(); |
|||
return img; |
|||
} |
|||
} |
} |
||
950行目: | 1,905行目: | ||
'click': function(){ |
'click': function(){ |
||
window.open(mw.util.getUrl('特別:差分/' + diffNum), '_blank'); |
window.open(mw.util.getUrl('特別:差分/' + diffNum), '_blank'); |
||
} |
} |
||
}; |
}; |
||
btns.push(diffBtn); |
btns.push(diffBtn); |
||
992行目: | 1,947行目: | ||
// Show the button(s) on the dialog |
// Show the button(s) on the dialog |
||
$dialog.dialog({ |
$dialog.dialog({ |
||
'position': { my: 'center', at: 'top |
'position': { my: 'center', at: 'top', of: window }, |
||
'buttons': btns |
'buttons': btns |
||
}); |
}); |
||
1,016行目: | 1,971行目: | ||
type = $(`#anr-user${i}-select`).children('option').filter(':selected').text(); // UserAN type specified in the dropdown |
type = $(`#anr-user${i}-select`).children('option').filter(':selected').text(); // UserAN type specified in the dropdown |
||
reportee = |
reportee = $(`#anr-user${i}-input`).val().trim2(); // input value |
||
if (reportee !== '') { // Skip if the input value is a null string |
if (reportee !== '') { // Skip if the input value is a null string |
||
1,032行目: | 1,987行目: | ||
case 'diff': |
case 'diff': |
||
contribs = `[[特別:差分/${reportee}|差分/${reportee}]]の投稿者`; |
contribs = `[[特別:差分/${reportee}|差分/${reportee}]]の投稿者`; |
||
break; |
break; |
||
default: |
default: |
||
contribs = reportee; |
contribs = reportee; |
||
1,064行目: | 2,019行目: | ||
// Reset dialog when closed |
// Reset dialog when closed |
||
$(document). |
$(document).off('dialogclose', '.anr-modal-dialog, .anr-preview-dialog, .anr-drpreview-dialog') |
||
.on('dialogclose', '.anr-modal-dialog, .anr-preview-dialog, .anr-drpreview-dialog', function() { |
|||
$(this).remove(); |
$(this).remove(); |
||
}); |
}); |
||
// Dynamically change the content of the section dropdown depending on the value selected in '報告先' |
// Dynamically change the content of the section dropdown depending on the value selected in '報告先' |
||
$(document).on('change', '#anr-target-options', function(){ |
$(document).off('change', '#anr-target-options').on('change', '#anr-target-options', function(){ |
||
const selectedTar = $(this).children('option').filter(':selected').text(); |
|||
switch(selectedTar) { |
switch(selectedTar) { |
||
case ANI: |
case ANI: |
||
$('.anr-section-div').empty(); |
$('.anr-section-div').empty(); |
||
$('.anr-section-div').append(sectionsI); |
$('.anr-section-div').append(sectionsI); |
||
$('#anr-section-options-i-date').text(getSectionI(false)); |
|||
$('#anr-section-options-i').css({'width': $(this).width()}); |
$('#anr-section-options-i').css({'width': $(this).width()}); |
||
$('.anr-section-div').css('display', 'block'); |
$('.anr-section-div').css('display', 'block'); |
||
$('.anr-target- |
$('.anr-target-pagelink-div').css('display', 'block'); |
||
$('#anr-target- |
$('#anr-target-pagelink').attr('href', mw.util.getUrl(ANI)); |
||
break; |
break; |
||
case ANS: |
case ANS: |
||
$('.anr-section-div').empty(); |
$('.anr-section-div').empty(); |
||
$('.anr-section-div').append(sectionsS); |
$('.anr-section-div').append(sectionsS); |
||
$('#anr-section-options-s').select2({'width': $(this).width()}); |
$('#anr-section-options-s').select2({'width': $(this).width()}); |
||
//$('#select2-anr-section-options-s-container').attr('style', rlsCSS); |
|||
$('.anr-section-div').css('display', 'block'); |
$('.anr-section-div').css('display', 'block'); |
||
$('.anr-target- |
$('.anr-target-pagelink-div').css('display', 'block'); |
||
$('#anr-target- |
$('#anr-target-pagelink').attr('href', mw.util.getUrl(ANS)); |
||
break; |
break; |
||
case AN3RR: |
case AN3RR: |
||
$('.anr-section-div').empty(); |
$('.anr-section-div').empty(); |
||
$('.anr-section-div').css('display', 'none'); |
$('.anr-section-div').css('display', 'none'); |
||
$('.anr-target- |
$('.anr-target-pagelink-div').css('display', 'block'); |
||
$('#anr-target- |
$('#anr-target-pagelink').attr('href', mw.util.getUrl(AN3RR)); |
||
break; |
break; |
||
} |
} |
||
1,098行目: | 2,056行目: | ||
// Add section name to the '報告先' link when section is specified |
// Add section name to the '報告先' link when section is specified |
||
$(document). |
$(document).off('change', '#anr-section-options-i, #anr-section-options-s') |
||
.on('change', '#anr-section-options-i, #anr-section-options-s', function(){ |
|||
let tarSection = ''; |
let tarSection = ''; |
||
1,109行目: | 2,068行目: | ||
if ($(this).find('option').filter(':selected').text() !== '選択してください') { |
if ($(this).find('option').filter(':selected').text() !== '選択してください') { |
||
tarSection = '#' + $(this).find('option').filter(':selected').text(); |
tarSection = '#' + $(this).find('option').filter(':selected').text(); |
||
$('#anr-target- |
$('#anr-target-pagelink').attr('href', mw.util.getUrl(tarPage + tarSection)); |
||
} |
} |
||
}); |
}); |
||
// |
// When the selection is changed in the type dropdown |
||
$(document).on('change','.anr-user-div select', function(e){ |
$(document).off('change','.anr-user-div select').on('change','.anr-user-div select', function(e){ |
||
const selectID = '#' + e.target.id; // #anr-userX-select |
|||
const valSelected = $(selectID).children('option').filter(':selected').text(); // Selected type |
|||
const inputID = selectID.replace('select', 'input'); // #anr-userX-input |
|||
const valInput = $(inputID).val().trim2(); // The input value |
|||
const checkboxDivID = selectID.replace('select', 'checkbox-div'); // #anr-userX-checkbox-div |
|||
const checkboxID = selectID.replace('select', 'checkbox'); // #anr-userX-checkbox |
|||
const idlinkDivID = selectID.replace('select', 'idlink-div'); // #anr-userX-idlink-div |
|||
const idlinkID = selectID.replace('select', 'idlink'); // #anr-userX-idlink |
|||
switch(valSelected) { |
|||
case 'UNL': |
|||
case 'User2': |
|||
$(checkboxDivID).css('display', 'block'); |
|||
toggleBlockStatusLink(inputID, false, false); |
|||
break; |
|||
case 'IP2': |
|||
toggleBlockStatusLink(inputID, false, false); |
|||
break; |
|||
case 'logid': |
|||
$(checkboxDivID).css('display', 'block'); |
|||
$(checkboxID).prop('checked', true); |
|||
$(idlinkDivID).css('display', 'block'); |
|||
$(idlinkID).attr('href', mw.util.getUrl('Special:redirect/logid/' + valInput)).text('特別:転送/logid/' + valInput); |
|||
toggleBlockStatusLink(inputID, true, true); |
|||
break; |
|||
case 'diff': |
|||
$(checkboxDivID).css('display', 'none'); |
|||
$(idlinkDivID).css('display', 'block'); |
|||
$(idlinkID).attr('href', mw.util.getUrl('Special:diff/' + valInput)).text('特別:差分/' + valInput); |
|||
toggleBlockStatusLink(inputID, true, false); |
|||
break; |
|||
default: |
|||
$(checkboxDivID).css('display', 'none'); |
|||
$(idlinkDivID).css('display', 'none'); |
|||
toggleBlockStatusLink(inputID, true, false); |
|||
} |
|||
if (valSelected === 'UNL' || valSelected === 'User2' ) { // if type=UNL or User2 |
|||
$(checkboxDivID).css('display', 'block'); |
|||
} else if (valSelected === 'logid' ) { // if type=logid |
|||
$(checkboxDivID).css('display', 'block'); |
|||
$(checkboxID).prop('checked', true); |
|||
$(aDivID).css('display', 'block'); |
|||
$(aID).attr('href', mw.util.getUrl('Special:redirect/logid/' + valInput)).text('特別:転送/logid/' + valInput); |
|||
} else if (valSelected === 'diff' ) { // if type=diff |
|||
$(checkboxDivID).css('display', 'none'); |
|||
$(aDivID).css('display', 'block'); |
|||
$(aID).attr('href', mw.util.getUrl('Special:diff/' + valInput)).text('特別:差分/' + valInput); |
|||
} else { // if type=none |
|||
$(checkboxDivID).css('display', 'none'); |
|||
$(aDivID).css('display', 'none'); |
|||
} |
|||
}); |
}); |
||
// When username is typed in, change dropdown options for UserAN types |
// When username is typed in, change dropdown options for UserAN types |
||
$(document).on('input', '.anr-user-div :text', function(e){ |
$(document).off('input', '.anr-user-div :text').on('input', '.anr-user-div :text', function(e){ |
||
const inputID = '#' + e.target.id; // #anr-userX-input |
|||
updateTypeDropdown(inputID); |
|||
let selectID = '#' + e.target.id.replace('input', 'select'); // #anr-user1-select (<select>) |
|||
typeDropdown(inputID, selectID); |
|||
resetDropdown(inputID, selectID); |
|||
}); |
}); |
||
// When 'hide username' is clicked, get logid, change dropdown options, show href and so on |
// When 'hide username' is clicked, get logid, change dropdown options, show href and so on |
||
$(document).off('change', '.anr-user-div :checkbox').on('change', '.anr-user-div :checkbox', function(e){ |
|||
let objLogid = {}; |
|||
$(document).on('change', '.anr-user-div :checkbox', function(e){ |
|||
const checkboxID = '#' + e.target.id; // #anr-userX-checkbox |
|||
const selectID = checkboxID.replace('checkbox', 'select'); // #anr-userX-select |
|||
const inputID = checkboxID.replace('checkbox', 'input'); // #anr-userX-input |
|||
const inputVal = $(inputID).val().trim2(); |
|||
const idlinkID = checkboxID.replace('checkbox', 'idlink'); // #anr-userX-idlink |
|||
const idlinkDivID = checkboxID.replace('checkbox', 'idlink-div'); // #anr-userX-idlink-div |
|||
if ($(checkboxID).is(':checked')) { // if the checkbox is checked |
if ($(checkboxID).is(':checked')) { // if the checkbox is checked |
||
1,181行目: | 2,143行目: | ||
$(selectID).children('.anr-opt-diff').prop('hidden', false); |
$(selectID).children('.anr-opt-diff').prop('hidden', false); |
||
$(selectID).children('.anr-opt-none').prop('hidden', false); |
$(selectID).children('.anr-opt-none').prop('hidden', false); |
||
$( |
$(idlinkDivID).css('display', 'block'); |
||
$( |
$(idlinkID).attr('href', mw.util.getUrl('Special:redirect/logid/' + logid)).text('特別:転送/logid/' + logid); |
||
toggleBlockStatusLink(inputID, true, true); |
|||
} |
|||
if ( |
if (Logids[inputVal] !== undefined) { |
||
$(inputID).val( |
$(inputID).val(Logids[inputVal]); // if the object knows the logid for the user, retrieve the data |
||
updateDropdown( |
updateDropdown(Logids[inputVal]); |
||
} else { |
} else { |
||
// if the object doesn't know the logid for the user, ask the API |
// if the object doesn't know the logid for the user, ask the API |
||
async function logidApi(){ |
|||
// Get logid from the API |
// Get logid from the API |
||
logid = await getLogid(inputVal); |
let logid = await getLogid(inputVal); |
||
// |
setTimeout(function(){ // Deliberately setting lag to prevent bugs |
||
// Check the obtained logid |
|||
alert('エラー\n\n取得可能なlogidが存在しません。Logidを手動で入力するか、type=diff または none を使用してください'); |
|||
if (logid === undefined) { // If undefined is returned, reject the checking of the checkbox |
|||
alert('エラー\n\n取得可能なlogidが存在しません。Logidを手動で入力するか、type=diff または none を使用してください'); |
|||
return; |
|||
$(checkboxID).prop('checked', false); |
|||
return; |
|||
} |
} else { // If a valid logid is returned |
||
// Set the logid to the input |
// Set the logid to the input |
||
$(inputID).val(logid); |
$(inputID).val(logid); |
||
// Push username and logid into object if it doesn't have them |
// Push username and logid into object if it doesn't have them |
||
if ( |
if (Logids[inputVal] === undefined) { |
||
Logids[inputVal] = logid; |
|||
} |
} |
||
if ( |
if (Logids[logid] === undefined) { |
||
Logids[logid] = inputVal; |
|||
} |
} |
||
// Update type dropdown |
|||
updateDropdown(logid); |
|||
} |
|||
}, 100); |
|||
} |
|||
logidApi(); |
|||
} |
} |
||
1,225行目: | 2,193行目: | ||
} else { // if the checkbox is unchecked |
} else { // if the checkbox is unchecked |
||
if ( |
if (Logids[inputVal] !== undefined) { |
||
$(inputID).val( |
$(inputID).val(Logids[inputVal]); // if the object knows the username for the logid, retrieve the data |
||
$(selectID).children('.anr-opt-UNL').prop('hidden', false).prop('selected', true); |
$(selectID).children('.anr-opt-UNL').prop('hidden', false).prop('selected', true); |
||
$(selectID).children('.anr-opt-User2').prop('hidden', false); |
$(selectID).children('.anr-opt-User2').prop('hidden', false); |
||
1,234行目: | 2,202行目: | ||
$(selectID).children('.anr-opt-diff').prop('hidden', true); |
$(selectID).children('.anr-opt-diff').prop('hidden', true); |
||
$(selectID).children('.anr-opt-none').prop('hidden', false); |
$(selectID).children('.anr-opt-none').prop('hidden', false); |
||
$( |
$(idlinkDivID).css('display', 'none'); |
||
toggleBlockStatusLink(inputID, false, false); |
|||
} else { |
} else { |
||
1,261行目: | 2,230行目: | ||
// When buttons are moused on and off |
// When buttons are moused on and off |
||
$(document). |
$(document).off('mouseover mouseleave', '.anr-modal-dialog form button') |
||
.on({ |
|||
'mouseover': function(e) { |
'mouseover': function(e) { |
||
e.target.style.borderColor = '#999999'; |
e.target.style.borderColor = '#999999'; |
||
1,269行目: | 2,239行目: | ||
} |
} |
||
}, '.anr-modal-dialog form button'); |
}, '.anr-modal-dialog form button'); |
||
// Copy VIP name |
|||
$(document).on('click', '.anr-viplist-btn', function() { |
|||
let vipSelectVal = $('#anr-viplist-select').find('option').filter(':selected').text().trim(); |
|||
let vipLink = '[[WP:VIP#' + vipSelectVal + ']]'; |
|||
if (vipSelectVal !== '[[Wikipedia:進行中の荒らし行為]]を検索') { |
|||
copyToClipboard(vipLink); |
|||
} else { |
|||
alert('ドロップダウンの値が選択されていません'); |
|||
} |
|||
}); |
|||
// Function to copy a string to the clipboard |
|||
function copyToClipboard(str) { |
|||
try { // This should handle browser incompatibility |
|||
let $temp = $('<input>'); |
|||
$('body').append($temp); // Create a temporarily hidden text field |
|||
$temp.val(str).select(); // Copy the text string into the field and select the text |
|||
document.execCommand('copy'); // Copy it to the clipboard |
|||
$temp.remove(); // Remove the text field |
|||
} |
|||
catch (err) { |
|||
alert('ご利用のブラウザはこの機能に対応していません'); |
|||
console.log(err); |
|||
} |
|||
} |
|||
// When the summary checkbox is (un)checked |
// When the summary checkbox is (un)checked |
||
$(document).on('change', '#anr-summary-checkbox', function(){ |
$(document).off('change', '#anr-summary-checkbox').on('change', '#anr-summary-checkbox', function(){ |
||
let $textarea = $('#anr-summary-text'); |
let $textarea = $('#anr-summary-text'); |
||
1,309行目: | 2,253行目: | ||
$textarea.css('display','none').val(''); |
$textarea.css('display','none').val(''); |
||
} |
} |
||
}); |
}); |
||
// When 'check block status before edit' is (un)checked |
|||
}); |
|||
$('#anr-blockstatus-checkbox').change(function(){ |
|||
if ($(this).is(':checked')) { // Box is checked |
|||
checkBlockStatusBeforeEdit = true; |
|||
} else { // Box is unchecked |
|||
checkBlockStatusBeforeEdit = false; |
|||
} |
|||
}); |
|||
// When 'check dpulicate reports before edit' is (un)checked |
|||
$('#anr-duplicatereport-checkbox').change(function(){ |
|||
if ($(this).is(':checked')) { // Box is checked |
|||
checkDuplicateReportsBeforeEdit = true; |
|||
} else { // Box is unchecked |
|||
checkDuplicateReportsBeforeEdit = false; |
|||
} |
|||
}); |
|||
}); // addPortletLink |
|||
// Function to get the last day of the month |
// Function to get the last day of the month |
||
1,318行目: | 2,284行目: | ||
} |
} |
||
/ |
/** |
||
* Function to get the current date and the section name to which to report |
|||
* @param {Boolean} last - if true, returns the name of the last section |
|||
function getSectionI(){ |
|||
* @returns {String} - section name |
|||
*/ |
|||
function getSectionI(last){ |
|||
let d = new Date(); |
let d = new Date(); |
||
if (last) { |
|||
let subtract = 5; |
|||
if (d.getDate() === 1 || d.getDate() === 2) { |
|||
subtract = 3; |
|||
} else if (d.getDate() === 31) { |
|||
subtract = 6; |
|||
} |
|||
d.setDate(d.getDate() - subtract); |
|||
} |
|||
let sectionName; |
|||
switch(true) { |
switch(true) { |
||
case (1 <= d.getDate() && d.getDate() <= 5): |
case (1 <= d.getDate() && d.getDate() <= 5): |
||
sectionName = `${d.getFullYear()}年${d.getMonth()+1}月1日 - 5日新規報告`; |
|||
break; |
break; |
||
case (6 <= d.getDate() && d.getDate() <= 10): |
case (6 <= d.getDate() && d.getDate() <= 10): |
||
sectionName = `${d.getFullYear()}年${d.getMonth()+1}月6日 - 10日新規報告`; |
|||
break; |
break; |
||
case (11 <= d.getDate() && d.getDate() <= 15): |
case (11 <= d.getDate() && d.getDate() <= 15): |
||
sectionName = `${d.getFullYear()}年${d.getMonth()+1}月11日 - 15日新規報告`; |
|||
break; |
break; |
||
case (16 <= d.getDate() && d.getDate() <= 20): |
case (16 <= d.getDate() && d.getDate() <= 20): |
||
sectionName = `${d.getFullYear()}年${d.getMonth()+1}月16日 - 20日新規報告`; |
|||
break; |
break; |
||
case (21 <= d.getDate() && d.getDate() <= 25): |
case (21 <= d.getDate() && d.getDate() <= 25): |
||
sectionName = `${d.getFullYear()}年${d.getMonth()+1}月21日 - 25日新規報告`; |
|||
break; |
break; |
||
case (26 <= d.getDate() && d.getDate() <= lastDay(d.getFullYear(), d.getMonth())): |
case (26 <= d.getDate() && d.getDate() <= lastDay(d.getFullYear(), d.getMonth())): |
||
sectionName = `${d.getFullYear()}年${d.getMonth()+1}月26日 - ${lastDay(d.getFullYear(), d.getMonth())}日新規報告`; |
|||
break; |
break; |
||
default: |
default: |
||
undefined; |
undefined; |
||
} |
} |
||
return |
return sectionName; |
||
} |
} |
||
1,355行目: | 2,336行目: | ||
// Function to check if a user exists locally |
// Function to check if a user exists locally |
||
async function userExists(username) { |
async function userExists(username) { |
||
return new Promise(function(resolve, reject) { |
return new Promise(function(resolve, reject) { |
||
new mw.Api().get({ |
new mw.Api().get({ |
||
action: 'query', |
'action': 'query', |
||
list: 'users', |
'list': 'users', |
||
ususers: username, |
'ususers': username, |
||
formatversion: 2 |
'formatversion': 2 |
||
}).done(function(res){ |
}).done(function(res){ |
||
if (res.query.users[0].userid !== undefined) { // If user exists |
if (res.query.users[0].userid !== undefined) { // If user exists |
||
1,372行目: | 2,353行目: | ||
// Function to manipulate dropdown options for UserAN types (also maniputes show/hide of checkbox) |
// Function to manipulate dropdown options for UserAN types (also maniputes show/hide of checkbox) |
||
let |
let updateTypeDropdownTimeout; |
||
function |
function updateTypeDropdown(inputID) { |
||
// Optional parameter: if false, the function doesn't select 'none' when random numbers or strings are typed into the input |
|||
if (setNone === undefined) { |
|||
setNone = true; |
|||
} |
|||
let tarVal = |
let tarVal = $(inputID).val().trim2(); // The value typed into the input |
||
let |
let selectID = inputID.replace('input', 'select'); // #anr-userX-select |
||
let |
let checkboxDivID = inputID.replace('input', 'checkbox-div'); // #anr-userX-checkbox-div |
||
let |
let checkboxID = inputID.replace('input', 'checkbox'); // #anr-userX-checkbox |
||
let idlinkDivID = inputID.replace('input', 'idlink-div'); // #anr-userX-idlink-div |
|||
clearTimeout( |
clearTimeout(updateTypeDropdownTimeout); // Run the async function only once when there's been no input change for 0.35 seconds |
||
updateTypeDropdownTimeout = setTimeout(async function(){ |
|||
if (tarVal === '') { // if the field is empty |
if (tarVal === '') { // if the field is empty |
||
$(selectID).prop('disabled', true); // |
$(selectID).prop('disabled', true).children('.anr-opt-none').prop('selected', true); // Disable dropdown and select 'none' |
||
$(checkboxDivID).css('display', 'none'); // |
$(checkboxDivID).css('display', 'none'); // Hide 'hide username' checkbox |
||
$(checkboxID).prop('checked', false); // |
$(checkboxID).prop('checked', false); // Uncheck the checkbox |
||
$(idlinkDivID).css('display', 'none'); // Hide logid/diff link |
|||
toggleBlockStatusLink(inputID, true, false); |
|||
} else { // if the field is filled |
} else { // if the field is filled |
||
1,402行目: | 2,381行目: | ||
$(selectID).children('.anr-opt-UNL').prop('hidden', true); |
$(selectID).children('.anr-opt-UNL').prop('hidden', true); |
||
$(selectID).children('.anr-opt-User2').prop('hidden', true); |
$(selectID).children('.anr-opt-User2').prop('hidden', true); |
||
$(selectID).children('.anr-opt-IP2').prop('hidden' |
$(selectID).children('.anr-opt-IP2').prop({'hidden': false, 'selected': true}); |
||
$(selectID).children('.anr-opt-logid').prop('hidden', true); |
$(selectID).children('.anr-opt-logid').prop('hidden', true); |
||
$(selectID).children('.anr-opt-diff').prop('hidden', true); |
$(selectID).children('.anr-opt-diff').prop('hidden', true); |
||
1,408行目: | 2,387行目: | ||
$(checkboxDivID).css('display', 'none'); // hide 'hide username' checkbox |
$(checkboxDivID).css('display', 'none'); // hide 'hide username' checkbox |
||
$(checkboxID).prop('checked', false); // uncheck the checkbox |
$(checkboxID).prop('checked', false); // uncheck the checkbox |
||
$( |
$(idlinkDivID).css('display', 'none'); |
||
toggleBlockStatusLink(inputID, false, false); |
|||
} else if (await userExists(tarVal)) { // if user |
} else if (await userExists(tarVal)) { // if user |
||
$(selectID).children('.anr-opt-UNL').prop('hidden' |
$(selectID).children('.anr-opt-UNL').prop({'hidden': false, 'selected': true}); |
||
$(selectID).children('.anr-opt-User2').prop('hidden', false); |
$(selectID).children('.anr-opt-User2').prop('hidden', false); |
||
$(selectID).children('.anr-opt-IP2').prop('hidden', true); |
$(selectID).children('.anr-opt-IP2').prop('hidden', true); |
||
1,420行目: | 2,400行目: | ||
$(checkboxDivID).css('display', 'block'); // show 'hide username' checkbox |
$(checkboxDivID).css('display', 'block'); // show 'hide username' checkbox |
||
$(checkboxID).prop('checked', false); // uncheck the checkbox |
$(checkboxID).prop('checked', false); // uncheck the checkbox |
||
$( |
$(idlinkDivID).css('display', 'none'); |
||
toggleBlockStatusLink(inputID, false, false); |
|||
} else { // if something else (like random numbers or strings) |
} else { // if something else (like random numbers or strings) |
||
1,429行目: | 2,410行目: | ||
$(selectID).children('.anr-opt-logid').prop('hidden', false); |
$(selectID).children('.anr-opt-logid').prop('hidden', false); |
||
$(selectID).children('.anr-opt-diff').prop('hidden', false); |
$(selectID).children('.anr-opt-diff').prop('hidden', false); |
||
$(selectID).children('.anr-opt-none').prop({'hidden': false, 'selected': true}); |
|||
$(selectID).children('.anr-opt-none').prop('hidden', false).prop('selected', true); |
|||
} else { |
|||
$(selectID).children('.anr-opt-none').prop('hidden', false); |
|||
} |
|||
$(checkboxDivID).css('display', 'none'); // hide 'hide username' checkbox |
$(checkboxDivID).css('display', 'none'); // hide 'hide username' checkbox |
||
toggleBlockStatusLink(inputID, true, false); |
|||
$(aDivID).css('display', 'none'); |
|||
} |
} |
||
1,445行目: | 2,421行目: | ||
} |
} |
||
/** |
|||
// Function to reset dropdown value to 'none' if input is blanked, and hide unnecessary things |
|||
* Function to show/hide 'This user has blocks' links |
|||
function resetDropdown(inputID, selectID) { |
|||
* @param {String} inputID - the ID of the input |
|||
* @param {Boolean} forceHide - if true, hide the block status link without API requests |
|||
* @param {Boolean} convertLogid - if true, try to convert a logid to a username |
|||
*/ |
|||
async function toggleBlockStatusLink(inputID, forceHide, convertLogid) { |
|||
let username = $(inputID).val().trim2(); // The value in the input |
|||
let $bsLinkDiv = $(inputID.replace('input', 'blockstatus-div')); // #anr-userX-blockstatus-div |
|||
let $bsLink = $(inputID.replace('input', 'blockstatus')); // #anr-userX-blockstatus |
|||
if (forceHide && convertLogid) { |
|||
if (Logids[username] !== undefined) { // If the object knows who that logid is for |
|||
username = Logids[username]; // Convert the logid to the username and continue to API requests |
|||
} else { // If the object doesn't know who that logid is for |
|||
$bsLinkDiv.css('display', 'none'); // Hide the link div and exit function |
|||
return; |
|||
} |
|||
} else if (forceHide) { // If forceHide is true, basically just hide the bsLink without API requests |
|||
$bsLinkDiv.css('display', 'none'); // Hide the link div |
|||
return; |
|||
let checkboxDivID = selectID.replace('select', 'checkbox-div'); // ID of div containing <input type="checkbox"> tag |
|||
let aDivID = selectID.replace('select', 'a-div'); // ID of div containing <a> tag |
|||
if (trimA($(inputID).val()) === '') { |
|||
$(selectID).children('.anr-opt-none').prop('selected', true); |
|||
$(checkboxDivID).css('display', 'none'); |
|||
$(aDivID).css('display', 'none'); |
|||
} |
} |
||
// Check the block status of the user, and if blocked, update the bsLink, or else, hide the link |
|||
let blocked = await getBlocked([username]); |
|||
if (blocked.length !== 0) { // If the user typed into the input is blocked |
|||
$bsLink.attr('href', mw.util.getUrl('特別:投稿記録/' + username)); // Update the link |
|||
$bsLinkDiv.css('display', 'block'); // Show the link div |
|||
} else { |
|||
$bsLinkDiv.css('display', 'none'); // Hide the link div |
|||
} |
|||
return; |
|||
} |
|||
// Function to get an array of blocked users & IPs from an array of random users & IPs |
|||
async function getBlocked(namesArr) { |
|||
let users = []; |
|||
let ips = []; |
|||
let blocked = []; |
|||
// Sort names to users and IPs |
|||
for (i = 0; i < namesArr.length; i++) { |
|||
if (mw.util.isIPAddress(namesArr[i], true)) { // Push IPs into the array |
|||
ips.push(namesArr[i]); |
|||
} else { // Push users into the array |
|||
users.push(namesArr[i]); |
|||
} |
|||
} |
|||
// Check who's blocked |
|||
if (users.length !== 0 && ips.length !== 0) { // If namesArr contains both users and IPs |
|||
blocked = blocked.concat(await getBlockedUsers(users), await getBlockedIps(ips)); |
|||
} else if (users.length !== 0) { // If namesArr only contains users |
|||
blocked = blocked.concat(await getBlockedUsers(users)); |
|||
} else if (ips.length !== 0) { // If namesArr only contains IPs |
|||
blocked = blocked.concat(await getBlockedIps(ips)); |
|||
} else { |
|||
return blocked; // [], empty array |
|||
} |
|||
// Sort the blocked in the original order |
|||
for (i = namesArr.length -1; i >= 0; i--) { |
|||
if (!isInArray(namesArr[i], blocked)) { // If not blocked |
|||
namesArr.splice(i, 1); // Remove the name from the array |
|||
} |
|||
} |
|||
return namesArr; |
|||
} |
|||
// Function to get an array of blocked users from an array of random users |
|||
async function getBlockedUsers(usersArr) { |
|||
let blockedArr = []; |
|||
// API query |
|||
return new Promise(function(resolve, reject) { |
|||
new mw.Api().get({ |
|||
'action': 'query', |
|||
'list': 'blocks', |
|||
'bklimit': usersArr.length, |
|||
'bkusers': usersArr.join('|'), |
|||
'bkprop': 'user', |
|||
'formatversion': 2 |
|||
}).done(function(res){ |
|||
let blockstatusApi = res.query.blocks; |
|||
if (blockstatusApi.length === 0) { // If none of the users is blocked |
|||
resolve(blockedArr); // Return [] |
|||
} else { // If there are one or more blocked users |
|||
for (let i = 0; i < blockstatusApi.length; i++) { |
|||
blockedArr.push(blockstatusApi[i].user); // Push blocked users into the array |
|||
} |
|||
resolve(blockedArr); // Return e.g. [user1, user2...] |
|||
} |
|||
}); |
|||
}); |
|||
} |
|||
// Function to get an array of blocked IPs from an array of random IPs |
|||
async function getBlockedIps(ipsArr) { |
|||
let blockedArr = []; |
|||
let blocked; |
|||
for (let i = 0; i < ipsArr.length; i++) { |
|||
blocked = await ipIsBlocked(ipsArr[i]); |
|||
if (blocked) { |
|||
blockedArr.push(ipsArr[i]); |
|||
} |
|||
if (i === ipsArr.length -1) { |
|||
return blockedArr; |
|||
} |
|||
} |
|||
} |
|||
// Function to check if a given IP is blocked (true if the IP is blocked and false if not) |
|||
async function ipIsBlocked(ip) { |
|||
return new Promise(function(resolve, reject) { |
|||
new mw.Api().get({ |
|||
'action': 'query', |
|||
'list': 'blocks', |
|||
'bklimit': 1, |
|||
'bkip': ip, // This parameter doesn't take multiple, pipe-separeted arguments, unlike the param 'bkusers' |
|||
'bkprop': 'user', // Get the blocked IP: Could be a range; Currently this value is not used in the function |
|||
'formatversion': 2 |
|||
}).done(function(res){ |
|||
if (res.query.blocks.length !== 0) { // If the IP is blocked |
|||
resolve(true); |
|||
} else { // If the IP is NOT blocked |
|||
resolve(false); |
|||
} |
|||
}); |
|||
}); |
|||
} |
} |
||
1,463行目: | 2,587行目: | ||
return new Promise(function(resolve, reject) { |
return new Promise(function(resolve, reject) { |
||
new mw.Api().get({ |
new mw.Api().get({ |
||
action: 'query', |
'action': 'query', |
||
list: 'logevents', |
'list': 'logevents', |
||
leuser: username, |
'leuser': username, |
||
ledir: 'newer', |
'ledir': 'newer', |
||
lelimit: 1, |
'lelimit': 1, |
||
formatversion: 2 |
'formatversion': 2 |
||
}).done(function(res){ |
}).done(function(res){ |
||
if (res.query.logevents.length === 0) { // If empty array is returned (=if no logevent exists) |
if (res.query.logevents.length === 0) { // If empty array is returned (=if no logevent exists) |
||
resolve( |
resolve(); |
||
} else { |
} else { |
||
resolve(res.query.logevents[0].logid); |
resolve(res.query.logevents[0].logid); |
||
1,483行目: | 2,607行目: | ||
return new Promise(function(resolve, reject) { |
return new Promise(function(resolve, reject) { |
||
new mw.Api().get({ |
new mw.Api().get({ |
||
action: 'query', |
'action': 'query', |
||
list: 'allrevisions', |
'list': 'allrevisions', |
||
arvprop: 'ids|timestamp', |
'arvprop': 'ids|timestamp', |
||
arvlimit: 5, |
'arvlimit': 5, |
||
arvuser: mw.config.get('wgUserName'), |
'arvuser': mw.config.get('wgUserName'), |
||
arvstart: curtimestamp, |
'arvstart': curtimestamp, |
||
arvdir: 'newer', |
'arvdir': 'newer', |
||
formatversion: 2 |
'formatversion': 2 |
||
}).done(function(res){ |
}).done(function(res){ |
||
if (res && res.query && res.query.allrevisions) { |
if (res && res.query && res.query.allrevisions) { |
||
1,499行目: | 2,623行目: | ||
} |
} |
||
if (i === revArr.length -1) { |
if (i === revArr.length -1) { |
||
resolve( |
resolve(); |
||
} |
} |
||
} |
} |
||
1,505行目: | 2,629行目: | ||
}); |
}); |
||
}); |
}); |
||
} |
} |
||
// Function to trim U+200E space |
|||
function trimA(str) { |
|||
return str.replace(/\u200e/g, '').trim(); |
|||
} |
|||
} // if (userIsInGruoup) |
|||
} |
|||
/** |
|||
// Function to check if an element is in an array |
|||
* Function to check if an element is in an array |
|||
* @param {String} el |
|||
* @param {Array} arr |
|||
* @returns {Boolean} |
|||
*/ |
|||
function isInArray (el, arr) { |
function isInArray (el, arr) { |
||
if (arr.indexOf(el) !== -1) { |
if (arr.indexOf(el) !== -1) { |
||
1,521行目: | 2,645行目: | ||
return false; |
return false; |
||
} |
} |
||
} |
|||
/** |
|||
* Function to check if elements of an array are all contained in another array |
|||
* @param {Array} arr1 |
|||
* @param {Array} arr2 |
|||
* @returns {Boolean} |
|||
*/ |
|||
function arrayIsInArray(arr1, arr2) { |
|||
for (let i = 0; i < arr1.length; i++) { |
|||
if (!isInArray(arr1[i], arr2)) { |
|||
return false; |
|||
} |
|||
} |
|||
return true; |
|||
} |
} |
||
/** |
|||
// Function to check what user group the current user belongs to |
|||
* Function to check what user group the CURRENT USER belongs to (thus it doesn't take a username parameter) |
|||
* @param {String} group - a group name such as 'autoconfirmed' and 'sysop' |
|||
* @returns {Boolean} |
|||
*/ |
|||
function userIsInGroup(group) { |
function userIsInGroup(group) { |
||
if (isInArray(group, mw.config.get('wgUserGroups'))) { |
if (isInArray(group, mw.config.get('wgUserGroups'))) { |
||
1,529行目: | 2,672行目: | ||
} else { |
} else { |
||
return false; |
return false; |
||
} |
|||
} |
|||
/** |
|||
* Function to check if a string contains a substring that is an element of an array |
|||
* @param {String} str |
|||
* @param {Array} arr |
|||
* @returns {Boolean} |
|||
*/ |
|||
function stringContainsElementInArray(str, arr) { |
|||
for (let i = 0; i < arr.length; i++) { |
|||
if (str.indexOf(arr[i]) !== -1) { |
|||
return true; |
|||
} |
|||
} |
|||
return false; |
|||
} |
|||
/** |
|||
* Function to count the number of an element in an array |
|||
* @param {String} el The array element to count |
|||
* @param {Array} arr The array in which to count the element |
|||
* @returns {integer} The number of the element in the array |
|||
*/ |
|||
function countElementsInArray(el, arr) { |
|||
const counts = {}; |
|||
arr.forEach(function(x) { |
|||
counts[x] = (counts[x] || 0) + 1; |
|||
}); |
|||
if (counts[el] === undefined) { |
|||
return 0; |
|||
} else { |
|||
return counts[el]; |
|||
} |
} |
||
} |
} |
2022年3月9日 (水) 07:34時点における版
/*************************************
* AN Reporter (ANR)
* Author: Dragoniez
* Version: 4.0
*************************************/
//<nowiki>
// Run the script only after Select2, jQuery UI, and the DOM are loaded
$.when(
$.getScript('https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js'),
mw.loader.using('jquery.ui'),
$.ready
).then(function(){
// Load CSS source for Select2
$('head')
.append($('<link />')
.attr({
'rel': 'stylesheet',
'href': 'https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css'
})
);
// Run the script only if the user is autoconfirmed and the page is not an edit page
if (userIsInGroup('autoconfirmed') && mw.config.get('wgAction') !== 'edit') {
// ********** UTILITY FUNCTIONS **********
/**
* String method to get rid of the U+200E space, in addition to the function of $.trim()
* @returns {String}
*/
String.prototype.trim2 = function() {
return this.replace(/\u200e/g, '').trim();
};
/**
* String method (alternative) to replace all occurences of a string with another
* (takes a replacer and a replacee as arguments)
* @returns {String}
*/
String.prototype.replaceAll2 = function() {
let replaced = '';
if (arguments.length %2 !== 0) {
return new Error('SyntaxError: replaceAll2 takes an even number of arguments.');
} else {
for (let i = 0; i < arguments.length; i = i + 2) {
if (i === 0) {
replaced = this.split(arguments[i]).join(arguments[i + 1]);
} else {
replaced = replaced.split(arguments[i]).join(arguments[i + 1]);
}
}
return replaced;
}
};
// Define the position of the '報告' button
let btnPosition;
let fSize; // Font size of the elements on the dialog
let s2fSize; // Font size of Select2 dropdown options
switch(mw.config.get('skin')) {
case 'vector':
case 'vector-2022':
btnPosition = 'p-views';
fSize = '80%';
s2fSize = '0.85em';
break;
case 'minerva':
btnPosition = 'p-personal';
fSize = '80%';
s2fSize = '0.85em';
break;
case 'monobook':
btnPosition = 'p-cactions';
fSize = '110%';
s2fSize = '1em';
break;
case 'timeless':
btnPosition = 'p-cactions';
fSize = '90%';
s2fSize = '0.9em';
break;
default:
btnPosition = 'p-cactions';
fSize = '80%';
s2fSize = '0.85em';
}
// Default CSS for Select2
$('head').append(
`<style>
.select2-selection__rendered {
padding: 1px 2px;
font-size: 1em;
line-height: normal !important;
}
.select2-results__option, .select2-results__group {
padding: 1px 8px;
font-size: ${s2fSize};
margin: 0;
}
.select2-container, .select2-selection--single {
height: auto !important;
}
</style>`
);
// Variables
let Logids = {}; // Object to store usernames and their corresponding logids
let checkBlockStatusBeforeEdit = true;
let checkDuplicateReportsBeforeEdit = true;
// Add ANR tab
$(mw.util.addPortletLink(btnPosition, '#', '報告', 'ca-anr', '管理者伝言板に利用者を報告', null, '#ca-move')) // '報告β' when debugging
.click(function(e){
// Cancel event that redirects the user to the href destination
e.preventDefault();
// ********** DIALOG CREATION **********
// CSS
const labelCSS =
'display: inline-block;' +
'width: 8ch;' ;
const marginCSS =
'margin: 1em 0;' ;
const siCSS = // For select and input
'border: 1px solid #d3d3d3;' +
'border-radius: 1%;' +
'background-color: white;' +
'padding: 2px 4px;' ;
const btnCSS = // For button
'color: black;' +
'font-weight: normal;' +
'border: 1px solid #d3d3d3;' +
'background-color: white;' +
'padding: 0.2em 0.5em;' +
'border-radius: 10%;' ;
/* Experimental design
const lsCSS = // Eliminate gap between inline-blocks
'letter-spacing: -1em;' +
'white-space: nowrap' ;
const rlsCSS = // Reset line-spacing in child elements
'letter-spacing: normal' ;
*/
// Page names
const ANI = 'Wikipedia:管理者伝言板/投稿ブロック';
const ANS = 'Wikipedia:管理者伝言板/投稿ブロック/ソックパペット';
const AN3RR = 'Wikipedia:管理者伝言板/3RR';
const Iccic = 'Wikipedia:進行中の荒らし行為/長期/Iccic/投稿ブロック依頼';
const ISECHIKA = 'Wikipedia:管理者伝言板/投稿ブロック/いせちか';
const KAGE = 'Wikipedia:管理者伝言板/投稿ブロック/影武者';
const KIYOSHIMA = 'Wikipedia:管理者伝言板/投稿ブロック/清島達郎';
const SHINJU = 'Wikipedia:管理者伝言板/投稿ブロック/真珠王子';
// Sections on WP:AN/I
let sectionsI =
//<div class="anr-section-div">
` <label for="anr-section-options-i" style="${labelCSS}">節</label>` +
` <select id="anr-section-options-i" style="${siCSS}">` +
` <option selected disabled hidden>選択してください</option>` +
` <option id="anr-section-options-i-date"></option>` +
` <option>不適切な利用者名</option>` +
` <option>公開アカウント</option>` +
` <option>公開プロキシ・ゾンビマシン・ボット・不特定多数</option>` +
` <option>犯罪行為またはその疑いのある投稿</option>` +
` </select>`;
//</div>
// Sections on WP:AN/S
let sectionsS =
//<div class="anr-section-div">
` <label for="anr-section-options-s" style="${labelCSS}">名称</label>` +
` <select id="anr-section-options-s" style="${siCSS}">` +
` <option selected disabled hidden>選択してください</option>` +
` <optgroup label="系列が立てられていないもの">` +
` <option>著作権侵害・犯罪予告</option>` +
` <option>名誉毀損・なりすまし・個人情報</option>` +
` <option>妨害編集・いたずら</option>` +
` <option>その他</option>` +
` </optgroup>` +
` <optgroup label="LTA">` +
` <option>声優・特撮関連荒らし系 (203)</option>` +
` <option>Audia3sb系 (3SB)</option>` +
` <option>愛知@nifty荒らし系(AICHI)</option>` +
` <option>愛の氷系 (AINO)</option>` +
` <option>秋田ぷらら可変IP系(AKITAPLALA)</option>` +
` <option>Akoyano系 (AKY)</option>` +
` <option>Asaklira系(ASA)</option>` +
` <option>麻原英太系 (ASACOV)</option>` +
` <option>アジアンビ系 (ASIANB)</option>` +
` <option>Bulut系 (Asperger、ASPE)</option>` +
` <option>백돌系(BAEG)</option>` +
` <option>ぼかんてぃん系(BOQ)</option>` +
` <option>ブリッ系 (BUR)</option>` +
` <option>Bz.i.yqs系(BZIYQS)</option>` +
` <option>中央アジア史サブスタブ濫造系(CASTUB)</option>` +
` <option>ダルメーター系(DARU)</option>` +
` <option>X-enon147系 (DOI)</option>` +
` <option>ドラえもん・ギャンブル関連のIP系(DORA)</option>` +
` <option>Ellsiemall系 (ELLS)</option>` +
` <option>イギリス可変IP系(ENS)</option>` +
` <option>放送局・送信所の記事で荒らしを行うIP(EOHS)</option>` +
` <option>EricNeedles3系 (ERIC3)</option>` +
` <option>IUCNレッドリスト関連荒らし系(FRL)</option>` +
` <option>Gamui系 (GAMUI)</option>` +
` <option>極楽サタン系 (GOKURAKU)</option>` +
` <option>Gordon S系(GORDON)</option>` +
` <option>Greaseno系 (GREA)</option>` +
` <option>Grimm系 (GRIMM)</option>` +
` <option>はー先輩系 (HAASEN)</option>` +
` <option>HAT系 (HAT)</option>` +
` <option>ヒースロー系 (HEATHROW)</option>` +
` <option>おぉたむすねィく探検隊系(HEBI)</option>` +
` <option>Hero123系 (HERO123)</option>` +
` <option>Hightechodap系 (HGTCHDP)</option>` +
` <option>Iccic系 (Iccic)</option>` + // Has an independent page
` <option>池沼ガイジ系 (IKE)</option>` +
` <option>いせちか系 (ISECHIKA)</option>` + // Has an independent page
` <option>天体名プロジェクト系(JANNET)</option>` +
` <option>Jj9系 (JJ9)</option>` +
` <option>課代さん系(KADAI)</option>` +
` <option>影武者系(KAGE)</option>` + // Has an independent page
` <option>かめでぃー系(KAMEDY)</option>` +
` <option>かなべえコバトン系 (KANAKOBA)</option>` +
` <option>Nbckfkh系(KFKH)</option>` +
` <option>清島達郎系 (清島、KIYOSHIMA)</option>` + // Has an independent page
` <option>木崎妃系 (KIZAKI)</option>` +
` <option>韓国KT系 (KKT)</option>` +
` <option>Masato Koizumi系(KOIZUMI、M.K.)</option>` +
` <option>Konbudon系(KONBU)</option>` +
` <option>テレビ局関連記事を荒らす韓国IP系(KORTV)</option>` +
` <option>久保帯人関連荒らし系 (KUBOREL)</option>` +
` <option>M21系 (M21)</option>` +
` <option>MASA系 (Mr.ちゅらさん、CHURASAN、MASA)</option>` +
` <option>マヤオ系 (MAYAO)</option>` +
` <option>Mikihisa系(MIKI)</option>` +
` <option>Milky palace系 (Milky)</option>` +
` <option>水戸ソフトバンク可変IP系 (MITO)</option>` +
` <option>猛烈な勢いで赤リンクを無差別除去するアカウント群系(MOUAKA)</option>` +
` <option>MShared系 (MShared)</option>` +
` <option>名取の納豆系(NATO)</option>` +
` <option>Die ndbtk系 (NDBTK)</option>` +
` <option>NoSaito・みそかつおにんにく系 (NMT)</option>` +
` <option>NODA系 (NODA)</option>` +
` <option>Notsu (NOTSU)</option>` +
` <option>カテゴリ・リダイレクト・サブスタブ濫造を行うIP系(NTTPC)</option>` +
` <option>(内部リンク除去)大阪ZAQ可変IP系(OSAKAZAQ)</option>` +
` <option>親子他人丼系(OYAKO)</option>` +
` <option>Pingpongpang (PPP)</option>` +
` <option>川野名 倫系(RIN、DEARU)</option>` +
` <option>さんさんさんさん系 (SAN)</option>` +
` <option>詐称コピペ系 (SASHO)</option>` +
` <option>沙耶奈系(SAYANA)</option>` +
` <option>整数関連荒らしIP系 (SEISU)</option>` +
` <option>荒らし自己差し戻しIP系 (SELFREVERT)</option>` +
` <option>真珠王子系(SHINJU)</option>` + // Has an independent page
` <option>すらいむさん系(SLIME)</option>` +
` <option>新川温泉系 (SNKW)</option>` +
` <option>ソウ系(SOH)</option>` +
` <option>埼玉楽天モバイルIP系 (STRM)</option>` +
` <option>Suzukitaro系 (ツバル、SUZU)</option>` +
` <option>Syun respect for music系 (SYUN)</option>` +
` <option>涼宮ハルヒ20062009系 (SZMY)</option>` +
` <option>TANS系 (TANS)</option>` +
` <option>ゼロタロス系 (TAROSU)</option>` +
` <option>多摩ケーブルネットワークIP系 (T-NET)</option>` +
` <option>若いナマケモノは不要系(WAK)</option>` +
` <option>ホワイト・ジャック系 (カダフィ元帥、WHITE)</option>` +
` <option>Wpcon abuse系 (WPCON)</option>` +
` <option>Yanajin33系(YAN)</option>` +
` <option>揶揄リダイレクト作成荒らし系(YAYURE)</option>` +
` <option>黄色関係のIP系 (YELLOW)</option>` +
` <option>Yqm系(YQM)</option>` +
` <option>隊士蘭堂系</option>` +
` </optgroup>` +
` </select>`;
//</div>
// Username input
let userHtml =
//<div class="anr-user-div">
` <div id="anr-user1-input-div">` +
` <label for="anr-user1-input" style="${labelCSS}">利用者</label>` +
` <input id="anr-user1-input" style="width: 34ch; ${siCSS}">` +
` <select disabled id="anr-user1-select" style="${siCSS}">` +
` <option class="anr-opt-UNL">UNL</option>` +
` <option class="anr-opt-User2">User2</option>` +
` <option class="anr-opt-IP2">IP2</option>` +
` <option class="anr-opt-logid">logid</option>` +
` <option class="anr-opt-diff">diff</option>` +
` <option selected class="anr-opt-none">none</option>` +
` </select>` +
` </div>` +
` <div id="anr-user1-checkbox-div" style="display: none;">` +
` <label class="anr-emptylabel" style="${labelCSS}"></label>` +
` <input type="checkbox" id="anr-user1-checkbox">` +
` <label for="anr-user1-checkbox">利用者名を隠す</label>` +
` </div>` +
` <div id="anr-user1-idlink-div" style="display: none;">` +
` <label for="anr-user1-idlink" style="${labelCSS}"></label>` +
` <a id="anr-user1-idlink" href="" target="_blank"></a>` +
` </div>` +
` <div id="anr-user1-blockstatus-div" style="display: none;">` +
` <label for="anr-user1-blockstatus" style="${labelCSS}"></label>` +
` <a id="anr-user1-blockstatus" href="" target="_blank" style="color: MediumVioletRed;">ブロックあり</a>` +
` </div>`;
//</div>
// The whole html contour
let modalHtml =
`<div class="anr-modal-dialog" title="AN Reporter" style="max-height: 80vh; min-width: 54ch;">` +
` <div class="anr-modal-header">` +
` <h2>利用者を報告</h2>` +
` </div>` +
` <div class="anr-modal-body">` +
` <form>` +
` <div class="anr-target-div" style="${marginCSS}">` +
` <label for="anr-target-options" style="${labelCSS}">報告先</label>` +
` <select id="anr-target-options" style="${siCSS}">` +
` <option selected disabled hidden>選択してください</option>` +
` <option>${ANI}</option>` +
` <option>${ANS}</option>` +
` <option>${AN3RR}</option>` +
` </select>` +
` <div class="anr-target-pagelink-div" style="display: none;">` +
` <label class="anr-emptylabel" for="anr-target-pagelink" style="${labelCSS}"></label>` +
` <a id="anr-target-pagelink" href="" target="_blank">報告先を確認</a>` +
` </div>` +
` </div>` +
` <div class="anr-section-div" style="${marginCSS} display: none;">` +
// sectionsX +
` </div>` +
` <div class="anr-user-div" style="${marginCSS}">` +
userHtml +
` <div class="anr-btn-div">` +
` <button type="button" class="anr-addBtn" style="${btnCSS}">追加</button>` +
` </div>` +
` </div>` +
` <div class="anr-reason-div" style="${marginCSS}">` +
` <label for="anr-reason-text" style="${labelCSS}">理由</label>` +
` <textarea id="anr-reason-text" rows="8" style="width: 100%"></textarea>` +
` </div>` +
` <div class="anr-summary-div" style="${marginCSS}">` +
` <input id="anr-summary-checkbox" type="checkbox">` +
` <label for="anr-summary-checkbox">要約を指定</label>` +
` <textarea id="anr-summary-text" rows="3" style="width: 100%; display: none;"></textarea>` +
` </div>` +
` <div class="anr-checkbox-div" style="${marginCSS}">` +
` <input checked id="anr-blockstatus-checkbox" type="checkbox">` +
` <label for="anr-blockstatus-checkbox">報告前にブロック状態をチェック</label>` +
` <br>` +
` <input checked id="anr-duplicatereport-checkbox" type="checkbox">` +
` <label for="anr-duplicatereport-checkbox">報告前に重複報告をチェック</label>` +
` </div>` +
` </form>` +
` </div>` +
`</div>`;
// Add the frame div to the page
$('body').append(modalHtml);
// Script ad for edit summary
const scriptAd = ' ([[User:Dragoniez/AN Reporter|AN Reporter]])';
//const scriptAd = ' ([[User:Dragoniez/AN Reporter|AN Reporter Experimental]])'; // For debugging
// Show dialog
$('.anr-modal-dialog').dialog({
'resizable': false,
'height': 'auto',
'width': 'auto',
'modal': true,
'position': { my: 'center', at: 'top', of: window },
'open': function(){
// CSS
dialogCSS();
// Show VIP list
VIPList();
// Initialize variables
checkBlockStatusBeforeEdit = true;
checkDuplicateReportsBeforeEdit = true;
/* NOTE: The value of Logids is inherited from before reopening the dialog.
* If any bug is reported, consider resetting the variable here as well. */
// Get the name of the user to report if it can be retrieved from the page
let username = mw.config.get('wgRelevantUserName'); // Note: This does not pick up IP ranges
// Workaround to pick up IP ranges
if (username === null && mw.config.get('wgCanonicalSpecialPageName') === 'Contributions') {
let relUsername = $('#firstHeading').text().replace('の投稿記録', '');
if (mw.util.isIPAddress(relUsername, true)) {
username = relUsername;
}
}
// Exit function if the current user is on his/her own page or username has remained undefined or null
if (!username || username === mw.config.get('wgUserName')) {
return;
}
/* Initialize the username input and type dropdown
* Note: The function below is partially a duplicate of updateTypeDropdown(), but deliberately coded that way
* to prevent bugs and eliminate unnecessary API requests */
async function initializeTypeDropdown() {
let inputID = '#anr-user1-input';
let selectID = '#anr-user1-select';
let checkboxDivID = '#anr-user1-checkbox-div';
$(inputID).val(username); // Fill the input with the username
$(selectID).prop('disabled', false); // enable dropdown
if (mw.util.isIPAddress(username, true)) { // if IP
$(selectID).children('.anr-opt-UNL').prop('hidden', true);
$(selectID).children('.anr-opt-User2').prop('hidden', true);
$(selectID).children('.anr-opt-IP2').prop({'hidden': false, 'selected': true});
$(selectID).children('.anr-opt-logid').prop('hidden', true);
$(selectID).children('.anr-opt-diff').prop('hidden', true);
$(selectID).children('.anr-opt-none').prop('hidden', false);
$(checkboxDivID).css('display', 'none'); // hide 'hide username' checkbox
await toggleBlockStatusLink(inputID, false, false);
} else { // if user
$(selectID).children('.anr-opt-UNL').prop({'hidden': false, 'selected': true});
$(selectID).children('.anr-opt-User2').prop('hidden', false);
$(selectID).children('.anr-opt-IP2').prop('hidden', true);
$(selectID).children('.anr-opt-logid').prop('hidden', true);
$(selectID).children('.anr-opt-diff').prop('hidden', true);
$(selectID).children('.anr-opt-none').prop('hidden', false);
$(checkboxDivID).css('display', 'block'); // show 'hide username' checkbox
await toggleBlockStatusLink(inputID, false, false);
}
}
initializeTypeDropdown();
},
'buttons': [{
'text': 'プレビュー',
'click': previewBtn
}, {
'text': '報告',
'click': reportBtn
}]
});
// ********** EVENT HANDLERS AND FUNCTIONS **********
// Function to change the CSS of the dialog
function dialogCSS() {
$('.ui-dialog-content, .ui-dialog-buttonpane, .ui-corner-all, .ui-draggable, .ui-resizable').css('background', '#FFF0E4');
$('.ui-button').css({
'color': 'black',
'background-color': 'white'
});
$('.ui-dialog-titlebar, .ui-dialog-titlebar-close').attr('style', 'background: #FEC493 !important;');
$('.ui-dialog').css('font-size', fSize);
}
// WP:VIP list (for copy to clipboard)
async function VIPList() {
return new Promise(function(resolve, reject) {
new mw.Api().get({
'action': 'parse',
'page': 'Wikipedia:進行中の荒らし行為',
'prop': 'sections',
'formatversion': '2'
}).then(function(res){
if (res && res.parse) {
// Get VIP's names
const sectionInfo = res.parse.sections;
const excludeList = [
'記述について',
'急を要する二段階',
'配列',
'ブロック等の手段',
'このページに利用者名を加える',
'注意と選択',
'警告の方法',
'未登録(匿名・IP)ユーザーの場合',
'登録済み(ログイン)ユーザーの場合',
'警告中',
'関連項目'
];
let vipList = [];
for (let i = 0; i < sectionInfo.length; i++) {
if (!isInArray(sectionInfo[i].line, excludeList) && sectionInfo[i].level === '3') {
vipList.push(`<option>${sectionInfo[i].line}</option>`);
}
}
// Show the VIP list on the dialog
const VIPListHtml =
'<div class="anr-viplist-div" style="width: 100%;">' +
' <select id="anr-viplist-select">' +
' <option selected disabled hidden>[[Wikipedia:進行中の荒らし行為]]を検索</option>' +
vipList.join() +
' </select>' +
` <button type="button" class="anr-viplist-btn" style="${btnCSS}">コピー</button>` +
'</div>';
$('.anr-reason-div').before(VIPListHtml);
$('#anr-viplist-select').select2();
resolve();
} else {
resolve();
}
}).catch(function(){
resolve();
});
});
}
// Copy a VIP name when the 'copy' button is hit
$(document).off('click', '.anr-viplist-btn').on('click', '.anr-viplist-btn', function() {
const vipSelectVal = $('#anr-viplist-select').find('option').filter(':selected').text().trim();
const vipLink = '[[WP:VIP#' + vipSelectVal + ']]';
if (vipSelectVal !== '[[Wikipedia:進行中の荒らし行為]]を検索') {
copyToClipboard(vipLink);
} else {
alert('ドロップダウンの値が選択されていません');
}
});
// Function to copy a string to the clipboard
function copyToClipboard(str) {
try { // This should handle browser incompatibility
let $temp = $('<input>');
$('body').append($temp); // Create a temporarily hidden text field
$temp.val(str).select(); // Copy the text string into the field and select the text
document.execCommand('copy'); // Copy it to the clipboard
$temp.remove(); // Remove the text field
}
catch (err) {
alert('ご利用のブラウザはこの機能に対応していません');
console.log(err);
}
}
// Function to check information typed into the form
function editPrep() {
let rqFieldsEmpty = false;
// Check if at least one username is given and get users to report (and their UserAN types)
let users = [];
let types = [];
let duplicates = [];
let usernameInInput = '';
let selectedType = '';
let tempUsername = ''; // An escape hatch for username (for t=logid)
for (let i = 1; i < Infinity; i++) { // Loop through all inputs
if ($(`#anr-user${i}-input`).length === 0) { // if selector is not found
break; // exit for
} else { // if selector is found
usernameInInput = $(`#anr-user${i}-input`).val().trim2();
selectedType = $(`#anr-user${i}-select`).children('option').filter(':selected').text();
if (usernameInInput !== '') { // if input is not empty
// If t=logid, necessary to check if Logids has a username corresponding to the logid
if (selectedType === 'logid' && Logids[usernameInInput] !== undefined) {
// If the object has a username/logid pair, get username from the logid
tempUsername = Logids[usernameInInput];
// If either of the username or the logid is already in the array 'users',
// and if they have yet to be listed as duplicates
if ((isInArray(usernameInInput, users) || isInArray(tempUsername, users)) &&
!isInArray(usernameInInput, duplicates) && !isInArray(tempUsername, duplicates)) {
// List both the username and the logid as duplicates
duplicates.push(usernameInInput, tempUsername);
}
} else { // If logids are irrelevant
// If the username is already in the array 'users' and hasn't been listed as a duplicate
if (isInArray(usernameInInput, users) && !isInArray(usernameInInput, duplicates)) {
// List the username as a duplicate
duplicates.push(usernameInInput);
}
}
users.push(usernameInInput); // Push the username into the array
types.push(selectedType); // Push the UserAN type into the array
}
}
// Reset variables
tempUsername = '';
}
// Get the name of the section to edit
let pageToEdit = $('#anr-target-options').children('option').filter(':selected').text();
let sectionToEdit = '選択してください';
const ANSOptSelected = $('#anr-section-options-s').find('option').filter(':selected').text();
let reportToANS = false;
if (pageToEdit === ANI) { // If WP:AN/I is selected as the target page to edit
sectionToEdit = $('#anr-section-options-i').children('option').filter(':selected').text();
// Update the target section for cases in which the date has changed since the date-dependent section was chosen
if (sectionToEdit.match(/^\d{4}年\d{1,2}月\d{1,2}日 - \d{1,2}日新規報告$/) !== null) {
sectionToEdit = getSectionI(false);
$('#anr-section-options-i-date').text(getSectionI(false));
}
} else if (pageToEdit === ANS) { // If WP:AN/S is selected as the target page to edit
reportToANS = true;
switch(ANSOptSelected) {
case 'Iccic系 (Iccic)':
pageToEdit = Iccic;
sectionToEdit = '新規依頼';
break;
case 'いせちか系 (ISECHIKA)':
pageToEdit = ISECHIKA;
sectionToEdit = '新規依頼';
break;
case '影武者系(KAGE)':
pageToEdit = KAGE;
sectionToEdit = '新規依頼';
break;
case '清島達郎系 (清島、KIYOSHIMA)':
pageToEdit = KIYOSHIMA;
sectionToEdit = '新規依頼';
break;
case '真珠王子系(SHINJU)':
pageToEdit = SHINJU;
sectionToEdit = '新規依頼';
break;
default:
sectionToEdit = ANSOptSelected;
}
} else if (pageToEdit === AN3RR) { // If WP:AN/3RR is selected as the target page to edit
sectionToEdit = '3RR';
}
// Check if necessary fields are filled
if (
pageToEdit === '選択してください' || // The page dropdown's remained 選択してください
sectionToEdit === '選択してください' || // The section dropdown's remained 選択してください
$('#anr-reason-text').val().trim2() === '' || // No reason is given
users.length === 0 // No username is given
) {
rqFieldsEmpty = true;
}
// UserAN template and the reason of the report
const UserAN = '{{UserAN|t=TYPE|USER}}';
let reason = $('#anr-reason-text').val().trim2();
if (reason.substring(reason.length - 4) !== '~~~~') { // If reason doesn't contain signature, add one
reason = reason + '--~~~~';
}
// Get edit summary
const editSummarySection = '/*' + sectionToEdit + '*/';
let editSummary =
$('#anr-summary-text').val().trim2() === '' ?
editSummarySection + genEditSummary().replace(' - ', '') + scriptAd:
editSummarySection + $('#anr-summary-text').val().trim2() + scriptAd;
// Get text to add to the page
let textToSubmit = '';
if (users.length < 2) { // If user to report is just one
textToSubmit = '\* ' + UserAN.replaceAll2('TYPE', types[0], 'USER', users[0]) + ' - ' + reason;
} else { // If two or more
for (let i = 0; i < users.length; i++) {
textToSubmit += '\* ' + UserAN.replaceAll2('TYPE', types[i], 'USER', users[i]) + '\n';
}
textToSubmit += ': ' + reason;
}
// Return values
return {
'users': users,
'types': types,
'duplicates': duplicates,
'pageToEdit': pageToEdit,
'sectionToEdit': sectionToEdit,
'reportToANS': reportToANS,
'rqFieldsEmpty': rqFieldsEmpty,
'editSummary': editSummary,
'textToSubmit': textToSubmit
}
}
// Function for the 'preview' button of the dialog
function previewBtn() {
// Check if the necessary fields are filled and get edit information
let ep = editPrep();
if (ep.rqFieldsEmpty) {
alert('必須項目が入力・選択されていません'); // Show error and cancel the preview
return;
}
// If the inputs have duplicates in them
if (ep.duplicates.length !== 0) {
let confirmMsg =
'以下の利用者について、重複入力がある可能性があります。\n\n' +
ep.duplicates.join(', ') + '\n\n' +
'プレビューを続行する場合は OK を、フォームに戻る場合は Cancel を押してください';
if (confirm(confirmMsg) === false) { // If cancelled
return;
}
}
// Preview dialog contour
const ANSMisc = `<a href="${mw.util.getUrl('WP:AN/S#OTH')}" target="_blank">WP:AN/S#その他</a>`;
const previewDiv =
'<div class="anr-preview-dialog" title="AN Reporter Preview">' +
' <div id="anr-preview-header" style="padding: 0.5em;">' +
' <p id="anr-preview-loading">' +
` プレビューを読み込み中${toggleLoadingSpinner('add')}` +
' </p>' +
' <p id="anr-preview-warning" style="display: none;">' +
' 注意1: このプレビュー上のリンクは全て新しいタブで開かれます' +
' <br>' +
` 注意2: 報告先が ${ANSMisc} の場合、このプレビューには表示されませんが「他X月X日」のヘッダーは必要に応じて自動挿入されます` +
' </p>' +
' </div>' +
' <div id="anr-preview-body" style="display: none; font-size: 1.1em; padding-top: 1em; border-top: 1px solid silver;">' +
' <div id="anr-preview-text" style="border: 1px solid silver; padding: 0.2em 0.5em; background: white;">' +
// previewHtml
' </div>' +
' <div id="anr-preview-summary" style="margin-top: 0.8em; border: 1px solid silver; padding: 0.2em 0.5em; background: white;">' +
// summaryHtml
' </div>' +
' </div>' +
'</div>';
// Show preview dialog
$('body').append(previewDiv);
$('.anr-preview-dialog').dialog({
'height': 'auto',
'width': $('#content').width() * 0.8,
'modal': true,
'position': { my: 'center', at: 'top', of: window },
'open': async function(){
// CSS
dialogCSS();
// Convert text on the dialog to html
const parsed = await convertWikitextToHtml(ep.textToSubmit, ep.editSummary);
if (parsed) {
let previewHtml = parsed.htmltext;
let summaryHtml = parsed.htmlsummary.replaceAll2('API', ep.pageToEdit);
$('#anr-preview-text').append(previewHtml);
$('#anr-preview-summary').append(summaryHtml);
$('.autocomment a').css('color', 'gray'); // Change color of section spec in summary
$('.anr-preview-dialog a').attr('target', '_blank'); // Open all links on a new tab
$('#anr-preview-body').css('display', 'block');
$('#anr-preview-loading').remove();
$('#anr-preview-warning').css('display', 'inline');
} else {
$('#anr-preview-loading').text('プレビューの読み込みに失敗しました').css('color', 'MediumVioletRed');
setTimeout(function(){
$('.anr-preview-dialog').dialog('close');
}, 5000);
}
},
'buttons': [{
'text': '閉じる',
'click': function(){
$(this).dialog('close');
}
}]
});
}
/**
* Convert wikitext to its HTML format
* @param {String} wikitext The wikitext to convert
* @param {String} wikisummary The summary to convert
* @returns {{htmltext: string, htmlsummary: string}}
*/
async function convertWikitextToHtml(wikitext, wikisummary) {
return new Promise(function(resolve, reject) {
$.ajax({
'url': mw.util.wikiScript('api'),
'data': {
'format': 'json',
'action': 'parse',
'text': wikitext,
'summary': wikisummary,
'contentmodel': 'wikitext',
'prop': 'text',
'disableeditsection': true
},
'dataType': 'json',
'type': 'POST',
success: function(res) {
resolve({
'htmltext': res.parse.text['*'],
'htmlsummary': res.parse.parsedsummary['*']
});
},
error: function(err) {
console.log(err);
resolve();
}
});
});
}
// Function for the 'report' button of the dialog
function reportBtn() {
// Check if the necessary fields are filled and get edit information
let ep = editPrep();
if (ep.rqFieldsEmpty) {
alert('必須項目が入力・選択されていません'); // Show error and cancel the edit
return;
}
// If the inputs have duplicates in them
if (ep.duplicates.length !== 0) {
let confirmMsg =
'以下の利用者について、重複入力がある可能性があります。\n\n' +
ep.duplicates.join(', ') + '\n\n' +
'報告を続行する場合は OK を、フォームに戻る場合は Cancel を押してください';
if (confirm(confirmMsg) === false) { // If cancelled
return;
}
}
// Variables for edit
let $dialog = $('.anr-modal-dialog');
let msgEditing = '<div class="anr-editing" />';
let msgDone = ''; // Message to show when edit attempt is done
let wikiPagename = ep.pageToEdit + '#' + ep.sectionToEdit; // The wiki pagename for link
let editFailed = false; // Boolean value to pass to function when edit attempt is done
// Change dialog content
$dialog.find('form').css('display', 'none'); // Hide dialog content
$dialog.dialog({'buttons': [] }); // Hide the button
$dialog.append(msgEditing);
// For debugging
//ep.pageToEdit = '利用者:Dragoniez/test';
//ep.editSummary = 'Test edit via mediawiki API' + scriptAd;
/**
* Function to check block status before edit
* @returns {Array} [] if no one is blocked, [user1, user2...] if someone is blocked
*/
async function preeditBlockStatusQuery() {
// Update message on the dialog
msgEditing =
`<p>報告対象者のブロック情報を取得しています${toggleLoadingSpinner('add')}</p>`;
$('.anr-editing').append(msgEditing);
// Extract users and IPs from the array
let tar = [];
for (let i = 0; i < ep.users.length; i++) {
switch(ep.types[i]) {
case 'UNL':
case 'User2':
case 'IP2':
if (!isInArray(ep.users[i], tar)) {
tar.push(ep.users[i]);
}
break;
case 'logid':
if (Logids[ep.users[i]] !== undefined) { // If the logid can be converted to a username
if (!isInArray(Logids[ep.users[i]], tar)) {
tar.push(Logids[ep.users[i]]); // Push username instead of logid
}
}
break;
case 'diff':
case 'none':
// Do nothing
}
}
// Check if any of the users is blocked
const blocked = await getBlocked(tar);
// If any of the users is blocked
if (blocked.length !== 0) {
// Update message on the dialog
msgEditing =
toggleLoadingSpinner('remove') +
`<p style="color: MediumVioletRed">ブロック済みの利用者を検出しました</p>`;
$('.anr-editing').append(msgEditing);
let inputVal;
let $bsLinkDiv;
let $bsLink;
// Update dialog
for (let i = 1; i < Infinity; i++) { // Loop through all inputs
if ($(`#anr-user${i}-input`).length === 0) { // if selector is not found
break; // exit for
} else { // if selector is found
inputVal = $(`#anr-user${i}-input`).val().trim2();
$bsLinkDiv = $(`#anr-user${i}-blockstatus-div`);
$bsLink = $(`#anr-user${i}-blockstatus`);
$bsLinkDiv.css('display', 'none'); // Temporarily hide the div
if (isInArray(inputVal, blocked)) {
$bsLink.attr('href', mw.util.getUrl('特別:投稿記録/' + inputVal));
$bsLinkDiv.css('display', 'block');
}
}
}
} else {
// Update message on the dialog
msgEditing =
toggleLoadingSpinner('remove') +
`<p style="color: MediumSeaGreen">ブロック済みの利用者は検出されませんでした</p>`;
$('.anr-editing').append(msgEditing);
}
return blocked;
}
/**
* Function to check duplicate reports
* @returns {{wikitext: string, reportees: Array}} wikitext === null if sections aren't found, wikitext === ''
* if there's no duplicate, wikitext === SECTIONTEXT to fetch preview from if there's any. If SECTIONTEXT
* is returned, reportees === [reportee1, reportee2...], without logids that can be converted to usernames.
*/
async function preeditDuplicateReportQuery() {
// Update message on the dialog
msgEditing =
`<p>重複報告情報を取得しています${toggleLoadingSpinner('add')}</p>`;
$('.anr-editing').append(msgEditing);
// Get sections and the whole wikitext of the page to which to report
async function getWikitextAndSectionInfo(){
return new Promise(function(resolve) {
new mw.Api().get({
'action': 'parse',
'page': ep.pageToEdit, // May need to change this parameter for debugging
'prop': 'wikitext|sections',
'formatversion': '2'
}).then(function(res){
resolve({
'sections': res.parse.sections,
'wikitext': res.parse.wikitext
});
});
});
}
const parsed = await getWikitextAndSectionInfo();
let sections = parsed.sections; // Array of objects
let wikitext = parsed.wikitext; // String
let sectiontitles = [];
let sectionheaders = ['']; // Array of equal-enclosed section headers (e.g. == SECTION ==)
// Note: the top section has no header, thus arr[0] = ''
// Get section titles and their corresponding equal-enclosed wikitext
for (let i = 0; i < sections.length; i++) {
// The transcluded '新規依頼' sections on WP:AN/S are duplicate names, hence shouldn't be included in the array
if (sections[i].index.indexOf('T') === -1) { // If the section is not loaded from another page
sectiontitles.push(sections[i].line); // Get section titles
switch(sections[i].level) { // Get equal-enclosed section headers
case '2':
sectionheaders.push('== ' + sections[i].line + ' ==');
break;
case '3':
sectionheaders.push('=== ' + sections[i].line + ' ===');
break;
case '4':
sectionheaders.push('==== ' + sections[i].line + ' ====');
break;
case '5':
sectionheaders.push('===== ' + sections[i].line + ' =====');
break;
}
}
}
// The sections in which to search for duplicate reports
let tarSections;
const tarSectionsI = [
getSectionI(true),
getSectionI(false),
'不適切な利用者名',
'公開アカウント',
'公開プロキシ・ゾンビマシン・ボット・不特定多数',
'犯罪行為またはその疑いのある投稿'
];
const tarSectionsS = [
'著作権侵害・犯罪予告',
'名誉毀損・なりすまし・個人情報',
'妨害編集・いたずら',
'その他',
ep.sectionToEdit
];
const tarSections3RR = ['3RR'];
const tarSectionsSubpagedLTA = ['新規依頼'];
switch(ep.pageToEdit) {
case ANI:
tarSections = tarSectionsI;
break;
case ANS:
tarSections = tarSectionsS;
break;
case AN3RR:
tarSections = tarSections3RR;
break;
case Iccic:
case ISECHIKA:
case KAGE:
case KIYOSHIMA:
case SHINJU:
tarSections = tarSectionsSubpagedLTA;
break;
default: // For debugging, corresponding to the sections on [[User:Dragoniez/test]]
tarSections = tarSectionsS;
}
// Check if the target sections exist
if (!arrayIsInArray(tarSections, sectiontitles)) {
sectionNotFound();
return {'wikitext': null};
}
// Separate the whole text into an array of sections
let sectionsPiped = sectiontitles.join('|').replaceAll2('(', '\\(', ')', '\\)', '.', '\\.');
let regex = new RegExp(`={2,5}\\s*(?:${sectionsPiped})\\s*={2,5}`, 'g');
let sectionContent = wikitext.split(regex); // Array of the content of each section, without section headers
// Re-add to sectionContent the '== TITLE ==' header that's been removed by the split() above
for (let i = 0; i < sectionContent.length; i++) {
sectionContent[i] = sectionheaders[i] + sectionContent[i];
}
// Remove irrelevant sections from the array of the content of each section (sectionContent)
sectionsPiped = tarSections.join('|').replaceAll2('(', '\\(', ')', '\\)', '.', '\\.');
regex = new RegExp(`={2,5}\\s*(?:${sectionsPiped})\\s*={2,5}`, 'g');
for (let i = sectionContent.length -1; i >= 0; i--) {
if (sectionContent[i].search(regex) === -1) {
sectionContent.splice(i, 1);
}
}
console.log('The contents of each section:');
console.log(sectionContent);
// Get all the reportees in the inputs, and if user, try to get logids
let reportees = [];
let logid;
for (let i = 0; i < ep.users.length; i++) { // Loop through all the usernames typed into the inputs
let user = ep.users[i];
let type = ep.types[i];
if (type === 'UNL' || type === 'User2') {
if (!isInArray(user, reportees)) { // If not already in the array
if (Logids[user] !== undefined) { // If the code already knows the logid for the user
reportees.push(user, Logids[user]); // Push the username and the logid into the array
} else { // If the code doesn't know a logid for the user
logid = await getLogid(user); // Try to fetch one from the API
if (logid !== undefined) { // If a valid logid is returned
Logids[user] = logid;
Logids[logid] = user;
reportees.push(user, logid); // Push the username and the logid into the array
}
}
}
} else if (type === 'logid') {
// user = numeral, Logids[user] = username
if (Logids[user] !== undefined) { // If the logid can be converted to a username
if (!isInArray(Logids[user], reportees)) { // If the corresponding username is not in the array
reportees.push(Logids[user]); // Push the username into the array
}
if (!isInArray(user, reportees)) { // If the logid is not in the array
reportees.push(user); // Push the logid into the array
}
} else { // If the logid cannot be converted to a username
if (!isInArray(user, reportees)) { // If the logid is not in the array
reportees.push(user); // Push the logid into the array
}
}
} else { // If other than t=UNL, t=User2, and t=logid
if (!isInArray(user, reportees)) { // If not already in the array
reportees.push(user); // Push the username into the array
}
}
logid = undefined; // Reset before the next iteration
}
// Extract UserAN templates and find duplicate reports
let templates = [];
let dupTemplates = [];
let duplicateFound = false;
for (let i = sectionContent.length -1; i >= 0; i--) { // Loop through all the relevant section texts
/* Extract UserAN templates from the section text
* This returns an array of the occurrences of UserAN, e.g. [{{UserAN|t=UNL|ウィキ助}}, ...] */
templates = findTemplates(sectionContent[i], 'useran');
if (templates.length !== 0) { // If the section text contains at least one UserAN
for (let j = templates.length -1; j >= 0; j--) { // Loop through all the occurences of UserAN
if (stringContainsElementInArray(templates[j], reportees)) { // If there's a duplciate report
if (!isInArray(templates[j], dupTemplates)) {
dupTemplates.push(templates[j]); // List the occurence as a duplicate
}
duplicateFound = true;
}
}
if (!duplicateFound) { // Remove the section text from the array if it doesn't involve duplicate reports
sectionContent.splice(i, 1);
}
} else { // If the section text has no UserAN
sectionContent.splice(i, 1); // Remove the section text from the array
}
duplicateFound = false; // Reset
}
console.log('Templates that involve potential duplicate reports:');
console.log(dupTemplates);
console.log('Section contents that involve the templates:');
console.log(sectionContent);
// Return text and update dialog
if (sectionContent.length === 0) { // If there's no duplicate report
// Update message on the dialog
msgEditing =
`<p style="color: MediumSeaGreen">重複報告は検出されませんでした${toggleLoadingSpinner('remove')}</p>`;
$('.anr-editing').append(msgEditing);
// Return an empty string
return {'wikitext': ''};
} else { // If there're duplicate reports
// Update message on the dialog
msgEditing =
`<p style="color: MediumVioletRed">重複報告の可能性があります${toggleLoadingSpinner('remove')}</p>`;
$('.anr-editing').append(msgEditing);
// Highlight all the duplciate UserAN occurences
sectionContent = sectionContent.join(''); // Merge the separate sections
for (let i = 0; i < dupTemplates.length; i++) {
sectionContent = sectionContent.replaceAll2(dupTemplates[i], '<span style="background-color: #FEC493">' + dupTemplates[i] + '</span>');
}
// Get rid of logids that can be converted to usernames from the array 'reportees'
for (let i = reportees.length -1; i >= 0; i--) {
if (String(reportees[i]).match(/^\d+$/) !== null && Logids[reportees[i]] !== undefined) {
reportees.splice(i, 1);
}
}
// Return wikitext to fetch preview from
return {
'wikitext': sectionContent,
'reportees': reportees
};
}
}
// Function to get the latest revision of the administrator's noticeboard
async function getLastestRevision() {
return new Promise(function(resolve, reject) {
msgEditing =
`<p>最新版を取得しています${toggleLoadingSpinner('add')}</p>`;
$('.anr-editing').append(msgEditing);
new mw.Api().get({
'action': 'query',
'titles': ep.pageToEdit,
'prop': 'revisions',
'curtimestamp': true,
'formatversion': 2
}).done(function(res){
if (res && res.query && res.query.pages) { // If the latest revision is successfully retrieved
if (res.query.pages[0].missing !== true) { // If the page exists
// Get the timestamps of the latest revision and the API query
let baseTS = res.query.pages[0].revisions[0].timestamp; // The TS of the latest revision
let curTS = res.curtimestamp; // The TS of the API query
// Update message on the dialog
msgEditing =
' <p style="color: MediumSeaGreen">取得に成功しました</p>' +
` <p>セクション番号を取得しています${toggleLoadingSpinner('move')}</p>`;
$('.anr-editing').append(msgEditing);
// Return the timestamps as an object
resolve({
'baseTS': baseTS,
'curTS': curTS
});
} else { // If the page doesn't exist
msgDone =
toggleLoadingSpinner('remove') +
'<p style="color: MediumVioletRed">エラー: 報告先のページが存在しません</p>' +
manualEdit();
$('.anr-editing').append(msgDone);
editFailed = true;
editDone($dialog, editFailed, wikiPagename);
resolve();
}
} else { // If revision retrieval fails
queryFailed();
resolve();
}
});
});
}
// Function to get the section number from the section title
async function getSectionNumber() {
return new Promise(function(resolve, reject) {
new mw.Api().get({
'action': 'parse',
'page': ep.pageToEdit,
'formatversion': 2
}).done(function(res){
if (res && res.parse && res.parse.sections) { // If the section list is successfully retrieved
// Get the titles of all sections and their section numbers
let sectionsAPI = {};
for (let i = 0; i < Object.keys(res.parse.sections).length; i++) {
sectionsAPI[res.parse.sections[i].line] = res.parse.sections[i].index;
}
// Return a section number if the section is found, undefined if not
let sectionNum = sectionsAPI[ep.sectionToEdit];
if (sectionNum === undefined) { // If section title in the dropdown is not found
// Show the details of the error
sectionNotFound();
resolve();
} else { // If section title in the dropdown is found
// Update message
msgEditing =
' <p style="color: MediumSeaGreen">取得に成功しました</p>' +
` <p>最新版のテキストを取得しています${toggleLoadingSpinner('move')}</p>`;
$('.anr-editing').append(msgEditing);
resolve(sectionNum);
}
} else { // If the section list retrieval fails
queryFailed();
resolve();
}
});
});
}
// Function to get the text to replace with the current text in the section
async function getTextToReplace(sectionNum) {
return new Promise(function(resolve, reject) {
// Get the text of the latest revision
new mw.Api().get({
'action': 'parse',
'page': ep.pageToEdit,
'section': sectionNum,
'prop': 'wikitext',
'formatversion': 2
}).done(function(res){
if (res && res.parse) {
// Update message
msgEditing =
' <p style="color: MediumSeaGreen">取得に成功しました</p>' +
` <p>報告を試みています${toggleLoadingSpinner('move')}</p>`;
$('.anr-editing').append(msgEditing);
// Get the whole text to append
let wikitextObtained = res.parse.wikitext;
let wholeTextToSubmit;
const delimiter = '<!-- ◆';
const miscHeader = `{{bgcolor|#eee|{{Visible anchor|他${today()}}}|div}}`;
if (ep.reportToANS) { // If the target is WP:AN/S
// Add div if the target section is 'その他' but lacks div for the current date
if (ep.sectionToEdit === 'その他' && wikitextObtained.indexOf(miscHeader) === -1) {
ep.textToSubmit = '; ' + miscHeader + '\n\n' + ep.textToSubmit;
}
// Insert text into the right place
let wikitextSplit = wikitextObtained.split(delimiter);
if (wikitextSplit.length === 3) {
wholeTextToSubmit =
wikitextSplit[0] + delimiter + wikitextSplit[1].trim2() + '\n\n' +
ep.textToSubmit + '\n\n' + delimiter + wikitextSplit[2];
resolve(wholeTextToSubmit);
} else { // If the structure of the section has been changed
// Show error and quit the procedure
msgDone =
toggleLoadingSpinner('remove') +
'<p style="color: MediumVioletRed">報告に失敗しました</p>' +
'<p>セクション構造が改変されているため、' +
`<a href="${mw.util.getUrl('User talk:Dragoniez/AN Reporter')}" target="_blank">開発者</a>` +
'に連絡をお願いします</p>' +
manualEdit();
$('.anr-editing').append(msgDone);
editFailed = true;
editDone($dialog, editFailed, wikiPagename);
resolve();
}
} else { // If the target is WP:AN/I or WP:AN/3RR
wholeTextToSubmit = wikitextObtained.trim2() + '\n\n' + ep.textToSubmit;
resolve(wholeTextToSubmit);
}
} else { // If wikitext retrieval fails
queryFailed();
resolve();
}
});
});
}
// Function to edit the page
function edit(sectionNum, text, baseTS, curTS) {
$.ajax({
'url': mw.util.wikiScript('api'),
'data': {
'format': 'json',
'action': 'edit',
'title': ep.pageToEdit,
'section': sectionNum,
'text': text,
'summary': ep.editSummary,
'basetimestamp': baseTS,
'starttimestamp': curTS,
'token': mw.user.tokens.get('csrfToken'), // May comment this out for debugging
'curtimestamp': true
},
'dataType': 'json',
'type': 'POST',
success: function(res) {
// If the edit was successful
if (res && res.edit && res.edit.result == 'Success') {
setTimeout(async function(){
const diffNum = await getDiffNum(res.curtimestamp); // Get diff number
// Show message
toggleLoadingSpinner('remove');
$('.anr-editing').append(`<p style="color: MediumSeaGreen">報告が完了しました</p>`);
editDone($dialog, editFailed, wikiPagename, diffNum);
return;
}, 0);
// If the edit failed
} else if (res && res.error) {
// Show the details of the error
msgDone =
toggleLoadingSpinner('remove') +
'<p style="color: MediumVioletRed">報告に失敗しました</p>' +
'<br>' +
'<p>詳細:</p>' +
`<p>${res.error.info}</p>` +
manualEdit();
$('.anr-editing').append(msgDone);
editFailed = true;
editDone($dialog, editFailed, wikiPagename);
return;
// If unknown error occurs
} else {
// Show message
msgDone =
toggleLoadingSpinner('remove') +
'<p style="color: MediumVioletRed">不明なエラーが発生しました</p>' +
manualEdit();
$('.anr-editing').append(msgDone);
editFailed = true;
editDone($dialog, editFailed, wikiPagename);
return;
}
}
});
}
// Function to execute report
async function reportUsers() {
// Check the block status of the reportees if the checkbox is checked
let blocked = [];
if (checkBlockStatusBeforeEdit) {
blocked = await preeditBlockStatusQuery(); // Query who's blocked
}
if (blocked.length !== 0) { // If any of the reportees is blocked
// Update dialog buttons
$dialog.dialog({
'buttons': [{
'text': '続行',
'click': function(){
$(this).dialog({'buttons': [] });
reportUsersSecondProcedure();
}
}, {
'text': '戻る',
'click': function(){
$('.anr-modal-dialog form').css('display', 'block');
$('.anr-editing').remove();
$dialog.dialog({
'buttons': [{
'text': 'プレビュー',
'click': previewBtn
}, {
'text': '報告',
'click': reportBtn
}]
});
}
}, {
'text': '中止',
'click': function(){
$(this).dialog('close');
}
}]
});
} else { // If no one is blocked
reportUsersSecondProcedure();
}
}
async function reportUsersSecondProcedure() {
// Check duplicate reports if the checkbox is checked
let dr;
if (checkDuplicateReportsBeforeEdit) {
dr = await preeditDuplicateReportQuery();
}
switch(dr.wikitext) {
case null: // Sections aren't found
return;
case undefined: // checkDuplicateReportsBeforeEdit === false
case '': // No duplicate report found
reportUsersFinalProcedure();
return;
default: // Possible duplicate reports present
// Update dialog buttons
$dialog.dialog({
'buttons': [{
'text': '確認',
'click': function() {
previewDuplicateReports(dr.wikitext, dr.reportees);
}
}, {
'text': '続行',
'click': function(){
$(this).dialog({'buttons': [] });
reportUsersFinalProcedure();
}
}, {
'text': '戻る',
'click': function(){
$('.anr-modal-dialog form').css('display', 'block');
$('.anr-editing').remove();
$dialog.dialog({
'buttons': [{
'text': 'プレビュー',
'click': previewBtn
}, {
'text': '報告',
'click': reportBtn
}]
});
}
}, {
'text': '中止',
'click': function(){
$(this).dialog('close');
}
}]
});
}
}
async function reportUsersFinalProcedure() {
const rev = await getLastestRevision();
if (rev === undefined) {
return;
} else {
var baseTS = rev.baseTS;
var curTS = rev.curTS;
}
const sectionNum = await getSectionNumber();
if (sectionNum === undefined) {
return;
}
const text = await getTextToReplace(sectionNum);
if (text === undefined) {
return;
}
edit(sectionNum, text, baseTS, curTS);
}
/**
* Function to preview duplicate reports on a new dialog
* @param {String} wikitext The wikitext to convert to html and preview
* @param {Array} reportees The array of reportees, logids should be converted to usernames if possible beforehand
*/
function previewDuplicateReports(wikitext, reportees) {
// Show logids in parentheses
for (let i = 0; i < reportees.length; i++) {
if (Logids[reportees[i]]) { // If the code knows the logid for a user
reportees[i] += ' (' + Logids[reportees[i]] + ')'; // Show the logid in parentheses
}
}
// Create dialog
let duplicateReportPreviewDiv =
'<div class="anr-drpreview-dialog" title="AN Reporter Duplicate Report Preview" style="max-height: 80vh;">' +
' <div id="anr-drpreview-header" style="padding: 0.5em;">' +
' <p id="anr-drpreview-loading">' +
` 読み込み中${toggleLoadingSpinner('add')}` +
' </p>' +
' <p id="anr-drpreview-userlist" style="display: none; font-size: larger">' +
' <span style="font-weight: bold">入力された利用者名:</span>' +
' <br>' +
reportees.join(', ') +
' </p>' +
' </div>' +
' <div id="anr-drpreview-body" style="display: none; font-size: 1.1em; padding: 0.5em; border: 1px solid silver; background-color: white;">' +
// Added when the dialog is opened
' </div>' +
'</div>';
$('body').append(duplicateReportPreviewDiv);
// Show preview dialog
$('.anr-drpreview-dialog').dialog({
'height': 'auto',
'width': $('#content').width() * 0.8,
'modal': true,
'position': { my: 'center', at: 'top', of: window },
'open': async function(){
// CSS
dialogCSS();
// Convert the wikitext to an html form
const wikitextInHtml = await convertWikitextToHtml(wikitext, '');
if (wikitextInHtml) {
$('#anr-drpreview-body').append(wikitextInHtml.htmltext);
$('.anr-drpreview-dialog a').attr('target', '_blank'); // Open all links on a new tab
$('#anr-drpreview-body').css('display', 'block');
$('#anr-drpreview-loading').remove();
$('#anr-drpreview-userlist').css('display', 'inline');
} else {
$('#anr-drpreview-loading').text('読み込みに失敗しました').css('color', 'MediumVioletRed');
setTimeout(function(){
$('.anr-drpreview-dialog').dialog('close');
}, 10000);
}
},
'buttons': [{
'text': '閉じる',
'click': function(){
$(this).dialog('close');
}
}]
});
}
// Function to show message when edit attempt is done
function queryFailed() {
msgDone =
toggleLoadingSpinner('remove') +
'<p style="color: MediumVioletRed">取得に失敗しました</p>' +
manualEdit();
$('.anr-editing').append(msgDone);
editFailed = true;
editDone($dialog, editFailed, wikiPagename);
}
// Function to generate the html for the manual edit helper tab
function manualEdit() {
let meHtml =
'<br>' +
'<p>手動編集用:</p>' +
`<textarea disabled rows="4" style="width: 100%">${ep.textToSubmit}</textarea>` +
'<br>' +
'<p>要約:</p>' +
`<textarea disabled rows="2" style="width: 100%">${ep.editSummary.replace(scriptAd, '')}</textarea>`;
return meHtml;
}
// Function to show error message if sections that must be there are not found
function sectionNotFound() {
msgDone =
toggleLoadingSpinner('remove') +
'<p style="color: MediumVioletRed">取得に失敗しました</p>' +
'<p>指定されたセクションが見つかりませんでした</p>' +
'<br>' +
'<p>考えられる原因:</p>' +
`<p>1. 編集先のページの節構成が変更された</p>` +
`<p>2. 通信に失敗した</p>` +
`<p>3. スクリプトのバグ</p>` +
manualEdit();
$('.anr-editing').append(msgDone);
editFailed = true;
editDone($dialog, editFailed, wikiPagename);
}
// Report the user(s)
reportUsers();
}
/**
* Function to extract templates from wikitext
* @param {String} text The text in which to search for templates
* @param {String} templateName [Optional] Specify the template name (case-insensitive)
* @returns {Array} An array of the extracted templates
*/
function findTemplates(text, templateName) {
// Split the text with '{{', the head delimiter of templates
const textSplit = text.split('{{');
let templates = [];
/* Explanation
* If the text to parse is "{{UserAN|t=none|{{Logid|123456789|{{User|EXAMPLE}}|s=}}}} - {{UserAN|USER}} is the same.",
* textSplit = ['', 'UserAN|t=none|', 'Logid|123456789|', 'User|EXAMPLE}}|s=}}}} - ', 'UserAN|USER}} is the same.']
*/
if (textSplit.length === 0) { // If the text has no tempalte in it
return templates; // Return an empty array
} else { // If the text has some templates in it
for (let i = 1; i < textSplit.length; i++) { // Loop through all elements (but arr[0]) in the split array
let TLCloseCnt = (textSplit[i].match(/\}\}/g) || []).length; // Get the number of '}}' in the split segment
let temp = ''; // Temporary escape hatch
switch(true) {
case TLCloseCnt === 0: // If the split segment doesn't have any '}}'
// Do nothing
break;
case TLCloseCnt === 1: // If the split segment itself is the whole of a template
temp = '{{' + textSplit[i].split('}}')[0] + '}}';
if (!isInArray(temp, templates)) {
templates.push(temp);
}
break;
/* Explanation
* If textSplit[3] ( = 'User|EXAMPLE}}|s=}}}} - '), .split('}}') returns
* ['User|EXAMPLE', '|s=', '', ' - ']. Thus '{{' + splitArr[0] +'}}' returns '{{User|EXAMPLE}}'.
*/
case TLCloseCnt > 1: // If templates are nested
for (let j = 0; j < TLCloseCnt; j++) { // Loop through all the nests
if (j === 0) { // The innermost template
temp = '{{' + textSplit[i].split('}}')[j] + '}}'; // Same as when TLCloseCnt === 1
if (!isInArray(temp, templates)) {
templates.push(temp);
}
} else { // Nesting templates
temp = '{{' + textSplit[i - j] + temp + textSplit[i].split('}}')[j] + '}}';
if (!isInArray(temp, templates)) {
templates.push(temp);
}
/* Explanation
* Assume that i is 3 and j is 1. Then the 'temp' variable has the value '{{User|EXAMPLE}}'
* because this is the value returned when j is 0. We want to nest this inside its parent
* template, and the left segment of the parent template is in textSplit[2] (thus 3 - 1).
* Now we just want to add the child template (stored in 'temp'), then what remains is the
* right segment of the parent template, and this is just the splitArr[1] of textSplit[3]
* (see also the explanation for when TLCloseCnt === 1.) If there are more templates,
* looping this process will do.
*/
}
}
break;
default:
// Do nothing
}
}
//console.log('Templates in the section:');
//console.log(templates);
// Check if the optional parameter is specified
if (templateName !== undefined && templates.length !== 0) {
let templateRegExp = new RegExp(templateName, 'i'); // Template names are case-insensitive
for (let i = templates.length -1; i >= 0; i--) {
if (templates[i].search(templateRegExp) === -1) { // If the array element doesn't contain the template name
templates.splice(i, 1); // Remove the element
}
}
}
//console.log('UserAN occurrences in the section:');
//console.log(templates);
return templates;
}
}
/**
* Function to add/remove/move a loading spinner
* @param {string} action - 'add', 'remove', or 'move'
*/
function toggleLoadingSpinner(action) {
const img = '<img ' +
'class="anr-loading-spinner" ' +
'src="https://upload.wikimedia.org/wikipedia/commons/4/42/Loading.gif" ' +
'style=' +
'vertical-align: middle; ' +
'max-height: 100%; ' +
'border: 0;" ' +
'/>';
switch(action) {
case 'add':
return img;
case 'remove':
$('.anr-loading-spinner').remove();
return '';
case 'move':
$('.anr-loading-spinner').remove();
return img;
}
}
// Action for when edit is done (in any way)
function editDone($dialog, editFailed, wikiPagename, diffNum) {
// Get the page name without a section specifier
let tarPage = wikiPagename.split('#')[0];
// Button to jump to diff
let btns = [];
let diffBtn, destBtn, closeBtn;
if (diffNum !== undefined) { // Show the button only if diff number is available
diffBtn = {
'text': '差分',
'click': function(){
window.open(mw.util.getUrl('特別:差分/' + diffNum), '_blank');
}
};
btns.push(diffBtn);
}
// Button to jump to the report page
if (editFailed || mw.config.get('wgPageName') !== tarPage) { // Show the button if the edit failed or if the user is NOT on the page
destBtn = {
'text': '報告先',
'click': function(){
window.open(mw.util.getUrl(wikiPagename), '_blank');
}
};
btns.push(destBtn);
}
// Button to close the dialog (always shown)
closeBtn = {
'text': '閉じる',
'click': function(){
$(this).dialog('close');
let curPage = mw.config.get('wgPageName');
if (
curPage === ANI ||
curPage === ANS ||
curPage === AN3RR ||
curPage === Iccic ||
curPage === ISECHIKA ||
curPage === KAGE ||
curPage === KIYOSHIMA ||
curPage === SHINJU ||
curPage === '利用者:Dragoniez/test' ||
curPage === '利用者:Dragoniez/test2'
) {
location.reload(true);
}
}
};
btns.push(closeBtn);
// Show the button(s) on the dialog
$dialog.dialog({
'position': { my: 'center', at: 'top', of: window },
'buttons': btns
});
}
// Function to generate edit summary automatically
function genEditSummary() {
let inputRemains = true;
let i = 1;
let arrOfContribs = [];
let contribs = '';
let type = '';
let reportee = '';
// Check content of all inputs into which usernames are typed
while (inputRemains) {
if ($(`#anr-user${i}-input`).length === 0) { // if selector doesn't exist
inputRemains = false; // No inputs remain to be checked
} else {
type = $(`#anr-user${i}-select`).children('option').filter(':selected').text(); // UserAN type specified in the dropdown
reportee = $(`#anr-user${i}-input`).val().trim2(); // input value
if (reportee !== '') { // Skip if the input value is a null string
// Get appropriate links depending on the UserAN type
switch(type) {
case 'UNL':
case 'User2':
case 'IP2':
contribs = `[[特別:投稿記録/${reportee}|${reportee}]]`;
break;
case 'logid':
contribs = `[[特別:転送/logid/${reportee}|Logid/${reportee}]]`;
break;
case 'diff':
contribs = `[[特別:差分/${reportee}|差分/${reportee}]]の投稿者`;
break;
default:
contribs = reportee;
}
// Push the link into the array
if (!isInArray(contribs, arrOfContribs)) {
arrOfContribs.push(contribs);
}
}
i++;
}
}
// Get edit summary
let textToShow = '';
if (arrOfContribs.length === 0) {
// Do nothing
} else if (arrOfContribs.length === 1) {
textToShow += '+' + arrOfContribs[0] + ' - ';
} else {
textToShow += '+' + arrOfContribs.join(', ') + ' - ';
}
return textToShow;
}
// Reset dialog when closed
$(document).off('dialogclose', '.anr-modal-dialog, .anr-preview-dialog, .anr-drpreview-dialog')
.on('dialogclose', '.anr-modal-dialog, .anr-preview-dialog, .anr-drpreview-dialog', function() {
$(this).remove();
});
// Dynamically change the content of the section dropdown depending on the value selected in '報告先'
$(document).off('change', '#anr-target-options').on('change', '#anr-target-options', function(){
const selectedTar = $(this).children('option').filter(':selected').text();
switch(selectedTar) {
case ANI:
$('.anr-section-div').empty();
$('.anr-section-div').append(sectionsI);
$('#anr-section-options-i-date').text(getSectionI(false));
$('#anr-section-options-i').css({'width': $(this).width()});
$('.anr-section-div').css('display', 'block');
$('.anr-target-pagelink-div').css('display', 'block');
$('#anr-target-pagelink').attr('href', mw.util.getUrl(ANI));
break;
case ANS:
$('.anr-section-div').empty();
$('.anr-section-div').append(sectionsS);
$('#anr-section-options-s').select2({'width': $(this).width()});
//$('#select2-anr-section-options-s-container').attr('style', rlsCSS);
$('.anr-section-div').css('display', 'block');
$('.anr-target-pagelink-div').css('display', 'block');
$('#anr-target-pagelink').attr('href', mw.util.getUrl(ANS));
break;
case AN3RR:
$('.anr-section-div').empty();
$('.anr-section-div').css('display', 'none');
$('.anr-target-pagelink-div').css('display', 'block');
$('#anr-target-pagelink').attr('href', mw.util.getUrl(AN3RR));
break;
}
});
// Add section name to the '報告先' link when section is specified
$(document).off('change', '#anr-section-options-i, #anr-section-options-s')
.on('change', '#anr-section-options-i, #anr-section-options-s', function(){
let tarSection = '';
let tarPage = '';
if ($(this).attr('id') === 'anr-section-options-i') {
tarPage = ANI;
} else if ($(this).attr('id') === 'anr-section-options-s') {
tarPage = ANS;
}
if ($(this).find('option').filter(':selected').text() !== '選択してください') {
tarSection = '#' + $(this).find('option').filter(':selected').text();
$('#anr-target-pagelink').attr('href', mw.util.getUrl(tarPage + tarSection));
}
});
// When the selection is changed in the type dropdown
$(document).off('change','.anr-user-div select').on('change','.anr-user-div select', function(e){
const selectID = '#' + e.target.id; // #anr-userX-select
const valSelected = $(selectID).children('option').filter(':selected').text(); // Selected type
const inputID = selectID.replace('select', 'input'); // #anr-userX-input
const valInput = $(inputID).val().trim2(); // The input value
const checkboxDivID = selectID.replace('select', 'checkbox-div'); // #anr-userX-checkbox-div
const checkboxID = selectID.replace('select', 'checkbox'); // #anr-userX-checkbox
const idlinkDivID = selectID.replace('select', 'idlink-div'); // #anr-userX-idlink-div
const idlinkID = selectID.replace('select', 'idlink'); // #anr-userX-idlink
switch(valSelected) {
case 'UNL':
case 'User2':
$(checkboxDivID).css('display', 'block');
toggleBlockStatusLink(inputID, false, false);
break;
case 'IP2':
toggleBlockStatusLink(inputID, false, false);
break;
case 'logid':
$(checkboxDivID).css('display', 'block');
$(checkboxID).prop('checked', true);
$(idlinkDivID).css('display', 'block');
$(idlinkID).attr('href', mw.util.getUrl('Special:redirect/logid/' + valInput)).text('特別:転送/logid/' + valInput);
toggleBlockStatusLink(inputID, true, true);
break;
case 'diff':
$(checkboxDivID).css('display', 'none');
$(idlinkDivID).css('display', 'block');
$(idlinkID).attr('href', mw.util.getUrl('Special:diff/' + valInput)).text('特別:差分/' + valInput);
toggleBlockStatusLink(inputID, true, false);
break;
default:
$(checkboxDivID).css('display', 'none');
$(idlinkDivID).css('display', 'none');
toggleBlockStatusLink(inputID, true, false);
}
});
// When username is typed in, change dropdown options for UserAN types
$(document).off('input', '.anr-user-div :text').on('input', '.anr-user-div :text', function(e){
const inputID = '#' + e.target.id; // #anr-userX-input
updateTypeDropdown(inputID);
});
// When 'hide username' is clicked, get logid, change dropdown options, show href and so on
$(document).off('change', '.anr-user-div :checkbox').on('change', '.anr-user-div :checkbox', function(e){
const checkboxID = '#' + e.target.id; // #anr-userX-checkbox
const selectID = checkboxID.replace('checkbox', 'select'); // #anr-userX-select
const inputID = checkboxID.replace('checkbox', 'input'); // #anr-userX-input
const inputVal = $(inputID).val().trim2();
const idlinkID = checkboxID.replace('checkbox', 'idlink'); // #anr-userX-idlink
const idlinkDivID = checkboxID.replace('checkbox', 'idlink-div'); // #anr-userX-idlink-div
if ($(checkboxID).is(':checked')) { // if the checkbox is checked
// Function to update type dropdown
let updateDropdown = function(logid) {
$(selectID).children('.anr-opt-UNL').prop('hidden', true);
$(selectID).children('.anr-opt-User2').prop('hidden', true);
$(selectID).children('.anr-opt-IP2').prop('hidden', true);
$(selectID).children('.anr-opt-logid').prop('hidden', false).prop('selected', true);
$(selectID).children('.anr-opt-diff').prop('hidden', false);
$(selectID).children('.anr-opt-none').prop('hidden', false);
$(idlinkDivID).css('display', 'block');
$(idlinkID).attr('href', mw.util.getUrl('Special:redirect/logid/' + logid)).text('特別:転送/logid/' + logid);
toggleBlockStatusLink(inputID, true, true);
}
if (Logids[inputVal] !== undefined) {
$(inputID).val(Logids[inputVal]); // if the object knows the logid for the user, retrieve the data
updateDropdown(Logids[inputVal]);
} else {
// if the object doesn't know the logid for the user, ask the API
async function logidApi(){
// Get logid from the API
let logid = await getLogid(inputVal);
setTimeout(function(){ // Deliberately setting lag to prevent bugs
// Check the obtained logid
if (logid === undefined) { // If undefined is returned, reject the checking of the checkbox
alert('エラー\n\n取得可能なlogidが存在しません。Logidを手動で入力するか、type=diff または none を使用してください');
$(checkboxID).prop('checked', false);
return;
} else { // If a valid logid is returned
// Set the logid to the input
$(inputID).val(logid);
// Push username and logid into object if it doesn't have them
if (Logids[inputVal] === undefined) {
Logids[inputVal] = logid;
}
if (Logids[logid] === undefined) {
Logids[logid] = inputVal;
}
// Update type dropdown
updateDropdown(logid);
}
}, 100);
}
logidApi();
}
} else { // if the checkbox is unchecked
if (Logids[inputVal] !== undefined) {
$(inputID).val(Logids[inputVal]); // if the object knows the username for the logid, retrieve the data
$(selectID).children('.anr-opt-UNL').prop('hidden', false).prop('selected', true);
$(selectID).children('.anr-opt-User2').prop('hidden', false);
$(selectID).children('.anr-opt-IP2').prop('hidden', true);
$(selectID).children('.anr-opt-logid').prop('hidden', true);
$(selectID).children('.anr-opt-diff').prop('hidden', true);
$(selectID).children('.anr-opt-none').prop('hidden', false);
$(idlinkDivID).css('display', 'none');
toggleBlockStatusLink(inputID, false, false);
} else {
alert('エラー\n\nLogidにはアカウント作成記録以外のものも含まれるため、logidからユーザー名への変換機能は実装していません。' +
'テキストボックス下のリンク先からユーザー名を取得するか、手動入力してください。なお、ユーザー名からlogidへの変換が行われた' +
'場合のみ、その逆の変換が可能です');
$(checkboxID).prop('checked', true);
}
}
});
// When the 'add' button is hit, add another input layer
let userCnt = 1;
$('.anr-addBtn').click(function(){
userCnt++;
let replaceTar = new RegExp(`${userCnt-1}-`, 'g');
userHtml = userHtml.replace(replaceTar, `${userCnt}-`); // 1 → 2, 2 → 3 and so forth
$('.anr-btn-div').before(userHtml);
$(`#anr-user${userCnt}-input-div`).css('margin-top', '0.2em');
});
// When buttons are moused on and off
$(document).off('mouseover mouseleave', '.anr-modal-dialog form button')
.on({
'mouseover': function(e) {
e.target.style.borderColor = '#999999';
},
'mouseleave': function(e) {
e.target.style.borderColor = '#d3d3d3';
}
}, '.anr-modal-dialog form button');
// When the summary checkbox is (un)checked
$(document).off('change', '#anr-summary-checkbox').on('change', '#anr-summary-checkbox', function(){
let $textarea = $('#anr-summary-text');
if ($(this).is(':checked')) { // Box is checked
// Show textarea
$textarea.css('display','inline-block').val(genEditSummary());
} else { // Box is unchecked
$textarea.css('display','none').val('');
}
});
// When 'check block status before edit' is (un)checked
$('#anr-blockstatus-checkbox').change(function(){
if ($(this).is(':checked')) { // Box is checked
checkBlockStatusBeforeEdit = true;
} else { // Box is unchecked
checkBlockStatusBeforeEdit = false;
}
});
// When 'check dpulicate reports before edit' is (un)checked
$('#anr-duplicatereport-checkbox').change(function(){
if ($(this).is(':checked')) { // Box is checked
checkDuplicateReportsBeforeEdit = true;
} else { // Box is unchecked
checkDuplicateReportsBeforeEdit = false;
}
});
}); // addPortletLink
// Function to get the last day of the month
let lastDay = function(y,m){
return new Date(y, m +1, 0).getDate();
}
/**
* Function to get the current date and the section name to which to report
* @param {Boolean} last - if true, returns the name of the last section
* @returns {String} - section name
*/
function getSectionI(last){
let d = new Date();
if (last) {
let subtract = 5;
if (d.getDate() === 1 || d.getDate() === 2) {
subtract = 3;
} else if (d.getDate() === 31) {
subtract = 6;
}
d.setDate(d.getDate() - subtract);
}
let sectionName;
switch(true) {
case (1 <= d.getDate() && d.getDate() <= 5):
sectionName = `${d.getFullYear()}年${d.getMonth()+1}月1日 - 5日新規報告`;
break;
case (6 <= d.getDate() && d.getDate() <= 10):
sectionName = `${d.getFullYear()}年${d.getMonth()+1}月6日 - 10日新規報告`;
break;
case (11 <= d.getDate() && d.getDate() <= 15):
sectionName = `${d.getFullYear()}年${d.getMonth()+1}月11日 - 15日新規報告`;
break;
case (16 <= d.getDate() && d.getDate() <= 20):
sectionName = `${d.getFullYear()}年${d.getMonth()+1}月16日 - 20日新規報告`;
break;
case (21 <= d.getDate() && d.getDate() <= 25):
sectionName = `${d.getFullYear()}年${d.getMonth()+1}月21日 - 25日新規報告`;
break;
case (26 <= d.getDate() && d.getDate() <= lastDay(d.getFullYear(), d.getMonth())):
sectionName = `${d.getFullYear()}年${d.getMonth()+1}月26日 - ${lastDay(d.getFullYear(), d.getMonth())}日新規報告`;
break;
default:
undefined;
}
return sectionName;
}
// Function to get today's date
function today() {
let d = new Date();
return d.getMonth()+1 + '月' + d.getDate() + '日';
}
// Function to check if a user exists locally
async function userExists(username) {
return new Promise(function(resolve, reject) {
new mw.Api().get({
'action': 'query',
'list': 'users',
'ususers': username,
'formatversion': 2
}).done(function(res){
if (res.query.users[0].userid !== undefined) { // If user exists
resolve(1); // Return true
} else { // If user doesn't exist
resolve(0); // Return false
}
});
});
}
// Function to manipulate dropdown options for UserAN types (also maniputes show/hide of checkbox)
let updateTypeDropdownTimeout;
function updateTypeDropdown(inputID) {
let tarVal = $(inputID).val().trim2(); // The value typed into the input
let selectID = inputID.replace('input', 'select'); // #anr-userX-select
let checkboxDivID = inputID.replace('input', 'checkbox-div'); // #anr-userX-checkbox-div
let checkboxID = inputID.replace('input', 'checkbox'); // #anr-userX-checkbox
let idlinkDivID = inputID.replace('input', 'idlink-div'); // #anr-userX-idlink-div
clearTimeout(updateTypeDropdownTimeout); // Run the async function only once when there's been no input change for 0.35 seconds
updateTypeDropdownTimeout = setTimeout(async function(){
if (tarVal === '') { // if the field is empty
$(selectID).prop('disabled', true).children('.anr-opt-none').prop('selected', true); // Disable dropdown and select 'none'
$(checkboxDivID).css('display', 'none'); // Hide 'hide username' checkbox
$(checkboxID).prop('checked', false); // Uncheck the checkbox
$(idlinkDivID).css('display', 'none'); // Hide logid/diff link
toggleBlockStatusLink(inputID, true, false);
} else { // if the field is filled
$(selectID).prop('disabled', false); // enable dropdown
if (mw.util.isIPAddress(tarVal, true)) { // if IP
$(selectID).children('.anr-opt-UNL').prop('hidden', true);
$(selectID).children('.anr-opt-User2').prop('hidden', true);
$(selectID).children('.anr-opt-IP2').prop({'hidden': false, 'selected': true});
$(selectID).children('.anr-opt-logid').prop('hidden', true);
$(selectID).children('.anr-opt-diff').prop('hidden', true);
$(selectID).children('.anr-opt-none').prop('hidden', false);
$(checkboxDivID).css('display', 'none'); // hide 'hide username' checkbox
$(checkboxID).prop('checked', false); // uncheck the checkbox
$(idlinkDivID).css('display', 'none');
toggleBlockStatusLink(inputID, false, false);
} else if (await userExists(tarVal)) { // if user
$(selectID).children('.anr-opt-UNL').prop({'hidden': false, 'selected': true});
$(selectID).children('.anr-opt-User2').prop('hidden', false);
$(selectID).children('.anr-opt-IP2').prop('hidden', true);
$(selectID).children('.anr-opt-logid').prop('hidden', true);
$(selectID).children('.anr-opt-diff').prop('hidden', true);
$(selectID).children('.anr-opt-none').prop('hidden', false);
$(checkboxDivID).css('display', 'block'); // show 'hide username' checkbox
$(checkboxID).prop('checked', false); // uncheck the checkbox
$(idlinkDivID).css('display', 'none');
toggleBlockStatusLink(inputID, false, false);
} else { // if something else (like random numbers or strings)
$(selectID).children('.anr-opt-UNL').prop('hidden', true);
$(selectID).children('.anr-opt-User2').prop('hidden', true);
$(selectID).children('.anr-opt-IP2').prop('hidden', true);
$(selectID).children('.anr-opt-logid').prop('hidden', false);
$(selectID).children('.anr-opt-diff').prop('hidden', false);
$(selectID).children('.anr-opt-none').prop({'hidden': false, 'selected': true});
$(checkboxDivID).css('display', 'none'); // hide 'hide username' checkbox
toggleBlockStatusLink(inputID, true, false);
}
}
}, 350);
}
/**
* Function to show/hide 'This user has blocks' links
* @param {String} inputID - the ID of the input
* @param {Boolean} forceHide - if true, hide the block status link without API requests
* @param {Boolean} convertLogid - if true, try to convert a logid to a username
*/
async function toggleBlockStatusLink(inputID, forceHide, convertLogid) {
let username = $(inputID).val().trim2(); // The value in the input
let $bsLinkDiv = $(inputID.replace('input', 'blockstatus-div')); // #anr-userX-blockstatus-div
let $bsLink = $(inputID.replace('input', 'blockstatus')); // #anr-userX-blockstatus
if (forceHide && convertLogid) {
if (Logids[username] !== undefined) { // If the object knows who that logid is for
username = Logids[username]; // Convert the logid to the username and continue to API requests
} else { // If the object doesn't know who that logid is for
$bsLinkDiv.css('display', 'none'); // Hide the link div and exit function
return;
}
} else if (forceHide) { // If forceHide is true, basically just hide the bsLink without API requests
$bsLinkDiv.css('display', 'none'); // Hide the link div
return;
}
// Check the block status of the user, and if blocked, update the bsLink, or else, hide the link
let blocked = await getBlocked([username]);
if (blocked.length !== 0) { // If the user typed into the input is blocked
$bsLink.attr('href', mw.util.getUrl('特別:投稿記録/' + username)); // Update the link
$bsLinkDiv.css('display', 'block'); // Show the link div
} else {
$bsLinkDiv.css('display', 'none'); // Hide the link div
}
return;
}
// Function to get an array of blocked users & IPs from an array of random users & IPs
async function getBlocked(namesArr) {
let users = [];
let ips = [];
let blocked = [];
// Sort names to users and IPs
for (i = 0; i < namesArr.length; i++) {
if (mw.util.isIPAddress(namesArr[i], true)) { // Push IPs into the array
ips.push(namesArr[i]);
} else { // Push users into the array
users.push(namesArr[i]);
}
}
// Check who's blocked
if (users.length !== 0 && ips.length !== 0) { // If namesArr contains both users and IPs
blocked = blocked.concat(await getBlockedUsers(users), await getBlockedIps(ips));
} else if (users.length !== 0) { // If namesArr only contains users
blocked = blocked.concat(await getBlockedUsers(users));
} else if (ips.length !== 0) { // If namesArr only contains IPs
blocked = blocked.concat(await getBlockedIps(ips));
} else {
return blocked; // [], empty array
}
// Sort the blocked in the original order
for (i = namesArr.length -1; i >= 0; i--) {
if (!isInArray(namesArr[i], blocked)) { // If not blocked
namesArr.splice(i, 1); // Remove the name from the array
}
}
return namesArr;
}
// Function to get an array of blocked users from an array of random users
async function getBlockedUsers(usersArr) {
let blockedArr = [];
// API query
return new Promise(function(resolve, reject) {
new mw.Api().get({
'action': 'query',
'list': 'blocks',
'bklimit': usersArr.length,
'bkusers': usersArr.join('|'),
'bkprop': 'user',
'formatversion': 2
}).done(function(res){
let blockstatusApi = res.query.blocks;
if (blockstatusApi.length === 0) { // If none of the users is blocked
resolve(blockedArr); // Return []
} else { // If there are one or more blocked users
for (let i = 0; i < blockstatusApi.length; i++) {
blockedArr.push(blockstatusApi[i].user); // Push blocked users into the array
}
resolve(blockedArr); // Return e.g. [user1, user2...]
}
});
});
}
// Function to get an array of blocked IPs from an array of random IPs
async function getBlockedIps(ipsArr) {
let blockedArr = [];
let blocked;
for (let i = 0; i < ipsArr.length; i++) {
blocked = await ipIsBlocked(ipsArr[i]);
if (blocked) {
blockedArr.push(ipsArr[i]);
}
if (i === ipsArr.length -1) {
return blockedArr;
}
}
}
// Function to check if a given IP is blocked (true if the IP is blocked and false if not)
async function ipIsBlocked(ip) {
return new Promise(function(resolve, reject) {
new mw.Api().get({
'action': 'query',
'list': 'blocks',
'bklimit': 1,
'bkip': ip, // This parameter doesn't take multiple, pipe-separeted arguments, unlike the param 'bkusers'
'bkprop': 'user', // Get the blocked IP: Could be a range; Currently this value is not used in the function
'formatversion': 2
}).done(function(res){
if (res.query.blocks.length !== 0) { // If the IP is blocked
resolve(true);
} else { // If the IP is NOT blocked
resolve(false);
}
});
});
}
// Function to get account creation logid
async function getLogid(username) {
return new Promise(function(resolve, reject) {
new mw.Api().get({
'action': 'query',
'list': 'logevents',
'leuser': username,
'ledir': 'newer',
'lelimit': 1,
'formatversion': 2
}).done(function(res){
if (res.query.logevents.length === 0) { // If empty array is returned (=if no logevent exists)
resolve();
} else {
resolve(res.query.logevents[0].logid);
}
});
});
}
// Function to get diff number and its URL after edit
async function getDiffNum(curtimestamp) {
return new Promise(function(resolve, reject) {
new mw.Api().get({
'action': 'query',
'list': 'allrevisions',
'arvprop': 'ids|timestamp',
'arvlimit': 5,
'arvuser': mw.config.get('wgUserName'),
'arvstart': curtimestamp,
'arvdir': 'newer',
'formatversion': 2
}).done(function(res){
if (res && res.query && res.query.allrevisions) {
let revArr = res.query.allrevisions;
for (let i = 0; i < revArr.length; i++) {
if (revArr[i].revisions[0].timestamp === curtimestamp) {
resolve(revArr[i].revisions[0].revid);
}
if (i === revArr.length -1) {
resolve();
}
}
}
});
});
}
} // if (userIsInGruoup)
/**
* Function to check if an element is in an array
* @param {String} el
* @param {Array} arr
* @returns {Boolean}
*/
function isInArray (el, arr) {
if (arr.indexOf(el) !== -1) {
return true;
} else {
return false;
}
}
/**
* Function to check if elements of an array are all contained in another array
* @param {Array} arr1
* @param {Array} arr2
* @returns {Boolean}
*/
function arrayIsInArray(arr1, arr2) {
for (let i = 0; i < arr1.length; i++) {
if (!isInArray(arr1[i], arr2)) {
return false;
}
}
return true;
}
/**
* Function to check what user group the CURRENT USER belongs to (thus it doesn't take a username parameter)
* @param {String} group - a group name such as 'autoconfirmed' and 'sysop'
* @returns {Boolean}
*/
function userIsInGroup(group) {
if (isInArray(group, mw.config.get('wgUserGroups'))) {
return true;
} else {
return false;
}
}
/**
* Function to check if a string contains a substring that is an element of an array
* @param {String} str
* @param {Array} arr
* @returns {Boolean}
*/
function stringContainsElementInArray(str, arr) {
for (let i = 0; i < arr.length; i++) {
if (str.indexOf(arr[i]) !== -1) {
return true;
}
}
return false;
}
/**
* Function to count the number of an element in an array
* @param {String} el The array element to count
* @param {Array} arr The array in which to count the element
* @returns {integer} The number of the element in the array
*/
function countElementsInArray(el, arr) {
const counts = {};
arr.forEach(function(x) {
counts[x] = (counts[x] || 0) + 1;
});
if (counts[el] === undefined) {
return 0;
} else {
return counts[el];
}
}
});
//</nowiki>