User:GeneralNotability/spihelper.js

//

// @ts-check

// GeneralNotability's rewrite of Tim's SPI helper script

// With contributions from 0xDeadbeef, DatGuy, Dreamy Jazz, L235, Tamzin, TheresNoTime, and Xiplus

// v2.9.1 "No sorcery threats"

/* global mw, $, importStylesheet, importScript, displayMessage, spiHelperCustomOpts */

// Adapted from User:Mr.Z-man/closeAFD

importStylesheet('User:GeneralNotability/spihelper-dev.css')

importScript('User:Timotheus Canens/displaymessage.js')

// Typedefs

/**

* @typedef SelectOption

* @type {Object}

* @property {string} label Text to display in the drop-down

* @property {string} value Value to return if this option is selected

* @property {boolean} selected Whether this item should be selected by default

* @property {boolean=} disabled Whether this item should be disabled

*/

/**

* @typedef BlockEntry

* @type {Object}

* @property {string} username Username to block

* @property {string} duration Duration of block

* @property {boolean} acb If set, account creation is blocked

* @property {boolean} ab Whether autoblock is enabled (for registered users)/

* logged-in users are blocked (for IPs)

* @property {boolean} ntp If set, talk page access is blocked

* @property {boolean} nem If set, email access is blocked

* @property {string} tpn Type of talk page notice to apply on block

*/

/**

* @typedef TagEntry

* @type {Object}

* @property {string} username Username to tag

* @property {string} tag Tag to apply to user

* @property {string} altmasterTag Altmaster tag to apply to user, if relevant

* @property {boolean} blocking Whether this account is marked for block as well

*/

/**

* @typedef ParsedArchiveNotice

* @type {Object}

* @property {string} username Case username

* @property {boolean} xwiki Whether the crosswiki flag is set

* @property {boolean} deny Whether the deny flag is set

* @property {boolean} notalk Whether the notalk flag is set

*/

// Globals

/* User setting related globals */

// User-configurable settings, these are the defaults but will be updated by

// spiHelperLoadSettings()

const spiHelperSettings = {

// Choices are 'watch' (unconditionally add to watchlist), 'preferences'

// (follow default preferences), 'nochange' (don't change the watchlist

// status of the page), and 'unwatch' (unconditionally remove)

watchCase: 'preferences',

watchCaseExpiry: 'indefinite',

watchArchive: 'nochange',

watchArchiveExpiry: 'indefinite',

watchTaggedUser: 'preferences',

watchTaggedUserExpiry: 'indefinite',

watchNewCats: 'nochange',

watchNewCatsExpiry: 'indefinite',

watchBlockedUser: true,

watchBlockedUserExpiry: 'indefinite',

// Lets people disable clerk options if they're not a clerk

clerk: true,

// Log all actions to Special:MyPage/spihelper_log

log: false,

// Reverse said log, so that the newest actions are at the top.

reversed_log: false,

// Enable the "move section" button

iUnderstandSectionMoves: false,

// Automatically tick the "Archive case" option if the case is closed

tickArchiveWhenCaseClosed: true,

// Use checkuserblock-account when CU blocking. False when not a CU, by default true when a CU

useCheckuserblockAccount: false,

// Default IPv6 listings to /64 in the block/tag socks menu

displayIPv6As64: true,

// These are for debugging to view as other roles. If you're picking apart the code and

// decide to set these (especially the CU option), it is YOUR responsibility to make sure

// you don't do something that violates policy

debugForceCheckuserState: null,

debugForceAdminState: null

}

// Can't set in the spiHelperSettings declaration because spiHelperIsCheckuser itself uses the settings var

spiHelperSettings.useCheckuserblockAccount = spiHelperIsCheckuser()

// Valid options for spiHelperSettings. Prevents invalid setting options being specified in the spioptions user subpage.

// This method only works options with discrete possible values. Settings without discrete possible values are checked for in spiHelperLoadSettings().

const spiHelperValidSettings = {

watchCase: ['preferences', 'watch', 'nochange', 'unwatch'],

watchArchive: ['preferences', 'watch', 'nochange', 'unwatch'],

watchTaggedUser: ['preferences', 'watch', 'nochange', 'unwatch'],

watchNewCats: ['preferences', 'watch', 'nochange', 'unwatch'],

watchBlockedUser: [true, false],

clerk: [true, false],

log: [true, false],

reversed_log: [true, false],

iUnderstandSectionMoves: [true, false],

tickArchiveWhenCaseClosed: [true, false],

useCheckuserblockAccount: [true, false],

debugForceCheckuserState: [null, true, false],

debugForceAdminState: [null, true, false]

}

// These user settings must be a valid date as defined by MediaWiki API. This is checked for in spiHelperValidateDate() via spiHelperLoadSettings()

const spiHelperSettingsNeedingValidDate = [

'watchCaseExpiry',

'watchArchiveExpiry',

'watchTaggedUserExpiry',

'watchNewCatsExpiry',

'watchBlockedUserExpiry'

]

/* Globals to describe the current SPI page */

/** @type {string} Name of the SPI page in wiki title form

* (e.g. Wikipedia:Sockpuppet investigations/Test) */

let spiHelperPageName = mw.config.get('wgPageName').replace(/_/g, ' ')

/** @type {number} The main page's ID - used to check if the page

* has been edited since we opened it to prevent edit conflicts

*/

let spiHelperStartingRevID = mw.config.get('wgCurRevisionId')

const spiHelperIsThisPageAnArchive = mw.config.get('wgPageName').match('Wikipedia:Sockpuppet_investigations/.*/Archive.*')

/** @type {string} Just the username part of the case */

let spiHelperCaseName

if (spiHelperIsThisPageAnArchive) {

spiHelperCaseName = spiHelperPageName.replace(/Wikipedia:Sockpuppet investigations\//g, ).replace(/\/Archive/, )

} else {

spiHelperCaseName = spiHelperPageName.replace(/Wikipedia:Sockpuppet investigations\//g, '')

}

/** list of section IDs + names corresponding to separate investigations */

let spiHelperCaseSections = []

/** @type {?number} Selected section, "null" means that we're opearting on the entire page */

let spiHelperSectionId = null

/** @type {?string} Selected section's name (e.g. "10 June 2020") */

let spiHelperSectionName = null

/** @type {ParsedArchiveNotice} */

let spiHelperArchiveNoticeParams

/** Map of top-level actions the user has selected */

const spiHelperActionsSelected = {

Case_act: false,

Block: false,

Links: false,

Note: false,

Close: false,

Rename: false,

Archive: false,

SpiMgmt: false

}

/** @type {BlockEntry[]} Requested blocks */

const spiHelperBlocks = []

/** @type {TagEntry[]} Requested tags */

const spiHelperTags = []

/** @type {string[]} Requested global locks */

const spiHelperGlobalLocks = []

// Count of unique users in the case (anything with a checkuser, checkip, user, ip, or vandal template on the page) for the block view

let spiHelperBlockTableUserCount = 0

// Count of unique users in the case (anything with a checkuser, checkip, user, ip, or vandal template on the page) for the link view (seperate needed as extra rows can be added)

let spiHelperLinkTableUserCount = 0

// The current wiki's interwiki prefix

const spiHelperInterwikiPrefix = spiHelperGetInterwikiPrefix()

// Map of active operations (used as a "dirty" flag for beforeunload)

// Values are strings representing the state - acceptable values are 'running', 'success', 'failed'

const spiHelperActiveOperations = new Map()

/* Globals to describe possible options for dropdown menus */

/** @type {SelectOption[]} List of possible selections for tagging a user in the block/tag interface

*/

const spiHelperTagOptions = [

{ label: 'None', selected: true, value: '' },

{ label: 'Suspected sock', value: 'blocked', selected: false },

{ label: 'Proven sock', value: 'proven', selected: false },

{ label: 'CU confirmed sock', value: 'confirmed', selected: false },

{ label: 'Blocked master', value: 'master', selected: false },

{ label: 'CU confirmed master', value: 'sockmasterchecked', selected: false },

{ label: '3X banned master', value: 'bannedmaster', selected: false }

]

/** @type {SelectOption[]} List of possible selections for tagging a user's altmaster in the block/tag interface */

const spiHelperAltMasterTagOptions = [

{ label: 'None', selected: true, value: '' },

{ label: 'Suspected alt master', value: 'suspected', selected: false },

{ label: 'Proven alt master', value: 'proven', selected: false }

]

/** @type {SelectOption[]} List of templates that CUs might insert */

const spiHelperCUTemplates = [

{ label: 'CU templates', selected: true, value: '', disabled: true },

{ label: 'Confirmed', selected: false, value: '{{confirmed}}' },

{ label: 'Confirmed/No Comment', selected: false, value: '{{confirmed-nc}}' },

{ label: 'Indistinguishable', selected: false, value: '{{tallyho}}' },

{ label: 'Likely', selected: false, value: '{{likely}}' },

{ label: 'Possilikely', selected: false, value: '{{possilikely}}' },

{ label: 'Possible', selected: false, value: '{{possible}}' },

{ label: 'Unlikely', selected: false, value: '{{unlikely}}' },

{ label: 'Unrelated', selected: false, value: '{{unrelated}}' },

{ label: 'Inconclusive', selected: false, value: '{{inconclusive}}' },

{ label: 'Need behavioral eval', selected: false, value: '{{behav}}' },

{ label: 'No sleepers', selected: false, value: '{{nosleepers}}' },

{ label: 'Stale', selected: false, value: '{{IPstale}}' },

{ label: 'No comment (IP)', selected: false, value: '{{ncip}}' }

]

/** @type {SelectOption[]} Templates that a clerk or admin might insert */

const spiHelperAdminTemplates = [

{ label: 'Admin/clerk templates', selected: true, value: '', disabled: true },

{ label: 'Duck', selected: false, value: '{{duck}}' },

{ label: 'Megaphone Duck', selected: false, value: '{{megaphone duck}}' },

{ label: 'IP blocked', selected: false, value: '{{IPblock}}' },

{ label: 'Blocked and tagged', selected: false, value: '{{bnt}}' },

{ label: 'Blocked, no tags', selected: false, value: '{{bwt}}' },

{ label: 'Blocked, awaiting tags', selected: false, value: '{{sblock}}' },

{ label: 'Blocked, tagged, closed', selected: false, value: '{{btc}}' },

{ label: 'Requested actions completed, closing', selected: false, value: '{{Action and close}}' },

{ label: 'Closing without action', selected: false, value: '{{Closing without action}}' },

{ label: 'Diffs needed', selected: false, value: '{{DiffsNeeded|moreinfo}}' },

{ label: 'Locks requested', selected: false, value: '{{GlobalLocksRequested}}' }

]

/* Globals for regexes */

// Regex to match the case status, group 1 is the actual status

const spiHelperCaseStatusRegex = /{{\s*SPI case status\s*\|?\s*(\S*?)\s*}}/i

// Regex to match closed case statuses (close or closed)

const spiHelperCaseClosedRegex = /^closed?$/i

const spiHelperClerkStatusRegex = /{{(CURequest|awaitingadmin|clerk ?request|(?:self|requestand|cu)?endorse|inprogress|decline(?:-ip)?|moreinfo|relisted|onhold)}}/i

const spiHelperSockSectionWithNewlineRegex = /====\s*Suspected sockpuppets\s*====\n*/i

const spiHelperAdminSectionWithPrecedingNewlinesRegex = /\n*\s*====\s*Clerk, CheckUser, and\/or patrolling admin comments<\/big>\s*====\s*/i

const spiHelperCUBlockRegex = /{{(checkuserblock(-account|-wide)?|checkuser block)}}/i

const spiHelperArchiveNoticeRegex = /{{\s*SPI\s*archive notice\|(?:1=)?([^|]*?)(\|.*)?}}/i

const spiHelperPriorCasesRegex = /{{spipriorcases}}/i

const spiHelperSectionRegex = /^(?:===[^=]*===|=====[^=]*=====)\s*$/m

// regex to remove hidden characters from form inputs - they mess up some things,

// especially mw.util.isIP

const spiHelperHiddenCharNormRegex = /\u200E/g

/* Other globals */

/** @type{string} Advert to append to the edit summary of edits */

const spihelperAdvert = ' (using spihelper.js)'

/* Used by the link view */

const spiHelperLinkViewURLFormats = {

editorInteractionAnalyser: { baseurl: 'https://sigma.toolforge.org/editorinteract.py', appendToQueryString: , userQueryStringKey: 'users', userQueryStringSeparator: '&', userQueryStringWrapper: , multipleUserQueryStringKeys: true, name: 'Editor Interaction Anaylser' },

interactionTimeline: { baseurl: 'https://interaction-timeline.toolforge.org/', appendToQueryString: 'wiki=enwiki', userQueryStringKey: 'user', userQueryStringSeparator: '&', userQueryStringWrapper: '', multipleUserQueryStringKeys: true, name: 'Interaction Timeline' },

timecardSPITools: { baseurl: 'https://spi-tools.toolforge.org/spi/timecard/' + spiHelperCaseName, appendToQueryString: , userQueryStringKey: 'users', userQueryStringSeparator: '&', userQueryStringWrapper: , multipleUserQueryStringKeys: true, name: 'Timecard comparisons' },

consolidatedTimelineSPITools: { baseurl: 'https://spi-tools.toolforge.org/spi/timecard/' + spiHelperCaseName, appendToQueryString: , userQueryStringKey: 'users', userQueryStringSeparator: '&', userQueryStringWrapper: , multipleUserQueryStringKeys: true, name: 'Consolidated Timeline (requires login)' },

pagesSPITools: { baseurl: 'https://spi-tools.toolforge.org/spi/timeline/' + spiHelperCaseName, appendToQueryString: , userQueryStringKey: 'users', userQueryStringSeparator: '&', userQueryStringWrapper: , multipleUserQueryStringKeys: true, name: 'SPI Tools Pages (requires login)' },

checkUserWikiSearch: { baseurl: 'https://checkuser.wikimedia.org/w/index.php', appendToQueryString: 'ns0=1', userQueryStringKey: 'search', userQueryStringSeparator: ' OR ', userQueryStringWrapper: '"', multipleUserQueryStringKeys: false, name: 'Checkuser wiki search' }

}

/* Actually put the portlets in place if needed */

if (mw.config.get('wgPageName').includes('Wikipedia:Sockpuppet_investigations/') &&

!mw.config.get('wgPageName').includes('Wikipedia:Sockpuppet_investigations/SPI/')) {

mw.loader.load('mediawiki.user')

$(spiHelperAddLink)

}

// Main functions - do the meat of the processing and UI work

const spiHelperTopViewHTML = `

Handling SPI case

`

/**

* Initialization functions for spiHelper, displays the top-level menu

*/

async function spiHelperInit () {

'use strict'

spiHelperCaseSections = await spiHelperGetInvestigationSectionIDs()

// Load archivenotice params

spiHelperArchiveNoticeParams = await spiHelperParseArchiveNotice(spiHelperPageName.replace(/\/Archive/, ''))

// First, insert the template text

displayMessage(spiHelperTopViewHTML)

// Narrow search scope

const $topView = $('#spiHelper_topViewDiv', document)

updateForRole($topView)

if (spiHelperArchiveNoticeParams.username === null) {

// No archive notice was found

const $warningText = $('#spiHelper_warning', $topView)

$warningText.show()

$warningText.append($('').text('Can\'t find archivenotice template! Automatically adding the archive notice to the page.'))

const newArchiveNotice = spiHelperMakeNewArchiveNotice(spiHelperCaseName, { xwiki: false, deny: false, notalk: false })

let pagetext = await spiHelperGetPageText(spiHelperPageName, false)

if (spiHelperPriorCasesRegex.exec(pagetext) === null) {

pagetext = '{{SPIpriorcases}}\n' + pagetext

}

pagetext = newArchiveNotice + '\n' + pagetext

if (pagetext.indexOf('__TOC__') === -1) {

pagetext = '__TOC__\n' + pagetext

}

await spiHelperEditPage(spiHelperPageName, pagetext, 'Adding archive notice', false, spiHelperSettings.watchCase, spiHelperSettings.watchCaseExpiry)

}

// Next, modify what's displayed

// Set the block selection label based on whether or not the user is an admin

$('#spiHelper_blockLabel', $topView).text(spiHelperIsAdmin() ? 'Block/tag socks' : 'Tag socks')

// Wire up a couple of onclick handlers

$('#spiHelper_Move', $topView).on('click', function () {

spiHelperUpdateArchive()

})

$('#spiHelper_Archive', $topView).on('click', function () {

spiHelperUpdateMove()

})

// Generate the section selector

const $sectionSelect = $('#spiHelper_sectionSelect', $topView)

$sectionSelect.on('change', () => {

spiHelperSetCheckboxesBySection()

})

// Add the dates to the selector

for (let i = 0; i < spiHelperCaseSections.length; i++) {

const s = spiHelperCaseSections[i]

$('

}

// All-sections selector...deliberately at the bottom, the default should be the first section

$('

// Only show options suitable for the archive subpage when running on the archives

if (!spiHelperIsThisPageAnArchive) {

$('.spiHelper_notOnArchive', $topView).show()

}

// Set the checkboxes to their default states

spiHelperSetCheckboxesBySection()

$('#spiHelper_GenerateForm', $topView).one('click', () => {

spiHelperGenerateForm()

})

}

const spiHelperActionViewHTML = `

Back to top menu


Handling SPI case

Changing case status

Changing SPI settings

Useful links for socks

Username Interaction analyser Interaction timeline Timecard Consolidated timeline Pages CU wiki
(All users)

Blocking and tagging socks

  • Suppress the usual block summary and only use {{checkuserblock-account}} and {{checkuserblock}} (no effect if "mark blocks as CU blocks" is not checked).

Username Blk? Duration ACB AB/AO NTP NEM Tag Alt Master Req Lock?
(All users)

Marking case as closed

Move section

Archiving case

Comments


`

/**

* Big function to generate the SPI form from the top-level menu selections

*

* Would fail ESlint no-unused-vars due to only being

* referenced in an onclick event

*

* @return {Promise}

*/

// eslint-disable-next-line no-unused-vars

async function spiHelperGenerateForm () {

'use strict'

spiHelperBlockTableUserCount = 0

spiHelperLinkTableUserCount = 0

const $topView = $('#spiHelper_topViewDiv', document)

spiHelperActionsSelected.Case_act = $('#spiHelper_Case_Action', $topView).prop('checked')

spiHelperActionsSelected.Block = $('#spiHelper_BlockTag', $topView).prop('checked')

spiHelperActionsSelected.Link = $('#spiHelper_userInfo', $topView).prop('checked')

spiHelperActionsSelected.Close = $('#spiHelper_Close', $topView).prop('checked')

spiHelperActionsSelected.Note = $('#spiHelper_Comment', $topView).prop('checked') || spiHelperActionsSelected.Case_act || spiHelperActionsSelected.Block || spiHelperActionsSelected.Close

spiHelperActionsSelected.Rename = $('#spiHelper_Move', $topView).prop('checked')

spiHelperActionsSelected.Archive = $('#spiHelper_Archive', $topView).prop('checked')

spiHelperActionsSelected.SpiMgmt = $('#spiHelper_SpiMgmt', $topView).prop('checked')

const pagetext = await spiHelperGetPageText(spiHelperPageName, false, spiHelperSectionId)

if (!(spiHelperActionsSelected.Case_act ||

spiHelperActionsSelected.Note || spiHelperActionsSelected.Close ||

spiHelperActionsSelected.Archive || spiHelperActionsSelected.Block || spiHelperActionsSelected.Link ||

spiHelperActionsSelected.Rename || spiHelperActionsSelected.SpiMgmt)) {

displayMessage('')

return

}

displayMessage(spiHelperActionViewHTML)

// Reduce the scope that jquery operates on

const $actionView = $('#spiHelper_actionViewDiv', document)

updateForRole($actionView)

// Wire up the action view

$('#spiHelper_backLink', $actionView).one('click', () => {

spiHelperInit()

})

if (spiHelperActionsSelected.Case_act) {

const result = spiHelperCaseStatusRegex.exec(pagetext)

let casestatus = ''

if (result) {

casestatus = result[1]

}

const canAddCURequest = (casestatus === '' || /^(?:admin|moreinfo|cumoreinfo|hold|cuhold|clerk|open)$/i.test(casestatus))

const cuRequested = /^(?:CU|checkuser|CUrequest|request|cumoreinfo)$/i.test(casestatus)

const cuEndorsed = /^(?:endorse(d)?)$/i.test(casestatus)

const cuCompleted = /^(?:inprogress|checking|relist(ed)?|checked|completed|declined?|cudeclin(ed)?)$/i.test(casestatus)

/** @type {SelectOption[]} Generated array of values for the case status select box */

const selectOpts = [

{ label: 'No action', value: 'noaction', selected: true }

]

if (spiHelperCaseClosedRegex.test(casestatus)) {

selectOpts.push({ label: 'Reopen', value: 'reopen', selected: false })

} else if (spiHelperIsClerk() && casestatus === 'clerk') {

// Allow clerks to change the status from clerk to open.

// Used when clerk assistance has been given and the case previously had the status 'open'.

selectOpts.push({ label: 'Mark as open', value: 'open', selected: false })

} else if (spiHelperIsAdmin() && casestatus === 'admin') {

// Allow admins to change the status to open from admin

// Used when admin assistance has been given to the non-admin clerk and the case previously had the status 'open'.

selectOpts.push({ label: 'Mark as open', value: 'open', selected: false })

}

if (spiHelperIsCheckuser()) {

selectOpts.push({ label: 'Mark as in progress', value: 'inprogress', selected: false })

}

if (spiHelperIsClerk() || spiHelperIsAdmin()) {

selectOpts.push({ label: 'Request more information', value: 'moreinfo', selected: false })

}

if (canAddCURequest) {

// Statuses only available if the case could be moved to "CU requested"

selectOpts.push({ label: 'Request CU', value: 'CUrequest', selected: false })

if (spiHelperIsClerk()) {

selectOpts.push({ label: 'Request CU and self-endorse', value: 'selfendorse', selected: false })

}

}

// CU already requested

if (cuRequested && spiHelperIsClerk()) {

// Statuses only available if CU has been requested, only clerks + CUs should use these

selectOpts.push({ label: 'Endorse for CU attention', value: 'endorse', selected: false })

// Switch the decline option depending on whether the user is a checkuser

if (spiHelperIsCheckuser()) {

selectOpts.push({ label: 'Endorse CU as a CheckUser', value: 'cuendorse', selected: false })

}

if (spiHelperIsCheckuser()) {

selectOpts.push({ label: 'Decline CU', value: 'cudecline', selected: false })

} else {

selectOpts.push({ label: 'Decline CU', value: 'decline', selected: false })

}

selectOpts.push({ label: 'Request more information for CU', value: 'cumoreinfo', selected: false })

} else if (cuEndorsed && spiHelperIsCheckuser()) {

// Let checkusers decline endorsed cases

if (spiHelperIsCheckuser()) {

selectOpts.push({ label: 'Decline CU', value: 'cudecline', selected: false })

}

selectOpts.push({ label: 'Request more information for CU', value: 'cumoreinfo', selected: false })

}

// This is mostly a CU function, but let's let clerks and admins set it

// in case the CU forgot (or in case we're un-closing))

if (spiHelperIsAdmin() || spiHelperIsClerk()) {

selectOpts.push({ label: 'Mark as checked', value: 'checked', selected: false })

}

if (spiHelperIsClerk() && cuCompleted) {

selectOpts.push({ label: 'Relist for another check', value: 'relist', selected: false })

}

if (spiHelperIsCheckuser()) {

selectOpts.push({ label: 'Place case on CU hold', value: 'cuhold', selected: false })

} else { // I guess it's okay for anyone to have this option

selectOpts.push({ label: 'Place case on hold', value: 'hold', selected: false })

}

selectOpts.push({ label: 'Request clerk action', value: 'clerk', selected: false })

// I think this is only useful for non-admin clerks to ask admins to do stuff

if (!spiHelperIsAdmin() && spiHelperIsClerk()) {

selectOpts.push({ label: 'Request admin action', value: 'admin', selected: false })

}

// Generate the case action options

spiHelperGenerateSelect('spiHelper_CaseAction', selectOpts)

// Add the onclick handler to the drop-down

$('#spiHelper_CaseAction', $actionView).on('change', function (e) {

spiHelperCaseActionUpdated($(e.target))

})

$('#spiHelper_actionView', $actionView).show()

}

if (spiHelperActionsSelected.SpiMgmt) {

const $xwikiBox = $('#spiHelper_spiMgmt_crosswiki', $actionView)

const $denyBox = $('#spiHelper_spiMgmt_deny', $actionView)

const $notalkBox = $('#spiHelper_spiMgmt_notalk', $actionView)

$xwikiBox.prop('checked', spiHelperArchiveNoticeParams.xwiki)

$denyBox.prop('checked', spiHelperArchiveNoticeParams.deny)

$notalkBox.prop('checked', spiHelperArchiveNoticeParams.notalk)

$('#spiHelper_spiMgmtView', $actionView).show()

}

if (spiHelperActionsSelected.Close) {

$('#spiHelper_closeView', $actionView).show()

}

if (spiHelperActionsSelected.Archive) {

$('#spiHelper_archiveView', $actionView).show()

}

// Only give the option to comment if we selected a specific section and we are not running on an archive subpage

if (spiHelperSectionId && spiHelperActionsSelected.Note && !spiHelperIsThisPageAnArchive) {

// generate the note prefixes

/** @type {SelectOption[]} */

const spiHelperNoteTemplates = [

{ label: 'Comment templates', selected: true, value: '', disabled: true }

]

if (spiHelperIsClerk()) {

spiHelperNoteTemplates.push({ label: 'Clerk note', selected: false, value: 'clerknote' })

}

if (spiHelperIsAdmin()) {

spiHelperNoteTemplates.push({ label: 'Administrator note', selected: false, value: 'adminnote' })

}

if (spiHelperIsCheckuser()) {

spiHelperNoteTemplates.push({ label: 'CU note', selected: false, value: 'cunote' })

}

spiHelperNoteTemplates.push({ label: 'Note', selected: false, value: 'takenote' })

// Wire up the select boxes

spiHelperGenerateSelect('spiHelper_noteSelect', spiHelperNoteTemplates)

$('#spiHelper_noteSelect', $actionView).on('change', function (e) {

spiHelperInsertNote($(e.target))

})

spiHelperGenerateSelect('spiHelper_adminSelect', spiHelperAdminTemplates)

$('#spiHelper_adminSelect', $actionView).on('change', function (e) {

spiHelperInsertTextFromSelect($(e.target))

})

spiHelperGenerateSelect('spiHelper_cuSelect', spiHelperCUTemplates)

$('#spiHelper_cuSelect', $actionView).on('change', function (e) {

spiHelperInsertTextFromSelect($(e.target))

})

$('#spiHelper_previewLink', $actionView).on('click', function () {

spiHelperPreviewText()

})

$('#spiHelper_commentView', $actionView).show()

}

if (spiHelperActionsSelected.Rename) {

if (spiHelperSectionId) {

$('#spiHelper_moveHeader', $actionView).text('Move section "' + spiHelperSectionName + '"')

} else {

$('#spiHelper_moveHeader', $actionView).text('Move/merge full case')

}

$('#spiHelper_moveView', $actionView).show()

}

if (spiHelperActionsSelected.Block || spiHelperActionsSelected.Link) {

// eslint-disable-next-line no-useless-escape

const checkuserRegex = /{{\s*check(?:user|ip)\s*\|\s*(?:1=)?\s*([^\|}]*?)\s*(?:\|master name\s*=\s*.*)?}}/gi

const results = pagetext.match(checkuserRegex)

const likelyusers = []

const likelyips = []

const possibleusers = []

const possibleips = []

likelyusers.push(spiHelperCaseName)

if (results) {

for (let i = 0; i < results.length; i++) {

const username = spiHelperNormalizeUsername(results[i].replace(checkuserRegex, '$1'))

const isIP = mw.util.isIPAddress(username, true)

if (!isIP && !likelyusers.includes(username)) {

likelyusers.push(username)

} else if (isIP && !likelyips.includes(username)) {

if (spiHelperSettings.displayIPv6As64 && mw.util.isIPv6Address(username, false)) {

likelyips.push(username.split(':').slice(0, 4).concat('0', '0', '0', '0').join(':') + '/64')

continue

}

likelyips.push(username)

}

}

}

const unnamedParameterRegex = /^\s*\d+\s*$/i

const socklistResults = pagetext.match(/{{\s*sock\s?list\s*([^}]*)}}/gi)

if (socklistResults) {

for (let i = 0; i < socklistResults.length; i++) {

const socklistMatch = socklistResults[i].match(/{{\s*sock\s?list\s*([^}]*)}}/i)[1]

// First split the text into parts based on the presence of a |

const socklistArguments = socklistMatch.split('|')

for (let j = 0; j < socklistArguments.length; j++) {

// Now try to split based on "=", if wasn't able to it means it's an unnamed argument

const splitArgument = socklistArguments[j].split('=')

let username = ''

if (splitArgument.length === 1) {

username = spiHelperNormalizeUsername(splitArgument[0])

} else if (unnamedParameterRegex.test(splitArgument[0])) {

username = spiHelperNormalizeUsername(splitArgument.slice(1).join('='))

}

if (username !== '') {

const isIP = mw.util.isIPAddress(username, true)

if (isIP && !likelyips.includes(username)) {

if (spiHelperSettings.displayIPv6As64 && mw.util.isIPv6Address(username, false)) {

likelyips.push(username.split(':').slice(0, 4).concat('0', '0', '0', '0').join(':') + '/64')

continue

}

likelyips.push(username)

} else if (!isIP && !likelyusers.includes(username)) {

likelyusers.push(username)

}

}

}

}

}

// eslint-disable-next-line no-useless-escape

const userRegex = /{{[^\|}{]*?(?:user|vandal|IP|noping)[^\|}{]*?\|\s*(?:1=)?\s*([^\|}]*?)\s*}}/gi

const userresults = pagetext.match(userRegex)

if (userresults) {

for (let i = 0; i < userresults.length; i++) {

const username = spiHelperNormalizeUsername(userresults[i].replace(userRegex, '$1'))

const isIP = mw.util.isIPAddress(username, true)

if (isIP && !possibleips.includes(username) &&

!likelyips.includes(username)) {

possibleips.push(username)

} else if (!isIP && !possibleusers.includes(username) &&

!likelyusers.includes(username)) {

possibleusers.push(username)

}

}

}

if (spiHelperActionsSelected.Block) {

// Show generation in progress so not to make people think its broken

$('#spiHelper_blockTagView', $actionView).show()

if (spiHelperIsAdmin()) {

$('#spiHelper_blockTagHeader', $actionView).text('Blocking and tagging socks')

} else {

$('#spiHelper_blockTagHeader', $actionView).text('Tagging socks')

}

// Wire up the "select all" options

$('#spiHelper_block_doblock', $actionView).on('click', function (e) {

spiHelperSetAllTableColumnOpts($(e.target), 'block')

})

$('#spiHelper_block_acb', $actionView).on('click', function (e) {

spiHelperSetAllTableColumnOpts($(e.target), 'block')

})

$('#spiHelper_block_ab', $actionView).on('click', function (e) {

spiHelperSetAllTableColumnOpts($(e.target), 'block')

})

$('#spiHelper_block_tp', $actionView).on('click', function (e) {

spiHelperSetAllTableColumnOpts($(e.target), 'block')

})

$('#spiHelper_block_email', $actionView).on('click', function (e) {

spiHelperSetAllTableColumnOpts($(e.target), 'block')

})

$('#spiHelper_block_lock', $actionView).on('click', function (e) {

spiHelperSetAllTableColumnOpts($(e.target), 'block')

})

$('#spiHelper_block_lock', $actionView).on('click', function (e) {

spiHelperSetAllTableColumnOpts($(e.target), 'block')

})

spiHelperGenerateSelect('spiHelper_block_tag', spiHelperTagOptions)

$('#spiHelper_block_tag', $actionView).on('change', function (e) {

spiHelperSetAllTableColumnOpts($(e.target), 'block')

})

spiHelperGenerateSelect('spiHelper_block_tag_altmaster', spiHelperAltMasterTagOptions)

$('#spiHelper_block_tag_altmaster', $actionView).on('change', function (e) {

spiHelperSetAllTableColumnOpts($(e.target), 'block')

})

$('#spiHelper_block_lock', $actionView).on('click', function (e) {

spiHelperSetAllTableColumnOpts($(e.target), 'block')

})

for (let i = 0; i < likelyusers.length; i++) {

spiHelperBlockTableUserCount++

await spiHelperGenerateBlockTableLine(likelyusers[i], true, spiHelperBlockTableUserCount)

}

for (let i = 0; i < likelyips.length; i++) {

spiHelperBlockTableUserCount++

await spiHelperGenerateBlockTableLine(likelyips[i], true, spiHelperBlockTableUserCount)

}

for (let i = 0; i < possibleusers.length; i++) {

spiHelperBlockTableUserCount++

await spiHelperGenerateBlockTableLine(possibleusers[i], false, spiHelperBlockTableUserCount)

}

for (let i = 0; i < possibleips.length; i++) {

spiHelperBlockTableUserCount++

await spiHelperGenerateBlockTableLine(possibleips[i], false, spiHelperBlockTableUserCount)

}

}

if (spiHelperActionsSelected.Link) {

// Wire up the "select all" options

$('#spiHelper_link_editorInteractionAnalyser', $actionView).on('click', function (e) {

spiHelperSetAllTableColumnOpts($(e.target), 'link')

})

$('#spiHelper_link_interactionTimeline', $actionView).on('click', function (e) {

spiHelperSetAllTableColumnOpts($(e.target), 'link')

})

$('#spiHelper_link_timecardSPITools', $actionView).on('click', function (e) {

spiHelperSetAllTableColumnOpts($(e.target), 'link')

})

$('#spiHelper_link_consolidatedTimelineSPITools', $actionView).on('click', function (e) {

spiHelperSetAllTableColumnOpts($(e.target), 'link')

})

$('#spiHelper_link_pagesSPITools', $actionView).on('click', function (e) {

spiHelperSetAllTableColumnOpts($(e.target), 'link')

})

$('#spiHelper_link_checkUserWikiSearch', $actionView).on('click', function (e) {

spiHelperSetAllTableColumnOpts($(e.target), 'link')

})

for (let i = 0; i < likelyusers.length; i++) {

spiHelperLinkTableUserCount++

await spiHelperGenerateLinksTableLine(likelyusers[i], spiHelperLinkTableUserCount)

}

for (let i = 0; i < likelyips.length; i++) {

spiHelperLinkTableUserCount++

await spiHelperGenerateLinksTableLine(likelyips[i], spiHelperLinkTableUserCount)

}

for (let i = 0; i < possibleusers.length; i++) {

spiHelperLinkTableUserCount++

await spiHelperGenerateLinksTableLine(possibleusers[i], spiHelperLinkTableUserCount)

}

for (let i = 0; i < possibleips.length; i++) {

spiHelperLinkTableUserCount++

await spiHelperGenerateLinksTableLine(possibleips[i], spiHelperLinkTableUserCount)

}

$('#spiHelper_sockLinksView', $actionView).show()

}

$('#spiHelper_blockTagView', $actionView).show()

}

// Wire up the submit button

$('#spiHelper_performActions', $actionView).one('click', () => {

spiHelperPerformActions()

})

}

/**

* Update the view for the roles of the person running the script

* by selectively hiding.

* view: @type JQuery object representing the class / id for the view

*/

async function updateForRole (view) {

// Hide items based on role

if (!spiHelperIsCheckuser()) {

// Hide CU options from non-CUs

$('.spiHelper_cuClass', view).hide()

}

if (!spiHelperIsAdmin()) {

// Hide block options from non-admins

$('.spiHelper_adminClass', view).hide()

}

if (!(spiHelperIsAdmin() || spiHelperIsClerk())) {

$('.spiHelper_adminClerkClass', view).hide()

}

}

/**

* Archives everything on the page that's eligible for archiving

*/

async function spiHelperOneClickArchive () {

'use strict'

spiHelperActiveOperations.set('oneClickArchive', 'running')

const pagetext = await spiHelperGetPageText(spiHelperPageName, false)

spiHelperCaseSections = await spiHelperGetInvestigationSectionIDs()

if (!spiHelperSectionRegex.test(pagetext)) {

alert('Looks like the page has been archived already.')

spiHelperActiveOperations.set('oneClickArchive', 'successful')

return

}

displayMessage('