User:Andrybak/Scripts/Not around.js

//

/*

*

* Copyright (c) 2024-2025 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.4'

};

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: {{ not around}}

* TODO: will not be detected.

*/

const templates = ['{{not around', '{{notaround', '{{inactive user', '{{user longterm inactive',

'{{gone', '{{Missing user', '{{Left wikipedia', '{{Missing wikipedian' ];

for (const template of templates) {

if (wikitext.toLowerCase().includes(template.toLowerCase())) {

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

}

})();

//