Board16 Recovery

save board 16

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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/[email protected]/dist/dexie.min.js
// @require      https://unpkg.com/[email protected]/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;
    };
}