User:Bugghost/Scripts/UserRoleIndicator.js

//

// Copied and edited from Novem Linguae's user highlighter simple: User:Novem_Linguae/Scripts/UserHighlighterSimple.js

class UserRoleIndicator {

/**

* @param {jQuery} $ jquery

* @param {Object} mw mediawiki

* @param {Window} window

*/

constructor( $, mw, window ) {

// eslint-disable-next-line no-jquery/variable-pattern

this.$ = $;

this.mw = mw;

this.window = window;

this.linksChecked = new Set([]);

}

async execute() {

//console.time("uri");

const defaultRoleInfoLookup = {

wmf: ['๐ŸŒ', 'Wikimedia Foundation (WMF)'],

bot: ['๐Ÿค–', 'Bot'],

stewards: ['๐Ÿฉบ', 'Steward or Ombud'],

arbcom: ['โš–๏ธ', 'Arbitration Committee member'],

bureaucrats: ['๐Ÿ’ผ', 'Bureaucrat'],

checkUsers: ['๐Ÿ•ต๏ธ๏ธ๏ธ๏ธ', 'Check User'],

admins: ['๐Ÿงน', 'Admin'],

formerAdmins: ['๐Ÿšฌ', 'Former Admin'],

newPageReviewers: ['๐Ÿงบ', 'New page reviewer'],

tenThousandEdits: ['๐Ÿ“š', 'More than 10,000 edits'],

extendedConfirmed: ['๐Ÿ“˜', 'Extended confirmed'],

lessThan500: ['๐Ÿฃ', 'Less than 500 edits'],

};

if(this.window.UserRoleIndicatorCustomLabels){

this.roleInfoLookup = { ...defaultRoleInfoLookup, ...window.UserRoleIndicatorCustomLabels };

}else{

this.roleInfoLookup = defaultRoleInfoLookup;

}

this.labelPosition = "after";

if(this.window.UserRoleIndicatorCustomPlacement){

this.labelPosition = this.window.UserRoleIndicatorCustomPlacement;

}

//console.time("get usernames")

await this.getUsernames();

//console.timeEnd("get usernames")

this.addCSS('user-role-indicator', 'font-size: smaller; display: inline; background: #b7b9ff55; padding: 0.1em; border-radius: 5px;')

this.addCSS('label-after', 'margin-left:3px;');

this.addCSS('label-before', 'margin-right:1px;margin-left:2px;');

const $links = this.$( '#article a, #bodyContent a, #mw_contentholder a' );

//console.time("linkloop")

$links.each( ( index, element ) => {

this.$link = this.$( element );

if(this.linksChecked.has(element)){

return;

}

this.linksChecked.add(element);

if ( !this.linksToAUser() ) {

return;

}

this.user = this.getUserName();

const isUserSubpage = this.user.includes( '/' );

if ( isUserSubpage ) {

return;

}

this.hasAdvancedPermissions = false;

this.addRoleInfoIfNeeded();

} );

//console.timeEnd("linkloop")

//console.timeEnd("uri");

//console.log("-------");

}

addCSS( htmlClass, cssDeclaration ) {

// .plainlinks is for Wikipedia Signpost articles

// To support additional custom signature edge cases, add to the selectors here.

this.mw.util.addCSS( `

.plainlinks .${ htmlClass }.external,

.${ htmlClass },

.${ htmlClass } b,

.${ htmlClass } big,

.${ htmlClass } font,

.${ htmlClass } kbd,

.${ htmlClass } small,

.${ htmlClass } span {

${ cssDeclaration }

}

` );

}

async getWikitextFromCache( title ) {

const api = new this.mw.ForeignApi( 'https://en.wikipedia.org/w/api.php' );

let wikitext = '';

await api.get( {

action: 'query',

prop: 'revisions',

titles: title,

rvslots: '*',

rvprop: 'content',

formatversion: '2',

uselang: 'content', // needed for caching

smaxage: '86400', // cache for 1 day

maxage: '86400' // cache for 1 day

} ).then( ( data ) => {

wikitext = data.query.pages[ 0 ].revisions[ 0 ].slots.main.content;

} );

return wikitext;

}

async getUsernames() {

if(this.wmf){

return;

}

const dataString = await this.getWikitextFromCache( 'User:NovemBot/userlist.js' );

const dataJSON = JSON.parse( dataString );

this.wmf = {

...dataJSON.founder,

...dataJSON.boardOfTrustees,

...dataJSON.staff

// WMF is hard-coded a bit further down. The script detects those strings in the username. This is safe to do because the WMF string is blacklisted from names, so has to be specially created.

// ...dataJSON['sysadmin'],

// ...dataJSON['global-interface-editor'],

// ...dataJSON['wmf-supportsafety'],

// ...dataJSON['mediawikiPlusTwo'],

// ...dataJSON['global-sysop'],

};

this.bot = dataJSON.bot;

this.stewards = dataJSON.steward;

this.arbcom = dataJSON.arbcom;

this.bureaucrats = dataJSON.bureaucrat;

this.admins = dataJSON.sysop;

this.checkUsers = dataJSON.checkuser;

this.formerAdmins = dataJSON.formeradmin;

this.newPageReviewers = dataJSON.patroller;

this.tenThousandEdits = dataJSON[ '10k' ];

this.extendedConfirmed = {

...dataJSON.extendedconfirmed,

...dataJSON.productiveIPs

};

}

hasHref( url ) {

return Boolean( url );

}

isAnchor( url ) {

return url.charAt( 0 ) === '#';

}

isHttpOrHttps( url ) {

return url.startsWith( 'http://', 0 ) ||

url.startsWith( 'https://', 0 ) ||

url.startsWith( '/', 0 );

}

/**

* Figure out the wikipedia article title of the link

*

* @param {string} url

* @param {mw.Uri} urlHelper

* @return {string}

*/

getTitle( url, urlHelper ) {

// for links in the format /w/index.php?title=Blah

const titleParameterOfUrl = this.mw.util.getParamValue( 'title', url );

if ( titleParameterOfUrl ) {

return titleParameterOfUrl;

}

// for links in the format /wiki/PageName. Slice off the /wiki/ (first 6 characters)

if ( urlHelper.path.startsWith( '/wiki/' ) ) {

return decodeURIComponent( urlHelper.path.slice( 6 ) );

}

return '';

}

notInUserOrUserTalkNamespace() {

const namespace = this.titleHelper.getNamespaceId();

const notInSpecialUserOrUserTalkNamespace = this.$.inArray( namespace, [ 2, 3 ] ) === -1;

return notInSpecialUserOrUserTalkNamespace;

}

linksToAUser() {

let url = this.$link.attr( 'href' );

if ( !this.hasHref( url ) || this.isAnchor( url ) || !this.isHttpOrHttps( url ) ) {

return false;

}

url = this.addDomainIfMissing( url );

// mw.Uri(url) throws an error if it doesn't like the URL. An example of a URL it doesn't like is https://meta.wikimedia.org/wiki/Community_Wishlist_Survey_2022/Larger_suggestions#1%, which has a section link to a section titled 1% (one percent).

let urlHelper;

try {

urlHelper = new this.mw.Uri( url );

} catch {

return false;

}

// Skip links that aren't to user pages

const isUserPageLink = url.includes( '/w/index.php?title=User' ) || url.includes( '/wiki/User' );

if ( !isUserPageLink ) {

return false;

}

// Even if it is a link to a userpage, skip URLs that have any parameters except title=User, action=edit, and redlink=. We don't want links to diff pages, section editing pages, etc. to be highlighted.

const urlParameters = urlHelper.query;

delete urlParameters.title;

delete urlParameters.action;

delete urlParameters.redlink;

const hasNonUserpageParametersInUrl = !this.$.isEmptyObject( urlParameters );

if ( hasNonUserpageParametersInUrl ) {

return false;

}

const title = this.getTitle( url, urlHelper );

// Handle edge cases such as https://web.archive.org/web/20231105033559/https://en.wikipedia.org/wiki/User:SandyGeorgia/SampleIssue, which shows up as isUserPageLink = true but isn't really a user page.

try {

this.titleHelper = new this.mw.Title( title );

} catch {

return false;

}

if ( this.notInUserOrUserTalkNamespace() ) {

return false;

}

const isDiscussionToolsSectionLink = url.includes( '#' );

if ( isDiscussionToolsSectionLink ) {

return false;

}

return true;

}

// Brandon Frohbieter, CC BY-SA 4.0, https://stackoverflow.com/a/4009771/3480193

countInstances( string, word ) {

return string.split( word ).length - 1;

}

/**

* mw.Uri(url) expects a complete URL. If we get something like /wiki/User:Test, convert it to https://en.wikipedia.org/wiki/User:Test. Without this, UserHighlighterSimple doesn't work on metawiki.

*

* @param {string} url

* @return {string} url

*/

addDomainIfMissing( url ) {

if ( url.startsWith( '/' ) ) {

url = window.location.origin + url;

}

return url;

}

/**

* @return {string}

*/

getUserName() {

const user = this.titleHelper.getMain().replace( /_/g, ' ' );

return user;

}

addRoleInfoIfAppropriate( listOfUsernames, label, descriptionForHover ) {

if ( listOfUsernames[ this.user ] === 1 ) {

this.addRoleIcon( label, descriptionForHover );

}

}

addRoleIcon( icon, descriptionForHover ) {

const title = this.$link.attr( 'title' );

if ( !title || title.startsWith( 'User:' ) ) {

this.$link.attr( 'title', descriptionForHover );

switch(this.labelPosition){

case "before":

this.$link.prepend($("

"+icon+"
"))

break;

default:

// Defaults to "after"

this.$link.append($("

"+icon+"
"))

break;

}

}

this.hasAdvancedPermissions = true;

}

addRoleInfoIfNeeded() {

// highlight anybody with "WMF" in their name, case insensitive. this should not generate false positives because "WMF" is on the username blacklist. see https://meta.wikimedia.org/wiki/Title_blacklist

if ( this.user.match( /^[^/]*WMF/i ) ) {

this.addRoleIcon( this.roleInfoLookup.wmf[0], this.roleInfoLookup.wmf[1] );

}

// TODO: grab the order from an array, so I can keep checkForPermission and addCSS in the same order easily, lowering the risk of the HTML title="" being one thing, and the color being another

this.addRoleInfoIfAppropriate( this.wmf, this.roleInfoLookup.wmf[0], this.roleInfoLookup.wmf[1]);

this.addRoleInfoIfAppropriate( this.bot, this.roleInfoLookup.bot[0], this.roleInfoLookup.bot[1]);

this.addRoleInfoIfAppropriate( this.stewards, this.roleInfoLookup.stewards[0], this.roleInfoLookup.stewards[1]);

this.addRoleInfoIfAppropriate( this.arbcom, this.roleInfoLookup.arbcom[0], this.roleInfoLookup.arbcom[1]);

this.addRoleInfoIfAppropriate( this.bureaucrats, this.roleInfoLookup.bureaucrats[0], this.roleInfoLookup.bureaucrats[1]);

this.addRoleInfoIfAppropriate( this.checkUsers, this.roleInfoLookup.checkUsers[0], this.roleInfoLookup.checkUsers[1]);

this.addRoleInfoIfAppropriate( this.admins, this.roleInfoLookup.admins[0], this.roleInfoLookup.admins[1]);

this.addRoleInfoIfAppropriate( this.formerAdmins, this.roleInfoLookup.formerAdmins[0], this.roleInfoLookup.formerAdmins[1]);

this.addRoleInfoIfAppropriate( this.newPageReviewers, this.roleInfoLookup.newPageReviewers[0], this.roleInfoLookup.newPageReviewers[1]);

this.addRoleInfoIfAppropriate( this.tenThousandEdits, this.roleInfoLookup.tenThousandEdits[0], this.roleInfoLookup.tenThousandEdits[1]);

this.addRoleInfoIfAppropriate( this.extendedConfirmed, this.roleInfoLookup.extendedConfirmed[0], this.roleInfoLookup.extendedConfirmed[1]);

// If they have no perms, then they are non-EC, so <500 edits

if ( !this.hasAdvancedPermissions ) {

this.addRoleIcon(this.roleInfoLookup.lessThan500[0], this.roleInfoLookup.lessThan500[1]);

}

}

}

var userRoleIndicator = new UserRoleIndicator( $, mw, window )

// Fire after wiki content is added to the DOM, such as when first loading a page, or when a gadget such as the XTools gadget loads.

mw.hook( 'wikipage.content' ).add( async () => {

await mw.loader.using( [ 'mediawiki.util', 'mediawiki.Uri', 'mediawiki.Title' ], async () => {

await userRoleIndicator.execute();

} );

} );

// Fire after an edit is successfully saved via JavaScript, such as edits by the Visual Editor and HotCat.

mw.hook( 'postEdit' ).add( async () => {

await mw.loader.using( [ 'mediawiki.util', 'mediawiki.Uri', 'mediawiki.Title' ], async () => {

await userRoleIndicator.execute();

} );

} );

//