Misc utils library

Library for misc util functions for userscripts

This script should not be not be installed directly. It is a library for other scripts to include with the meta directive // @require https://update.greasyfork.org/scripts/581026/1842191/Misc%20utils%20library.js

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Misc utils library
// @namespace    MiscUtilsLib
// @description  Library for misc util functions for userscripts
// @author       kekekeKaj
// @version      1.1.1
// @grant        none
// ==/UserScript==  

// IMPORTANT: update version number when publishing!!!
function getMiscUtilsLibInfo() {
    return 'Misc Utils Lib v1.1';
}

// ----LOCAL STORAGE CACHE-------

function getLocalStorageKeys() {
    return {
        stackMsgTemplates: generateLocalStorageKey('stackMsgTemplates'),
        reviewMsgTemplates: generateLocalStorageKey('reviewMsgTemplates'),
        recMsgTemplates: generateLocalStorageKey('recMsgTemplates'),

        lastClearTimestamp: generateLocalStorageKey('lastClearTimestamp'),
        reportedItemsInfo: generateLocalStorageKey('reportedItemsInfo'),
        modSettings: generateLocalStorageKey('modSettings'),

        reportInfoPrefix: generateLocalStorageKey('reportInfo.'),
        stackInfoPrefix: generateLocalStorageKey('stackInfo.'),
        reviewInfoPrefix: generateLocalStorageKey('reviewInfo.')
    }
}

function loadCachedReportedItemInfo(reportType, itemId) {
    if (itemId == null) {
        return null;
    }

    switch(reportType) {
        case 'stack':
            return loadFromJSONInLocalStorage(getLocalStorageKeys().stackInfoPrefix + itemId);
        case 'review':
            return loadFromJSONInLocalStorage(getLocalStorageKeys().reviewInfoPrefix + itemId);

        default:
            throw 'Cannot load cached reported item: invalid report type';
    }
}

function buildReportedItemStorageKey(reportType, itemId) {
    if (itemId == null) {
        return null;
    }

    switch(reportType) {
        case 'stack':
            return getLocalStorageKeys().stackInfoPrefix + itemId;
        case 'review':
            return getLocalStorageKeys().reviewInfoPrefix + itemId;

        default:
            throw 'Cannot build reported item storage key: invalid report type';
    }
}

function getSessionStorageKeys() {
    return {
        stackEditResult: generateLocalStorageKey('stackEditResult'),
        targetItemId: generateLocalStorageKey('targetItemId')
    }
}

function getReviewAdminStorageKeyBase() {
    return 'reviewAdmin';
}

function generateLocalStorageKey(keyword) {
    return `${getReviewAdminStorageKeyBase()}.${keyword}`;
}

function saveAsJSONInLocalStorage(key, item, logItem = false) {
    if (key == null || item == null) {
        console.warn('Cannot save item: key or item is null!');
        return;
    }
    setLocalStorageItem(key, JSON.stringify(item), logItem);
}

function loadFromJSONInLocalStorage(key, logItem = false) {
    if (key == null) {
        return null;
    }

    const objJSON = localStorage.getItem(key);
    const parsedObj = objJSON == null ? null : JSON.parse(objJSON);

    if (logItem) {
        console.info(`Loaded from key ${key}: `, parsedObj);
    }

    return parsedObj;
}

function loadModSettings(defaultModName) {
    const modSettings = getDefaultModSettings();
    if (defaultModName != null) {
        modSettings.modName = moderator;
    }
    
    overrideWithModSettings(
        modSettings, loadFromJSONInLocalStorage(getLocalStorageKeys().modSettings, true));

    return modSettings;
}

function setLocalStorageItem(key, item, logItem = false) {
    try {
        localStorage.setItem(key, item);

        if (logItem) {
            console.info(`Setting local storage value for ${key}: ${item}`);
        }
    } catch(err) {
        if (err instanceof DOMException) {
            console.warn('Error when trying to store item: ', err); 
            console.warn('LocalStorage may be full. Will attempt to clear storage');
            if (shouldClearReportCache()) {
                console.log("Auto clearing old report data.");
                clearMALModCache();
            }
        } else {
            console.error('Unknown error when storing item: ', err)
        }
    }
}

function clearMALModCache() {
    Object.keys(localStorage).forEach(function (e) {
        if (e.startsWith(getReviewAdminStorageKeyBase()) || e.includes("submissions&type")) {
            localStorage.removeItem(e);
        }
    });

    const lastClearTimestampKey = getLocalStorageKeys().lastClearTimestamp;
    localStorage.setItem(lastClearTimestampKey, String(Date.now()));
}

/**
 * Function to check whether to clear the queue cache.
 * @returns true if we don't have a last clear timestamp or if the timestamp is from more than a day ago.
 */
function shouldClearReportCache() {
    const lastClearTimestampKey = getLocalStorageKeys().lastClearTimestamp;
    const epochTimeStr = localStorage.getItem(lastClearTimestampKey);

    if (epochTimeStr == null) {
        return true;
    } 

    const lastClearTime = new Date(parseInt(epochTimeStr));
    const oneDayInMs = 60000 * 60 * 24;
    return lastClearTime == null || Date.now() - lastClearTime > oneDayInMs;
}

// --- FETCH ---
function fetchPostReq(url, data, successCallback, errorCallback) {
    fetch(
        url,
        {
            method: "POST",
            body:  data
        })
    .then(response => { return response.text() })
    .then(successCallback)
    .catch(errorCallback);
}

function fetchGetReq(url, successCallback, errorCallback) {
    fetch(url)
        .then(unwrapResponseTxt)
        .then(successCallback)
        .catch(errorCallback);
}

function unwrapResponseTxt(response) {
    if (response.ok) {
        return response.text();
    }

    throw new Error('Request failed for URL: ' + response.url);
}

// --- OTHER FUNCTIONS ---

/**
 * Loads msg templates and returns them in a map
 * @param {string} localStorageKey The local storage key from which to load the messages.
 * @returns {Map} of msg summary -> msgs
 */
function loadMsgTemplates(localStorageKey) {
    const rawTemplateStr = localStorage.getItem(localStorageKey);
    if (rawTemplateStr == null) {
        console.error('No templates found in local storage!');
        return null;
    }

    const splitTemplates = rawTemplateStr.split("*****").filter(segment => segment.trim().length > 0);

    // if the parsing was correct, we should end up with even number of segments representing pairs 
    // consisting of a stock message and its summary.
    if (splitTemplates.length % 2 !== 0) {
        console.error("Error parsing message templates!");
        return null;
    }

    const msgTemplateMap = new Map();
    for (let i = 0; i < splitTemplates.length; i += 2) {
        msgTemplateMap.set(splitTemplates[i].trim(), splitTemplates[i + 1].trim());
    }

    return msgTemplateMap;
}

function getDefaultModSettings() {
    return {
        modName: "",
        modTitle: "User Content Moderator",
        bulkActionDelayInMs: 200
    }
}

/**
 * Overrides an object's values with the ones from the mod's settings config.
 * 
 * @param {any} objToOverride 
 * @param {any} modSettings 
 */
function overrideWithModSettings(objToOverride, modSettings) {
    if (modSettings == null) {
        return;
    }

    if (hasNonWhitespaceValue(modSettings.modName)) {
        objToOverride.modName = modSettings.modName;
    }

    if (hasNonWhitespaceValue(modSettings.modTitle)) {
        objToOverride.modTitle = modSettings.modTitle;
    }

    if (modSettings.bulkActionDelayInMs != null) {
        objToOverride.bulkActionDelayInMs = modSettings.bulkActionDelayInMs;
    }
}

function getProfileUrl(username) {
    return `/profile/${username}`;
}