User:Polygnotus/Scripts/ExternalLinkMonitor2.js
Appearance
From Wikipedia, the free encyclopedia
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.
This code will be executed when previewing this page.
Documentation for this user script can be added at User:Polygnotus/Scripts/ExternalLinkMonitor2.
/** * External Link Monitor for Wikipedia (Enhanced with CiteHighlighter Integration) * Monitors recent changes that add external links to enwiki articles * Filters out: bot edits, top 10k domains, users with >500 edits * Colors links based on CiteHighlighter source ratings */ (function(){ 'use strict'; // Add link to Tools menu mw.loader.using(['mediawiki.util','mediawiki.api'],function(){ mw.util.addPortletLink( 'p-tb', mw.util.getUrl('Special:BlankPage/ExternalLinkMonitor'), 'External Link Monitor', 't-extlinkmonitor', 'Monitor external links added to articles' ); }); // Check if we're on the special page if(mw.config.get('wgCanonicalSpecialPageName')==='Blankpage'&& mw.config.get('wgPageName')==='Special:BlankPage/ExternalLinkMonitor'){ $(document).ready(function(){ initMonitor(); }); } functioninitMonitor(){ $('#firstHeading').text('External Link Monitor'); document.title='External Link Monitor'; const$container=$('#mw-content-text'); $container.html(` <div id="elm-container"> <div id="elm-status">Loading source ratings and top 10k domains...</div> <div id="elm-controls" style="margin: 15px 0; display: none;"> <button id="elm-start" style="padding: 8px 16px; font-size: 14px; cursor: pointer;">Start Monitoring</button> <button id="elm-stop" style="padding: 8px 16px; font-size: 14px; cursor: pointer; display: none;">Stop Monitoring</button> <span id="elm-count" style="margin-left: 15px; font-weight: bold;"></span> </div> <div id="elm-legend" style="margin: 15px 0; padding: 10px; border: 1px solid #ccc; background: #f9f9f9; display: none;"> <strong>Color Legend:</strong> <span style="margin-left: 10px; padding: 3px 8px; background: limegreen;">MEDRS</span> <span style="margin-left: 10px; padding: 3px 8px; background: lightgreen;">Reliable</span> <span style="margin-left: 10px; padding: 3px 8px; background: khaki;">Caution</span> <span style="margin-left: 10px; padding: 3px 8px; background: lightcoral;">Unreliable</span> <span style="margin-left: 10px; padding: 3px 8px; background: #ffcfd5;">Preprint</span> <span style="margin-left: 10px; padding: 3px 8px; background: #ffb347;">Unreliable Word</span> </div> <div id="elm-results" style="margin-top: 20px; font-family: monospace; font-size: 12px;"></div> </div> `); loadSourceRatings(); } // Storage for top 10k domains organized by first character constdomainsByChar={}; // Storage for CiteHighlighter source ratings letsourceRatings={}; letunreliableWords=[]; letisMonitoring=false; leteventSource=null; leteditCount=0; // Color mapping from CiteHighlighter constcolors={ unreliableWord:'#ffb347', preprint:'#ffcfd5', doi:'transparent', medrs:'limegreen', green:'lightgreen', yellow:'khaki', red:'lightcoral' }; asyncfunctionloadSourceRatings(){ try{ // Load CiteHighlighter source ratings constapi=newmw.Api(); constsourcesResponse=awaitapi.get({ action:'query', prop:'revisions', titles:'User:Novem Linguae/Scripts/CiteHighlighter/SourcesJSON.js', rvprop:'content', rvslots:'main', formatversion:2 }); if(sourcesResponse.query&&sourcesResponse.query.pages&&sourcesResponse.query.pages[0]){ constcontent=sourcesResponse.query.pages[0].revisions[0].slots.main.content; sourceRatings=JSON.parse(content); console.log('Loaded source ratings:',sourceRatings); } // Load unreliable words unreliableWords=getUnreliableWords(); // Now load top 10k domains loadTop10kDomains(); }catch(error){ $('#elm-status').text('Error loading source ratings: '+error.message); console.error('Error loading source ratings:',error); } } functiongetUnreliableWords(){ return[ '/comment', 'about-me', 'about-us', '/about/', 'acquire', 'announce', 'blog', 'blogspot', 'businesswire', 'caard', 'contact-us', 'contactus', 'essay', 'fandom', '/forum/', 'google.com/search', 'innovative', 'newswire', 'podcast', '/post/', 'preprint', 'press-release', 'pressrelease', 'prnews', 'railfan', 'sponsored', 'thread', 'user-review', 'viewtopic', 'weebly', 'wix', 'wordpress', '/wp-' ]; } functionloadTop10kDomains(){ constapi=newmw.Api(); api.get({ action:'query', prop:'revisions', titles:'User:Polygnotus/Data/Top10kDomains', rvprop:'content', rvslots:'main', formatversion:2 }).done(function(data){ if(data.query&&data.query.pages&&data.query.pages[0]){ constcontent=data.query.pages[0].revisions[0].slots.main.content; parseDomainList(content); $('#elm-status').text('Ready to monitor. Source ratings and top 10k domains loaded.'); $('#elm-controls').show(); $('#elm-legend').show(); setupEventHandlers(); }else{ $('#elm-status').text('Error: Could not load domain list.'); } }).fail(function(){ $('#elm-status').text('Error: Failed to fetch domain list from API.'); }); } functionparseDomainList(content){ // Initialize arrays for a-z and 0-9 for(leti=0;i<26;i++){ domainsByChar[String.fromCharCode(97+i)]=[]; } domainsByChar['0-9']=[]; domainsByChar['other']=[]; constlines=content.split('\n'); for(constlineoflines){ constdomain=line.trim().toLowerCase(); if(domain&&!domain.startsWith('#')){ constfirstChar=domain.charAt(0); if(firstChar>='a'&&firstChar<='z'){ domainsByChar[firstChar].push(domain); }elseif(firstChar>='0'&&firstChar<='9'){ domainsByChar['0-9'].push(domain); }else{ domainsByChar['other'].push(domain); } } } console.log('Loaded domains into character buckets:', Object.keys(domainsByChar).map(k=>k+': '+domainsByChar[k].length).join(', ')); } functionisTop10kDomain(url){ try{ consturlObj=newURL(url); lethostname=urlObj.hostname.toLowerCase(); // Remove www. prefix if(hostname.startsWith('www.')){ hostname=hostname.substring(4); } constfirstChar=hostname.charAt(0); letbucket; if(firstChar>='a'&&firstChar<='z'){ bucket=domainsByChar[firstChar]; }elseif(firstChar>='0'&&firstChar<='9'){ bucket=domainsByChar['0-9']; }else{ bucket=domainsByChar['other']; } if(!bucket)returnfalse; // Check if hostname or any parent domain is in the list constparts=hostname.split('.'); for(leti=0;i<parts.length;i++){ consttestDomain=parts.slice(i).join('.'); if(bucket.includes(testDomain)){ returntrue; } } returnfalse; }catch(e){ returnfalse; } } functiongetSourceRating(url){ try{ consturlObj=newURL(url); lethostname=urlObj.hostname.toLowerCase(); // Remove www. prefix if(hostname.startsWith('www.')){ hostname=hostname.substring(4); } // Check unreliable words first (highest priority in CiteHighlighter) consturlLower=url.toLowerCase(); for(constwordofunreliableWords){ if(urlLower.includes(word)){ return{color:'unreliableWord',label:'Unreliable Word'}; } } // Check each rating category constratingOrder=['red','yellow','green','medrs','doi','preprint']; for(constratingofratingOrder){ if(sourceRatings[rating]){ for(constsourceofsourceRatings[rating]){ // Check if the domain matches if(hostname===source||hostname.endsWith('.'+source)){ return{ color:rating, label:rating==='medrs'?'MEDRS': rating==='green'?'Reliable': rating==='yellow'?'Caution': rating==='red'?'Unreliable': rating==='preprint'?'Preprint': rating==='doi'?'DOI':rating }; } } } } returnnull; }catch(e){ returnnull; } } functionsetupEventHandlers(){ $('#elm-start').on('click',startMonitoring); $('#elm-stop').on('click',stopMonitoring); } functionstartMonitoring(){ if(isMonitoring)return; isMonitoring=true; editCount=0; $('#elm-start').hide(); $('#elm-stop').show(); $('#elm-status').text('Monitoring active...'); $('#elm-results').empty(); updateCount(); // Connect to Wikimedia EventStreams conststreamUrl='https://stream.wikimedia.org/v2/stream/mediawiki.page-links-change'; eventSource=newEventSource(streamUrl); eventSource.onopen=function(){ console.log('EventStream connection opened'); }; eventSource.onerror=function(e){ console.error('EventStream error:',e); if(isMonitoring){ appendResult('Connection error. Reconnecting...','error'); } }; eventSource.onmessage=function(event){ try{ constdata=JSON.parse(event.data); processChange(data); }catch(e){ console.error('Error parsing event data:',e); } }; } functionstopMonitoring(){ if(!isMonitoring)return; isMonitoring=false; $('#elm-start').show(); $('#elm-stop').hide(); $('#elm-status').text('Monitoring stopped.'); if(eventSource){ eventSource.close(); eventSource=null; } } functionprocessChange(data){ // Filter: only enwiki constmetaUri=data.meta&&data.meta.uri; if(!metaUri||!metaUri.startsWith('https://en.wikipedia.org')){ return; } // Filter: no bot edits constperformer=data.performer; if(!performer||performer.user_is_bot){ return; } // Filter: user edit count <= 500 constuserEditCount=performer.user_edit_count; if(userEditCount>500){ return; } // Filter: only main namespace (namespace 0) if(data.page_namespace!==0){ return; } // Check for added external links constaddedLinks=data.added_links; if(!addedLinks||addedLinks.length===0){ return; } constvalidLinks=[]; for(constlinkofaddedLinks){ if(link.external&&link.link){ if(!isTop10kDomain(link.link)){ constrating=getSourceRating(link.link); validLinks.push({ url:link.link, rating:rating }); } } } if(validLinks.length>0){ displayEdit(data,validLinks,performer); } } functiondisplayEdit(data,links,performer){ editCount++; updateCount(); constpageTitle=data.page_title||'Unknown'; constpageTitleEncoded=encodeURIComponent(pageTitle).replace(/%20/g,'_'); constrevId=data.rev_id||0; consttimestamp=data.meta&&data.meta.dt?newDate(data.meta.dt).toISOString():''; constuserName=performer.user_text||'Unknown'; constuserId=performer.user_id||0; constuserEditCount=performer.user_edit_count||0; constuserRegistration=performer.user_registration_dt||'Unknown'; lethtml='<div style="margin-bottom: 20px; padding: 10px; border: 1px solid #ccc; background: #f9f9f9;">'; html+=`<div style="font-weight: bold; margin-bottom: 5px;">[${timestamp}]</div>`; html+=`<div><a href="https://en.wikipedia.org/wiki/${pageTitleEncoded}" target="_blank">${escapeHtml(pageTitle)}</a></div>`; html+=`<div><a href="https://en.wikipedia.org/w/index.php?title=${pageTitleEncoded}&diff=${revId}" target="_blank">Diff ${revId}</a></div>`; html+=`<div style="margin-top: 8px;">User: <a href="https://en.wikipedia.org/wiki/User:${encodeURIComponent(userName)}" target="_blank">${escapeHtml(userName)}</a> `; html+=`(ID: ${userId}, Edits: ${userEditCount}, Registered: ${userRegistration})</div>`; html+='<div style="margin-top: 8px; font-weight: bold;">Added links:</div>'; html+='<ul style="margin: 5px 0;">'; for(constlinkoflinks){ constbgColor=link.rating?colors[link.rating.color]:'transparent'; constlabel=link.rating?` [${link.rating.label}]`:''; // Decode the URL completely constdecodedUrl=decodeURIComponent(link.url); html+=`<li style="background-color: ${bgColor}; padding: 3px; margin: 2px 0;">`; html+=`<a href="${escapeHtml(decodedUrl)}" target="_blank" rel="nofollow">${escapeHtml(decodedUrl)}</a>`; if(label){ html+=`<span style="font-weight: bold; margin-left: 5px;">${label}</span>`; } html+=`</li>`; } html+='</ul>'; html+='</div>'; $('#elm-results').prepend(html); // Keep only last 50 results constresults=$('#elm-results > div'); if(results.length>50){ results.slice(50).remove(); } } functionupdateCount(){ $('#elm-count').text(`Edits found: ${editCount}`); } functionappendResult(message,type){ constcolor=type==='error'?'red':'green'; $('#elm-results').prepend(`<div style="color: ${color}; margin-bottom: 10px;">${escapeHtml(message)}</div>`); } functionescapeHtml(text){ constmap={ '&':'&', '<':'<', '>':'>', '"':'"', "'":''' }; returnString(text).replace(/[&<>"']/g,function(m){returnmap[m];}); } })();