User:GeneralNotability/InvestorGoat.js
Appearance
Code that you insert on this page could contain malicious content capable of compromising your account. If you import a script from another page with "importScript", "mw.loader.load", "iusc", or "lusc", take note that this causes you to dynamically load a remote script, which could be changed by others. Editors are responsible for all edits and actions they perform, including by scripts. User scripts are not centrally supported and may malfunction or become inoperable due to software changes. A guide to help you find broken scripts is available. If you are unsure whether code you are adding to this page is safe, you can ask at the appropriate village pump. This code will be executed when previewing this page. |
Documentation for this user script can be added at User:GeneralNotability/InvestorGoat. |
// <nowiki>
// InvestorGoat, a suite of CheckUser tools.
// Parts taken from https://en.wikipedia.org/wiki/User:Firefly/checkuseragenthelper.js
/* global mw, $, UAParser */
$(async function ($) {
if (mw.config.get('wgCanonicalSpecialPageName') && mw.config.get('wgCanonicalSpecialPageName').startsWith('CheckUser')) {
await mw.loader.using('mediawiki.util')
InvestorGoatPrepUAs()
mw.hook('wikipage.content').add(InvestorGoatIPHook)
mw.hook('wikipage.content').add(InvestorGoatAddQuickReasonBoxHook)
}
mw.hook('wikipage.content').add(InvestorGoatHighlightLogs)
})
const InvestorGoatwmbApiKey = mw.user.options.get('userjs-wmbkey')
const InvestorGoatUAMap = new Map()
let InvestorGoatCompareTarget = null
async function InvestorGoatPrepUAs () {
await mw.loader.getScript('https://en.wikipedia.org/w/index.php?title=User:GeneralNotability/ua-parser.min.js&action=raw&ctype=text/javascript')
$('.mw-checkuser-agent').each(async function () {
const $el = $(this)
const ua = $el.text()
// Add indicators
$('<span>').addClass('InvestorGoat-device').text('DEV').css({ margin: '2px', 'user-select': 'none' }).appendTo($el.parent())
$('<span>').addClass('InvestorGoat-OS').text('OS').css({ margin: '2px', 'user-select': 'none' }).appendTo($el.parent())
$('<span>').addClass('InvestorGoat-browser').text('BR').css({ margin: '2px', 'user-select': 'none' }).appendTo($el.parent())
if (InvestorGoatwmbApiKey) {
if (!InvestorGoatUAMap.has(ua)) {
InvestorGoatUAMap.set(ua, null)
}
} else {
const parser = new UAParser()
if (!InvestorGoatUAMap.has(ua)) {
parser.setUA(ua)
const uaObj = parser.getResult()
InvestorGoatUAMap.set(ua, uaObj)
}
}
// Set up dummy link
const $link = $('<a>').attr('href', '#').on('click', InvestorGoatUAClicked)
$link.insertAfter($el)
$el.detach()
$link.append($el)
})
if (InvestorGoatwmbApiKey && InvestorGoatUAMap.size > 0) {
// Do a batch query if we're using the WhatIsMyBrowser API
const query = {}
query.user_agents = {}
query.parse_options = {
return_metadata_for_useragent: true
}
for (const [key, _] of InvestorGoatUAMap.entries()) {
query.user_agents[key] = key
}
const result = await $.ajax({
type: 'post',
url: 'https://api.whatismybrowser.com/api/v2/user_agent_parse_batch',
data: JSON.stringify(query),
headers: {
'x-api-key': InvestorGoatwmbApiKey
}
})
console.log(result)
for (const [key, value] of Object.entries(result.parses)) {
InvestorGoatUAMap.set(key, value)
}
// Flag interesting results
$('.mw-checkuser-agent').each(async function () {
const $el = $(this)
const ua = $el.text()
const val = InvestorGoatUAMap.get(ua)
// Add indicators
$('<span>').addClass('InvestorGoat-seen').text('#️⃣').css({ margin: '2px', 'user-select': 'none' })
.attr('title', `Estimated popularity: ${Math.pow(10, Math.floor(Math.log10(val.user_agent_metadata.times_seen))).toLocaleString()}s`).appendTo($el.parent().parent())
if (val.parse.is_weird) {
$('<span>').addClass('InvestorGoat-weird').text('❓').css({ margin: '2px', 'user-select': 'none' })
.attr('title', `UA flagged as weird: ${val.parse.is_weird_reason_code}`).appendTo($el.parent().parent())
}
if (val.parse.is_abusive) {
$('<span>').addClass('InvestorGoat-abusive').text('‼️').css({ margin: '2px', 'user-select': 'none' })
.attr('title', 'UA flagged as abusive').appendTo($el.parent().parent())
}
if (val.parse.is_spam) {
$('<span>').addClass('InvestorGoat-spam').text('🥫').css({ margin: '2px', 'user-select': 'none' })
.attr('title', 'UA flagged as spam').appendTo($el.parent().parent())
}
if (val.parse.is_restricted) {
$('<span>').addClass('InvestorGoat-restricted').text('🛑').css({ margin: '2px', 'user-select': 'none' })
.attr('title', 'UA flagged as restricted').appendTo($el.parent().parent())
}
if (val.parse.software_type === 'bot') {
$('<span>').addClass('InvestorGoat-bot').text('🤖').css({ margin: '2px', 'user-select': 'none' })
.attr('title', `UA flagged as bot: ${val.parse.software_sub_type}`).appendTo($el.parent().parent())
}
})
}
}
async function InvestorGoatUAClicked (event) {
InvestorGoatCompareTarget = InvestorGoatUAMap.get($(event.target).text())
if (InvestorGoatwmbApiKey) {
InvestorGoatUpdateUAColorsWmb()
} else {
InvestorGoatUpdateUAColors()
}
event.preventDefault()
}
async function InvestorGoatUpdateUAColors () {
const match = { backgroundColor: 'DarkGreen', color: 'white' }
const familyMismatch = { backgroundColor: 'DarkRed', color: 'white' }
const majorVersionSelectedAhead = { backgroundColor: 'orange', color: 'black' }
$('.mw-checkuser-agent').each(async function () {
const $el = $(this)
const uaObj = InvestorGoatUAMap.get($el.text())
const $devEl = $el.parent().parent().children('.InvestorGoat-device')
if (uaObj.device.type !== InvestorGoatCompareTarget.device.type) {
$devEl.css(familyMismatch).attr('title', `Device type mismatch, this is ${uaObj.device.type}, selected is ${InvestorGoatCompareTarget.device.type}`)
} else if (uaObj.device.vendor !== InvestorGoatCompareTarget.device.vendor) {
$devEl.css(majorVersionSelectedAhead).attr('title', `Device vendor mismatch, this is ${uaObj.device.vendor}, selected is ${InvestorGoatCompareTarget.device.vendor}`)
} else if (uaObj.device.version !== InvestorGoatCompareTarget.device.version) {
$devEl.css(majorVersionSelectedAhead).attr('title', `Device version mismatch, this is ${uaObj.device.version}, selected is ${InvestorGoatCompareTarget.device.version}`)
} else {
$devEl.css(match).attr('title', 'Devices match')
}
const $osEl = $el.parent().parent().children('.InvestorGoat-OS')
if (uaObj.os.name !== InvestorGoatCompareTarget.os.name) {
$osEl.css(familyMismatch).attr('title', `OS mismatch, selected is ${InvestorGoatCompareTarget.os.name}`)
} else if (uaObj.os.version !== InvestorGoatCompareTarget.os.version) {
$osEl.css(majorVersionSelectedAhead).attr('title', `OS version mismatch, selected is ${InvestorGoatCompareTarget.os.version}`)
} else {
$osEl.css(match).attr('title', 'OSes match')
}
const $browserEl = $el.parent().parent().children('.InvestorGoat-browser')
if (uaObj.browser.name !== InvestorGoatCompareTarget.browser.name) {
$browserEl.css(familyMismatch).attr('title', `Browser mismatch, selected is ${InvestorGoatCompareTarget.browser.name}`)
} else if (uaObj.browser.version !== InvestorGoatCompareTarget.browser.version) {
$browserEl.css(majorVersionSelectedAhead).attr('title', `Browser version mismatch, selected is ${InvestorGoatCompareTarget.browser.version}`)
} else {
$browserEl.css(match).attr('title', 'Browsers match')
}
})
}
async function InvestorGoatUpdateUAColorsWmb () {
const match = { backgroundColor: 'DarkGreen', color: 'white' }
const totalMismatch = { backgroundColor: 'DarkRed', color: 'white' }
const majorMismatch = { backgroundColor: 'orange', color: 'black' }
const minorMismatch = { backgroundColor: 'Yellow', color: 'black' }
const versionSelectedBehind = { backgroundColor: 'DarkBlue', color: 'white' }
const versionSelectedAhead = { backgroundColor: 'Yellow', color: 'black' }
$('.mw-checkuser-agent').each(async function () {
const $el = $(this)
const uaObj = InvestorGoatUAMap.get($el.text())
const $devEl = $el.parent().parent().children('.InvestorGoat-device')
if (uaObj.parse.hardware_type !== InvestorGoatCompareTarget.parse.hardware_type) {
$devEl.css(totalMismatch).attr('title', `Device type mismatch, this is "${uaObj.parse.hardware_type}", selected is "${InvestorGoatCompareTarget.parse.hardware_type}"`)
} else if (uaObj.parse.hardware_sub_type !== InvestorGoatCompareTarget.parse.hardware_sub_type) {
$devEl.css(majorMismatch).attr('title', `Device subtype mismatch, this is "${uaObj.parse.hardware_sub_type}", selected is "${InvestorGoatCompareTarget.parse.hardware_sub_type}"`)
} else if (uaObj.parse.hardware_sub_sub_type !== InvestorGoatCompareTarget.parse.hardware_sub_sub_type) {
$devEl.css(minorMismatch).attr('title', `Device sub-subtype mismatch, this is "${uaObj.parse.hardware_sub_sub_type}", selected is "${InvestorGoatCompareTarget.parse.hardware_sub_sub_type}"`)
} else if (uaObj.parse.simple_operating_platform_string !== InvestorGoatCompareTarget.parse.simple_operating_platform_string) {
$devEl.css(totalMismatch).attr('title', `Platform mismatch, this is "${uaObj.parse.simple_operating_platform_string}", selected is "${InvestorGoatCompareTarget.parse.simple_operating_platform_string}"`)
} else {
$devEl.css(match).attr('title', 'Devices match')
}
const $osEl = $el.parent().parent().children('.InvestorGoat-OS')
if (uaObj.parse.operating_system_name_code !== InvestorGoatCompareTarget.parse.operating_system_name_code) {
$osEl.css(totalMismatch).attr('title', `OS mismatch, this is "${uaObj.parse.operating_system_name}", selected is ${InvestorGoatCompareTarget.parse.operating_system_name}`)
} else if (uaObj.parse.operating_system_flavour_code !== InvestorGoatCompareTarget.parse.operating_system_flavour_code) {
$osEl.css(majorMismatch).attr('title', `OS flavor mismatch, this is "${uaObj.parse.operating_system_flavour}", selected is ${InvestorGoatCompareTarget.parse.operating_system_flavour}`)
} else {
let mismatch = false
for (let i = 0; i < uaObj.parse.operating_system_version_full.length; i++) {
if (i >= InvestorGoatCompareTarget.parse.operating_system_version_full.length) {
break
}
if (parseInt(uaObj.parse.operating_system_version_full[i]) < parseInt(InvestorGoatCompareTarget.parse.operating_system_version_full[i])) {
$osEl.css(versionSelectedAhead).attr('title', 'OS version mismatch, selected version is newer than this')
mismatch = true
break
} else if (parseInt(uaObj.parse.operating_system_version_full[i]) > parseInt(InvestorGoatCompareTarget.parse.operating_system_version_full[i])) {
$osEl.css(versionSelectedBehind).attr('title', 'OS version mismatch, selected version is older than this')
mismatch = true
break
}
}
if (!mismatch) {
$osEl.css(match).attr('title', 'OSes match')
}
}
const $browserEl = $el.parent().parent().children('.InvestorGoat-browser')
if (uaObj.parse.software_type !== InvestorGoatCompareTarget.parse.software_type) {
$browserEl.css(totalMismatch).attr('title', `Software type mismatch, this is "${uaObj.parse.software_type}", selected is ${InvestorGoatCompareTarget.parse.software_type}`)
} else if (uaObj.parse.software_sub_type !== InvestorGoatCompareTarget.parse.software_sub_type) {
$browserEl.css(totalMismatch).attr('title', `Software subtype mismatch, this is "${uaObj.parse.software_sub_type}", selected is ${InvestorGoatCompareTarget.parse.software_sub_type}`)
} else if (uaObj.parse.software_name_code !== InvestorGoatCompareTarget.parse.software_name_code) {
$browserEl.css(totalMismatch).attr('title', `Browser mismatch, this is "${uaObj.parse.software_name}", selected is ${InvestorGoatCompareTarget.parse.software_name}`)
} else {
let mismatch = false
for (let i = 0; i < uaObj.parse.software_version_full.length; i++) {
if (i >= InvestorGoatCompareTarget.parse.software_version_full.length) {
break
}
if (parseInt(uaObj.parse.software_version_full[i]) < parseInt(InvestorGoatCompareTarget.parse.software_version_full[i])) {
$browserEl.css(versionSelectedAhead).attr('title', 'Browser version mismatch, selected version is newer than this')
mismatch = true
break
} else if (parseInt(uaObj.parse.software_version_full[i]) > parseInt(InvestorGoatCompareTarget.parse.software_version_full[i])) {
$browserEl.css(versionSelectedBehind).attr('title', 'Browser version mismatch, selected version is older than this')
mismatch = true
break
}
}
if (!mismatch) {
$browserEl.css(match).attr('title', 'Browsers match')
}
}
})
}
const InvestorGoatSPECIALIPS = [
{ addr: '10.0.0.0', cidr: 8, color: 'red', hint: 'Internal IP' },
{ addr: '172.16.0.0', cidr: 12, color: 'red', hint: 'Internal IP' },
{ addr: '192.168.0.0', cidr: 16, color: 'red', hint: 'Internal IP' },
{ addr: '127.0.0.0', cidr: 8, color: 'red', hint: 'Loopback (WTF?)' },
{ addr: '185.15.56.0', cidr: 22, color: 'yellow', hint: 'WMF IP' },
{ addr: '91.198.174.0', cidr: 24, color: 'yellow', hint: 'WMF IP' },
{ addr: '198.35.26.0', cidr: 23, color: 'yellow', hint: 'WMF IP' },
{ addr: '208.80.152.0', cidr: 22, color: 'yellow', hint: 'WMF IP' },
{ addr: '103.102.166.0', cidr: 24, color: 'yellow', hint: 'WMF IP' },
{ addr: '143.228.0.0', cidr: 16, color: 'orange', hint: 'US Congress' },
{ addr: '12.185.56.0', cidr: 29, color: 'orange', hint: 'US Congress' },
{ addr: '12.147.170.144', cidr: 28, color: 'orange', hint: 'US Congress' },
{ addr: '74.119.128.0', cidr: 22, color: 'orange', hint: 'US Congress' },
{ addr: '156.33.0.0', cidr: 16, color: 'orange', hint: 'US Congress' },
{ addr: '165.119.0.0', cidr: 16, color: 'orange', hint: 'Executive Office of the President' },
{ addr: '198.137.240.0', cidr: 23, color: 'orange', hint: 'Executive Office of the President' },
{ addr: '204.68.207.0', cidr: 24, color: 'orange', hint: 'Executive Office of the President' },
{ addr: '149.101.0.0', cidr: 16, color: 'orange', hint: 'US Department of Justice' },
{ addr: '65.165.132.0', cidr: 24, color: 'orange', hint: 'US Dept of Homeland Security' },
{ addr: '204.248.24.0', cidr: 24, color: 'orange', hint: 'US Dept of Homeland Security' },
{ addr: '216.81.80.0', cidr: 20, color: 'orange', hint: 'US Dept of Homeland Security' },
{ addr: '131.132.0.0', cidr: 14, color: 'orange', hint: 'Canadian Dept of National Defence' },
{ addr: '131.136.0.0', cidr: 14, color: 'orange', hint: 'Canadian Dept of National Defence' },
{ addr: '131.140.0.0', cidr: 15, color: 'orange', hint: 'Canadian Dept of National Defence' },
{ addr: '192.197.82.0', cidr: 24, color: 'orange', hint: 'Canadian House of Commons' },
{ addr: '194.60.0.0', cidr: 18, color: 'orange', hint: 'UK Parliament' },
{ addr: '138.162.0.0', cidr: 16, color: 'orange', hint: 'US Department of the Navy' }
]
function InvestorGoatIsIPInRange (addr, targetRange, targetCidr) {
// https://stackoverflow.com/questions/503052/javascript-is-ip-in-one-of-these-subnets
const mask = -1 << (32 - +targetCidr) // eslint-disable-line no-bitwise
if (mw.util.isIPv4Address(addr, false)) {
const addrMatch = addr.match(/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/)
const addrInt = (+addrMatch[1] << 24) + (+addrMatch[2] << 16) + (+addrMatch[3] << 8) + // eslint-disable-line no-bitwise
(+addrMatch[4]) // eslint-disable-line no-bitwise
const targetMatch = targetRange.match(/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/)
const targetInt = (+targetMatch[1] << 24) + (+targetMatch[2] << 16) + (+targetMatch[3] << 8) + // eslint-disable-line no-bitwise
(+targetMatch[4]) // eslint-disable-line no-bitwise
return (addrInt & mask) === (targetInt & mask) // eslint-disable-line no-bitwise
}
// TODO: figure out ipv6
}
/**
* Get all IP userlinks on the page
*
* @param {JQuery} $content page contents
* @return {Map} list of unique users on the page and their corresponding links
*/
function InvestorGoatGetBlockLinks ($content) {
const userLinks = new Map()
const blockLinkRe = '^Special:Block/(.*)$'
$('a', $content).each(function () {
if (!$(this).attr('title')) {
// Ignore if the <a> doesn't have a title
return
}
const blockLinkMatch = $(this).attr('title').toString().match(blockLinkRe)
if (!blockLinkMatch) {
return
}
const user = decodeURIComponent(blockLinkMatch[1])
if (mw.util.isIPAddress(user)) {
if (!userLinks.get(user)) {
userLinks.set(user, [])
}
userLinks.get(user).push($(this))
}
})
return userLinks
}
async function InvestorGoatIPHook ($content) {
const usersOnPage = InvestorGoatGetBlockLinks($content)
usersOnPage.forEach(async (val, key, _) => {
let color = ''
let hint = ''
for (const range of InvestorGoatSPECIALIPS) {
if (InvestorGoatIsIPInRange(key, range.addr, range.cidr)) {
color = range.color
hint = range.hint
break
}
}
if (!color) {
return
}
val.forEach(($link) => {
$link.css({ backgroundColor: color })
$link.attr('title', hint)
})
})
}
const InvestorGoatCHECKREASONS = [
{ label: 'Check type (optional)', selected: true, value: '', disabled: true },
{ label: 'SPI-related', selected: false, value: 'spi' },
{ label: 'Second Opinion', selected: false, value: '2o' },
{ label: 'Unblock Request', selected: false, value: 'unblock' },
{ label: 'IPBE Request', selected: false, value: 'ipbe' },
{ label: 'ACC Request', selected: false, value: 'acc' },
{ label: 'CU/Paid Queue', selected: false, value: 'q' },
{ label: 'Suspected known sockmaster', selected: false, value: 'sock' },
{ label: 'Suspected LTA', selected: false, value: 'lta' },
{ label: 'Comparison to ongoing CU', selected: false, value: 'comp' },
{ label: 'Collateral check', selected: false, value: 'coll' },
{ label: 'Cross-wiki request', selected: false, value: 'xwiki' },
{ label: 'Discretionary check', selected: false, value: 'fish' },
{ label: 'Self-check for testing', selected: false, value: 'test' }
]
function InvestorGoatAddQuickReasonBoxHook ($content) {
const $select = $('<select>')
for (const reason of InvestorGoatCHECKREASONS) {
$('<option>')
.val(reason.value)
.prop('selected', reason.selected)
.text(reason.label)
.prop('disabled', reason.disabled)
.appendTo($select)
}
$select.on('change', function (e) {
InvestorGoatAddQuickReason($(e.target))
})
const $target = $('[name=reason]', $content)
$select.insertBefore($target)
}
/**
* Highlight CU log links where the log exists
*
* @param {JQuery} $content page contents
*/
function InvestorGoatHighlightLogs ($content) {
const cuSearchTargetRe = 'cuSearch=(.*)'
$('a.external.text', $content).each(async function () {
if (!$(this).attr('href')) {
// Ignore if the <a> doesn't have a title
return
}
const cuTargetMatch = $(this).attr('href').toString().match(cuSearchTargetRe)
if (!cuTargetMatch) {
return
}
const target = decodeURIComponent(cuTargetMatch[1]).replaceAll('+', ' ')
const api = new mw.Api()
const request = {
action: 'query',
list: 'checkuserlog',
cultarget: target,
cullimit: 100
}
try {
const response = await api.get(request)
const filteredEntries = response.query.checkuserlog.entries.filter(entry =>
!entry.target.includes('/') || parseInt(entry.target.split('/')[1]) >= 16)
const checkCount = filteredEntries.length
if (checkCount > 0) {
$(this).attr('title', `${checkCount} checks`)
let color = ''
if (response.query.checkuserlog.entries[0].checkuser === mw.config.get('wgUserName')) {
color = 'lightskyblue'
} else {
color = 'lightgreen'
}
// Have to use .style vice .css because .css doesn't understand !important
$(this).attr('style', `background-color: ${color} !important`)
}
} catch (error) {
console.log(`Error checking CU log: ${error}`)
}
})
}
function InvestorGoatAddQuickReason (source) {
const $inputField = $('[name=reason]')
$inputField.val('[' + source.val() + '] ' + $inputField.val())
}
// </nowiki>