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; }

async execute { const defaultRoleInfoLookup = { wmf: ['🌐', 'Wikimedia Foundation (WMF)'], bot: ['🤖', 'Bot'], stewards: ['🩺', 'Steward or Ombud'], arbcom: ['⚖️', 'Arbitration Committee member'], bureaucrats: ['💼‍', 'Bureaucrat'], 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; }

//		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; margin-left: 3px;')

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

// console.time("linkloop") $links.each( ( index, element ) => {			this.$link = this.$( 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") }

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 { 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.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 ); this.$link.append($(" "+icon+" ")) }

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.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]); }	} }

// 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 ( new UserRoleIndicator( $, mw, window ) ).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 ( new UserRoleIndicator( $, mw, window ) ).execute; } ); } );

//