您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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/[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; }; }