- // ==UserScript==
- // @name 字节圈增强脚本
- // @namespace http://tampermonkey.net/
- // @version 0.5
- // @license WTFPL
- // @description try to take over the world!
- // @author Yxxx
- // @match https://ee.bytedance.net/malaita/pc/*
- // @icon https://ee.bytedance.net/malaita/static/img/malaita.png
- // @grant GM_registerMenuCommand
- // @grant GM_unregisterMenuCommand
- // @grant GM_getValue
- // @grant GM_setValue
- // @grant unsafeWindow
- // ==/UserScript==
-
- (function () {
- const MENU_KEY_TIME_ORDER = 'menu_time_order';
- const MENU_KEY_AUTO_BACKUP = 'menu_auto_backup';
- const MENU_KEY_FILTER_NEW_BYTEDANCER = 'menu_filter_new_bytedancer';
- const MENU_KEY_FILTER_ANONYMOUS = 'menu_key_filter_anonymous';
- const MENU_KEY_POST_BLACKWORD_LIST = 'menu_key_post_blackword_list';
- const MENU_KEY_DEBUG_MENU = 'menu_key_debug_menu';
-
- const MENU_ALL = [
- [MENU_KEY_TIME_ORDER, '帖子按时间排序', true, () => { switchMenuCommand(MENU_KEY_TIME_ORDER); alert('请手动刷新页面以生效.'); }],
- [MENU_KEY_AUTO_BACKUP, '备份浏览过的帖子', false, () => { switchMenuCommand(MENU_KEY_AUTO_BACKUP); }],
- [MENU_KEY_FILTER_NEW_BYTEDANCER, '过滤新人报道 TODO', true, () => { switchMenuCommand(MENU_KEY_FILTER_NEW_BYTEDANCER) }],
- [MENU_KEY_FILTER_ANONYMOUS, '过滤匿名 TODO', true, () => { switchMenuCommand(MENU_KEY_FILTER_ANONYMOUS) }],
- [MENU_KEY_POST_BLACKWORD_LIST, '过滤词列表 (逗号分隔) TODO', '', () => { }],
- [MENU_KEY_DEBUG_MENU, 'DEBUG MENU', 0, () => { console.log(MENU_VALUE, REGISITED_MENU_ID) }]
- ];
- const MENU_VALUE = {};
- const REGISITED_MENU_ID = [];
-
- const TTQ_BACKUP_DB_NAME = 'ttq_backup_db';
-
- // region MENU
- function registerMenuCommand() {
- console.log(1);
- if (REGISITED_MENU_ID.length >= MENU_ALL.length) {
- REGISITED_MENU_ID.forEach(id => GM_unregisterMenuCommand(id));
- REGISITED_MENU_ID.length = 0;
- }
- MENU_ALL.forEach(([key, name, defaultValue, handler]) => {
- let v = MENU_VALUE[key] ?? GM_getValue(key);
- if (v == null){
- GM_setValue(key, defaultValue);
- v = defaultValue;
- };
- MENU_VALUE[key] = v;
- const menuId = GM_registerMenuCommand(`${v === true ? '✅ ' : v === false ? '❌ ': ''}${name}`, handler);
- REGISITED_MENU_ID.push(menuId);
- });
- }
-
- function switchMenuCommand(key) {
- const currentValue = MENU_VALUE[key];
- GM_setValue(key, !currentValue);
- MENU_VALUE[key] = !currentValue;
- registerMenuCommand();
- }
-
- function getMenuValue(key) {
- return MENU_VALUE[key];
- }
- // endregion
-
- // region indexDB
- function TTQDB() {
- this.db = null;
- this.isReady = false;
- this.dbName = TTQ_BACKUP_DB_NAME;
- this.dbVersion = 1;
- this.dbStoreName = 'ttq_backup_store';
- }
-
- TTQDB.prototype.init = function () {
- return new Promise((resolve, reject) => {
- const request = indexedDB.open(this.dbName, this.dbVersion);
- request.onerror = reject;
- request.onsuccess = () => {
- this.db = request.result;
- this.isReady = true;
- resolve(this.db);
- };
- request.onupgradeneeded = () => {
- const db = request.result;
- ['posts', 'comments', 'item_comments', 'likes', 'item_likes', 'users'].forEach(storeName => {
- if (!db.objectStoreNames.contains(storeName)) {
- const store = db.createObjectStore(storeName, { keyPath: 'id' });
- store.createIndex('id', 'id', { unique: true });
- }
- });
- };
- });
- }
-
- TTQDB.prototype.getStore = function (storeName, mode = 'readonly') {
- if (!this.db) { throw new Error('db not init'); }
- return this.db.transaction(storeName, mode).objectStore(storeName);
- }
-
- TTQDB.prototype.get = function (storeName, id) {
- return new Promise((resolve, reject) => {
- const store = this.getStore(storeName);
- const request = store.get(id);
- request.onerror = reject;
- request.onsuccess = () => resolve(request.result);
- });
- }
-
- // TTQDB.prototype.exists = function (storeName, id) {
- // return new Promise((resolve, reject) => {
- // this.ttqDB.get(storeName, id).then(data => {
- // resolve(!!data);
- // }).catch(reject);
- // });
- // }
-
- TTQDB.prototype.getAll = function (storeName) {
- return new Promise((resolve, reject) => {
- const store = this.getStore(storeName);
- const request = store.getAll();
- request.onerror = reject;
- request.onsuccess = () => resolve(request.result);
- });
- }
-
- // TTQDB.prototype.add = function (storeName, data) {
- // return new Promise((resolve, reject) => {
- // const store = this.getStore(storeName, 'readwrite');
- // const request = store.add(data);
- // request.onerror = reject;
- // request.onsuccess = () => resolve(request.result);
- // });
- // }
-
- TTQDB.prototype.put = function (storeName, data) {
- return new Promise((resolve, reject) => {
- const store = this.getStore(storeName, 'readwrite');
- this.get
- const request = store.put(data);
- request.onerror = reject;
- request.onsuccess = () => resolve(request.result);
- });
- }
-
- // TTQDB.prototype.delete = function (storeName, id) {
- // return new Promise((resolve, reject) => {
- // const store = this.getStore(storeName, 'readwrite');
- // const request = store.delete(id);
- // request.onerror = reject;
- // request.onsuccess = () => resolve(request.result);
- // });
- // }
-
- // TTQDB.prototype.clear = function (storeName) {
- // return new Promise((resolve, reject) => {
- // const store = this.getStore(storeName, 'readwrite');
- // const request = store.clear();
- // request.onerror = reject;
- // request.onsuccess = () => resolve(request.result);
- // });
- // }
-
- // TTQDB.prototype.count = function (storeName) {
- // return new Promise((resolve, reject) => {
- // const store = this.getStore(storeName);
- // const request = store.count();
- // request.onerror = reject;
- // request.onsuccess = () => resolve(request.result);
- // });
- // }
-
- // TTQDB.prototype.close = function () {
- // this.db && this.db.close();
- // }
-
- // TTQDB.prototype.deleteDB = function () {
- // return new Promise((resolve, reject) => {
- // const request = indexedDB.deleteDatabase(this.dbName);
- // request.onerror = reject;
- // request.onsuccess = () => resolve(request.result);
- // });
- // }
-
- const ttqDB = new TTQDB();
- window.unsafeWindow.ttqDB = ttqDB;
-
- async function backupRes(res) {
- if (!ttqDB.isReady) await ttqDB.init();
- res.entities && ['posts', 'comments', 'likes', 'users'].forEach(storeName =>
- res.entities[storeName] && Object.values(res.entities[storeName]).forEach(data => {
- ttqDB.put(storeName, data);
- console.log('put: ', storeName, data.id, data);
- })
- );
- res.relationships && ['item_comments', 'item_likes'].forEach(storeName =>
- res.relationships[storeName]
- && Object.entries(res.relationships[storeName])
- .forEach(
- ([id, data]) => ttqDB.put(storeName, { ...data, id: parseInt(id, 10) })
- )
- );
- }
- // endregion
-
- // region main
- // region main - hack fetch
- const originFetch = fetch;
- window.unsafeWindow.fetch = (url, options) => {
- return originFetch(url, options).then(async (response) => {
- console.log('hack: ', url, options)
- if (url.includes('/malaita/v2/user/settings/')) {
- // 获取用户设置
- if (getMenuValue(MENU_KEY_TIME_ORDER)) {
- console.log('hit settings api');
- const responseClone = response.clone();
- let res = await responseClone.json();
- res.feed_type = 1;
- console.log(res);
- return new Response(JSON.stringify(res), response);
- }
- } else if (
- url.includes('/malaita/feeds/enter/') // 首屏
- || url.includes('/malaita/feeds/time_sequential/') // 下拉刷新 feed
- || /\/malaita\/v2\/1\/items\/\d+\/detail\//.exec(url) // 展开评论
- || /\/malaita\/v2\/1\/items\/\d+\/likes\//.exec(url) // 展开点赞
- || /\/malaita\/v2\/users\/\d+\/\?email_prefix=/.exec(url) // 他人信息
- || /\/malaita\/users\/\d+\/posts\//.exec(url) // 主页 posts
- ) {
- // feed 与 展开点赞 / 评论列表
- const responseClone = response.clone();
- let res = await responseClone.json();
-
- // backup
- if (getMenuValue(MENU_KEY_AUTO_BACKUP)) {
- console.log('hit backup posts: ', url);
- backupRes(res);
- }
- return response;
- } else {
- return response;
- }
- });
- };
- // endregion
-
- // region main - hack xhr
- const originXhrOpen = XMLHttpRequest.prototype.open;
- XMLHttpRequest.prototype.open = function (method, url, ...args) {
- // 屏蔽埋点上报
- console.log(url)
- if (url.includes('snssdk.com/') || url.includes('https://ee.bytedance.net/sentry/api/')) {
- console.log('已拦截埋点上报: ', url);
- this.abort();
- return;
- }
- return originXhrOpen.call(this, method, url, ...args);
- };
- // endregion
-
- // region main - backup post
-
- // endregion
-
- // region main - register menu
- registerMenuCommand();
- // endregion
- // endregion
- })();