Board16 Recovery

save board 16

// ==UserScript==
// @name         Board16 Recovery
// @version      0.0.1
// @description  save board 16
// @icon         https://www.cc98.org/static/98icon.ico

// @author       You
// @namespace    http://tampermonkey.net/
// @license      MIT

// @match        https://www.cc98.org/board/16
// @match        https://www.cc98.org/board/16/*
// @match        https://www.cc98.org/error/404
// @require      https://unpkg.com/dexie@3.2.0/dist/dexie.min.js
// @require      https://unpkg.com/dexie-export-import@1.0.3/dist/dexie-export-import.js
// @grant        GM_getValue
// @grant        GM_setValue
// @run-at       document-start
// ==/UserScript==

/* global Dexie */

const log = console.log;
const sleep = (ms)=>new Promise(r=>setTimeout(r, ms));

const imported = GM_getValue('imported', false);
if(!imported) {
    importDatabase();
}

main();

// import database from json file
async function importDatabase() {
    await sleep(3000);
    const upload = element(`<input id="upload" type="file"></input>`);
    const message = element(`<p>点击上传json文件</p>`);

    function progressCallback ({totalRows, completedRows}) {
        message.textContent = `Progress: ${completedRows} of ${totalRows} rows`;
    }
    on(upload, 'change', async()=>{
        try {
            // devtools (> Application) > Storage > IndexedDB > cc98-board16-recovery-v1
            const db = await Dexie.import(upload.files[0], {progressCallback});
            message.textContent = "Import complete";
            const infos = await db.infos.toArray();
            log(infos);
            GM_setValue('boardInfo', infos.map(i=>i.info));
            GM_setValue('imported', true);
            await sleep(3000);
            location += '';
        } catch (error) {
            console.error(error);
        }
    });
    const row = document.querySelector('.board-head-bar > div.row');
    row.appendChild(upload);
    row.appendChild(message);
}

function element(html) {
    const t = document.createElement('template');
    t.innerHTML = html.trim();
    return t.content.firstChild;
}

function on(elem, event, func) {
    return elem.addEventListener(event, func, false);
}


async function main() {
    let boardInfo = {
        0: `{"id":16,"name":"","bigPaper":null,"logoUri":null,"parentId":6,"anonymousState":0,"privacyState":0,"viewerFilterState":0,"protectionLevel":1,"isLocked":true,"rootId":6,"description":"","boardMasters":[],"topicCount":5987,"postCount":1280849,"todayCount":0,"lastPostContent":"","allowPostOnly":2,"forbidRvpn":false,"canEntry":true,"internalState":0,"canVote":true,"isUserCustomBoard":false}`,
        1: `{"id":16,"name":"","boardMasters":[],"topicCount":20664,"postCount":1280849,"todayCount":0,"description":"","anonymousState":0}`,
    };
    let info1 = JSON.parse(boardInfo[1]);

    let db;
    if(imported) {
        db = new Dexie("cc98-board16-recovery-v1");
        db.version(2).stores({
            topics: "id",
            posts: "id,topicId",
            infos: "i",
        });

        boardInfo = GM_getValue('boardInfo');
        info1 = JSON.parse(boardInfo[1]);
        log(boardInfo);
    }


    const topicInfoRegExp = new RegExp("api.cc98.org/topic/\\d+$", 'i');
    const isTopicInfoAPI = (url) => topicInfoRegExp.test(url);

    const topicRegExp = new RegExp("/board/16/topic");
    const isTopicAPI = (url) => imported && (topicRegExp.test(url) || isTopicInfoAPI(url));

    const postRegExp = new RegExp("/topic/\\d+/post", 'i');
    const isPostAPI = (url) => imported && postRegExp.test(url);

    const hotPostRegExp = new RegExp("/topic/\\d+/hot-post", 'i');
    const isHotPostAPI = (url) => imported && hotPostRegExp.test(url);

    // get data from local database
    async function get(url) {
        try {
            if(isTopicInfoAPI(url)) {
                const topicId = Number(url.match(/topic\/(\d+)/)[1]);
                const data = await db.topics.where('id').equals(topicId).toArray();
                return data[0];
            } else if(isTopicAPI(url)) {
                const [offset, limit] = url.match(/from=(\d+)&size=(\d+)/).slice(1,3).map(Number);
                const data = await db.topics.reverse().offset(offset).limit(limit).toArray();
                return data;
            } else if(isPostAPI(url)) {
                const [topicId, offset, limit] = url.match(/topic\/(\d+)\/post\?from=(\d+)&size=(\d+)/i).slice(1,4).map(Number);
                // TODO: orderBy(':id')
                const data = await db.posts.where('topicId').equals(topicId).offset(offset).limit(limit).toArray();
                data.forEach(i=>{i.awards = JSON.parse(i.awards)});
                return data.sort((a,b)=>a.floor-b.floor);
            }
        } catch (error) {
            console.error(error);
            return [];
        }
    }

    // monkey patching Response.prototype.json
    const resolve = async (url, data) => {
        log(url);
        log('before', data);
        if (url == 'https://api.cc98.org/Board/all') {
            data[0].boards.push(info1);
        } else if(isPostAPI(url) || isTopicAPI(url)) {
            const realData = await get(url);
            data = realData;
        }
        log('after', data);
        return data;
    };
    const origResponseJSON = Response.prototype.json;
    Response.prototype.json = function () {
        return origResponseJSON.call(this).then((data) => resolve(this.url, data));
    };

    // monkey patching window.fetch
    const origFetch = unsafeWindow.fetch;
    unsafeWindow.fetch = async (...args) => {
        log('fetch', args);
        const url = args[0];
        if(url == 'https://api.cc98.org/board/16') {
            log('/board/16');
            return new Response(boardInfo[0]);
        } else if(url == 'https://api.cc98.org/topic/toptopics?boardid=16' || isHotPostAPI(url)) {
            log('toptopics || isHotPostAPI');
            return new Response(`[]`);
        } else if(isPostAPI(url) || isTopicAPI(url)) {
            log('isPostAPI || isTopicAPI', url);
            const response = new Response(`[]`);
            Object.defineProperty(response, 'url', { value: url });
            return response;
        }
        const response = await origFetch(...args);
        return response;
    };
}