User:Andrybak/Scripts/Not around.js

// /* * * Copyright (c) 2024 Andrei Rybak * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */

(function {	'use strict';

const config = { wikipage: 'Not around', version: '3.3' };

const USERSCRIPT_NAME = 'Not around userscript'; const LOG_PREFIX = `[${USERSCRIPT_NAME}]:`;

function error(...toLog) { console.error(LOG_PREFIX, ...toLog); }

function warn(...toLog) { console.warn(LOG_PREFIX, ...toLog); }

function info(...toLog) { console.info(LOG_PREFIX, ...toLog); }

function debug(...toLog) { console.debug(LOG_PREFIX, ...toLog); }

function notify(notificationMessage) { mw.notify(notificationMessage, {			title: USERSCRIPT_NAME		}); }	function errorAndNotify(errorMessage, rejection) { error(errorMessage, rejection); notify(errorMessage); }

const ABSENSE_YEARS_MINIMUM = 6; const mw = window.mw; const DEBUG = false;

function constructAd { return `using ${config.wikipage} v${config.version}`; }

function constructEditSummary(username, lastContribYear) { return `/* top */ add Template:Not around – user ${username} hasn't edited since ${lastContribYear} (${constructAd})`; }

/**	 * Asynchronously load specified number of contributions of specified username. */	function loadNLastUserContribs(username, n) { const api = new mw.Api; return api.get({			action: 'query',			list: 'usercontribs',			ucuser: username,			uclimit: n		}); }

/**	 * Asynchronously load the very last contribution of specified username. */	function loadLastUserContrib(username) { return new Promise((resolve, reject) => {			loadNLastUserContribs(username, 1).then(response => { debug(response); const lastContrib = response.query.usercontribs[0]; resolve(lastContrib); }, rejection => { reject(rejection); });		});	}

function isoStringToYear(timestamp) { const d = new Date(timestamp); return d.getUTCFullYear; }

function loadCurrentWikitext(pagename) { return new Promise((resolve, reject) => {			const api = new mw.Api;			api.get({ action: 'query', titles: pagename, prop: 'revisions', rvprop: 'content', rvslots: 'main', /* v2 has nicer field names in responses to this request */ formatversion: 2 }).then(response => { resolve(response.query.pages[0].revisions[0].slots.main.content); }, rejection => { reject(rejection); });		});	}

function addNotAroundTemplateIfAbsent(username, lastContribYear) { info(`${username} hasn't edited since ${lastContribYear}.`);

const userTalkPageTitle = 'User_talk:' + username; loadCurrentWikitext(userTalkPageTitle).then(wikitext => {			/*			 * TODO: The checks below are not enough: a mangled template invocation with spaces, like			 * TODO:   			 * TODO: will not be detected.			 */			if (wikitext.includes('{{Not around') || wikitext.includes('{{not around')) {				info(userTalkPageTitle + ' already has the template. Showing it to the user and aborting.');				location.assign('/wiki/' + userTalkPageTitle);				return;			}			const newWikitext = `{{Not around|date=${lastContribYear}}}\n` + wikitext;			const editSummary = constructEditSummary(username, lastContribYear);			if (DEBUG) {				debug(newWikitext.slice(0, 40));				debug(editSummary);			}			const api = new mw.Api;			api.postWithEditToken({ action: 'edit', /* TODO figure out how to do a preview instead of 'edit' */ title: userTalkPageTitle, text: newWikitext, summary: editSummary }).then(response => { // Show the edit performed by `postWithEditToken` to the user of the script. loadLastUserContrib(mw.user.getName).then(theEdit => {					location.assign('/wiki/Special:Diff/' + theEdit.revid);				}, rejection => {					errorAndNotify(`Cannot load last contribution by ${mw.user.getName}.`, rejection);				}); }, rejection => { errorAndNotify(`Cannot edit page ${userTalkPageTitle}`, rejection); });		});	}

function runPortlet { const username = mw.config.get('wgRelevantUserName'); if (!username) { errorAndNotify('Cannot find a username', null); return; }		loadLastUserContrib(username).then(lastContrib => {			if (!lastContrib) {				notify(`User ${username} has zero contributions. Aborting.`);				return;			}			if (lastContrib.user != username) {				errorAndNotify(`Received wrong user. Actual ${lastContrib.user} ≠ expected ${username}. Aborting.`, null);				return;			}			const lastContribYear = isoStringToYear(lastContrib.timestamp);			const currentYear = new Date.getUTCFullYear;			info('Last edit timestamp =', lastContrib.timestamp);			// check how long ago was the last contribution			if (currentYear - lastContribYear >= ABSENSE_YEARS_MINIMUM) {				addNotAroundTemplateIfAbsent(username, lastContribYear);			} else {				notify(`${username} is still an active user. Last edit was in year ${lastContribYear}. Aborting.`);			}		}, rejection => {			errorAndNotify(`Cannot load contributions of ${username}. Aborting.`, rejection);		}); }

function lazyLoadNotAround { debug('Loading...'); const namespaceNumber = mw.config.get('wgNamespaceNumber'); /* "Special", "User", and "User talk" */ if (namespaceNumber === -1 || namespaceNumber === 2 || namespaceNumber === 3) { if (!mw.loader.using) { warn('Function mw.loader.using is no loaded yet. Retrying...'); setTimeout(lazyLoadNotAround, 300); return; }			mw.loader.using(				['mediawiki.util'],				 => {					const link = mw.util.addPortletLink('p-cactions', '#', 'Not around', 'ca-notaround', 'add template {{Not around}}');					if (!link) {						info('Cannot create portlet link (mw.util.addPortletLink). Assuming unsupported skin. Aborting.');						return;					}					link.onclick = event => {						event.preventDefault;						mw.loader.using('mediawiki.api', runPortlet);					};				},				(e) => {					error('Cannot add portlet link', e);				}			); } else { warn('Triggered on a bad namespace =', namespaceNumber); }	}

if (document.readyState !== 'loading') { lazyLoadNotAround; } else { warn('Cannot load yet. Setting up a listener...'); document.addEventListener('DOMContentLoaded', lazyLoadNotAround); } }); //