Misc utils library

Library for misc util functions for userscripts

此腳本不應該直接安裝,它是一個供其他腳本使用的函式庫。欲使用本函式庫,請在腳本 metadata 寫上: // @require https://update.greasyfork.org/scripts/581026/1842191/Misc%20utils%20library.js

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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}`;
}