字节圈增强脚本

try to take over the world!

当前为 2023-02-03 提交的版本,查看 最新版本

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