字节圈增强脚本

try to take over the world!

目前為 2023-02-03 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name 字节圈增强脚本
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.5
  5. // @license WTFPL
  6. // @description try to take over the world!
  7. // @author Yxxx
  8. // @match https://ee.bytedance.net/malaita/pc/*
  9. // @icon https://ee.bytedance.net/malaita/static/img/malaita.png
  10. // @grant GM_registerMenuCommand
  11. // @grant GM_unregisterMenuCommand
  12. // @grant GM_getValue
  13. // @grant GM_setValue
  14. // @grant unsafeWindow
  15. // ==/UserScript==
  16.  
  17. (function () {
  18. const MENU_KEY_TIME_ORDER = 'menu_time_order';
  19. const MENU_KEY_AUTO_BACKUP = 'menu_auto_backup';
  20. const MENU_KEY_FILTER_NEW_BYTEDANCER = 'menu_filter_new_bytedancer';
  21. const MENU_KEY_FILTER_ANONYMOUS = 'menu_key_filter_anonymous';
  22. const MENU_KEY_POST_BLACKWORD_LIST = 'menu_key_post_blackword_list';
  23. const MENU_KEY_DEBUG_MENU = 'menu_key_debug_menu';
  24.  
  25. const MENU_ALL = [
  26. [MENU_KEY_TIME_ORDER, '帖子按时间排序', true, () => { switchMenuCommand(MENU_KEY_TIME_ORDER); alert('请手动刷新页面以生效.'); }],
  27. [MENU_KEY_AUTO_BACKUP, '备份浏览过的帖子', false, () => { switchMenuCommand(MENU_KEY_AUTO_BACKUP); }],
  28. [MENU_KEY_FILTER_NEW_BYTEDANCER, '过滤新人报道 TODO', true, () => { switchMenuCommand(MENU_KEY_FILTER_NEW_BYTEDANCER) }],
  29. [MENU_KEY_FILTER_ANONYMOUS, '过滤匿名 TODO', true, () => { switchMenuCommand(MENU_KEY_FILTER_ANONYMOUS) }],
  30. [MENU_KEY_POST_BLACKWORD_LIST, '过滤词列表 (逗号分隔) TODO', '', () => { }],
  31. [MENU_KEY_DEBUG_MENU, 'DEBUG MENU', 0, () => { console.log(MENU_VALUE, REGISITED_MENU_ID) }]
  32. ];
  33. const MENU_VALUE = {};
  34. const REGISITED_MENU_ID = [];
  35.  
  36. const TTQ_BACKUP_DB_NAME = 'ttq_backup_db';
  37.  
  38. // region MENU
  39. function registerMenuCommand() {
  40. console.log(1);
  41. if (REGISITED_MENU_ID.length >= MENU_ALL.length) {
  42. REGISITED_MENU_ID.forEach(id => GM_unregisterMenuCommand(id));
  43. REGISITED_MENU_ID.length = 0;
  44. }
  45. MENU_ALL.forEach(([key, name, defaultValue, handler]) => {
  46. let v = MENU_VALUE[key] ?? GM_getValue(key);
  47. if (v == null){
  48. GM_setValue(key, defaultValue);
  49. v = defaultValue;
  50. };
  51. MENU_VALUE[key] = v;
  52. const menuId = GM_registerMenuCommand(`${v === true ? '✅ ' : v === false ? '❌ ': ''}${name}`, handler);
  53. REGISITED_MENU_ID.push(menuId);
  54. });
  55. }
  56.  
  57. function switchMenuCommand(key) {
  58. const currentValue = MENU_VALUE[key];
  59. GM_setValue(key, !currentValue);
  60. MENU_VALUE[key] = !currentValue;
  61. registerMenuCommand();
  62. }
  63.  
  64. function getMenuValue(key) {
  65. return MENU_VALUE[key];
  66. }
  67. // endregion
  68.  
  69. // region indexDB
  70. function TTQDB() {
  71. this.db = null;
  72. this.isReady = false;
  73. this.dbName = TTQ_BACKUP_DB_NAME;
  74. this.dbVersion = 1;
  75. this.dbStoreName = 'ttq_backup_store';
  76. }
  77.  
  78. TTQDB.prototype.init = function () {
  79. return new Promise((resolve, reject) => {
  80. const request = indexedDB.open(this.dbName, this.dbVersion);
  81. request.onerror = reject;
  82. request.onsuccess = () => {
  83. this.db = request.result;
  84. this.isReady = true;
  85. resolve(this.db);
  86. };
  87. request.onupgradeneeded = () => {
  88. const db = request.result;
  89. ['posts', 'comments', 'item_comments', 'likes', 'item_likes', 'users'].forEach(storeName => {
  90. if (!db.objectStoreNames.contains(storeName)) {
  91. const store = db.createObjectStore(storeName, { keyPath: 'id' });
  92. store.createIndex('id', 'id', { unique: true });
  93. }
  94. });
  95. };
  96. });
  97. }
  98.  
  99. TTQDB.prototype.getStore = function (storeName, mode = 'readonly') {
  100. if (!this.db) { throw new Error('db not init'); }
  101. return this.db.transaction(storeName, mode).objectStore(storeName);
  102. }
  103.  
  104. TTQDB.prototype.get = function (storeName, id) {
  105. return new Promise((resolve, reject) => {
  106. const store = this.getStore(storeName);
  107. const request = store.get(id);
  108. request.onerror = reject;
  109. request.onsuccess = () => resolve(request.result);
  110. });
  111. }
  112.  
  113. // TTQDB.prototype.exists = function (storeName, id) {
  114. // return new Promise((resolve, reject) => {
  115. // this.ttqDB.get(storeName, id).then(data => {
  116. // resolve(!!data);
  117. // }).catch(reject);
  118. // });
  119. // }
  120.  
  121. TTQDB.prototype.getAll = function (storeName) {
  122. return new Promise((resolve, reject) => {
  123. const store = this.getStore(storeName);
  124. const request = store.getAll();
  125. request.onerror = reject;
  126. request.onsuccess = () => resolve(request.result);
  127. });
  128. }
  129.  
  130. // TTQDB.prototype.add = function (storeName, data) {
  131. // return new Promise((resolve, reject) => {
  132. // const store = this.getStore(storeName, 'readwrite');
  133. // const request = store.add(data);
  134. // request.onerror = reject;
  135. // request.onsuccess = () => resolve(request.result);
  136. // });
  137. // }
  138.  
  139. TTQDB.prototype.put = function (storeName, data) {
  140. return new Promise((resolve, reject) => {
  141. const store = this.getStore(storeName, 'readwrite');
  142. this.get
  143. const request = store.put(data);
  144. request.onerror = reject;
  145. request.onsuccess = () => resolve(request.result);
  146. });
  147. }
  148.  
  149. // TTQDB.prototype.delete = function (storeName, id) {
  150. // return new Promise((resolve, reject) => {
  151. // const store = this.getStore(storeName, 'readwrite');
  152. // const request = store.delete(id);
  153. // request.onerror = reject;
  154. // request.onsuccess = () => resolve(request.result);
  155. // });
  156. // }
  157.  
  158. // TTQDB.prototype.clear = function (storeName) {
  159. // return new Promise((resolve, reject) => {
  160. // const store = this.getStore(storeName, 'readwrite');
  161. // const request = store.clear();
  162. // request.onerror = reject;
  163. // request.onsuccess = () => resolve(request.result);
  164. // });
  165. // }
  166.  
  167. // TTQDB.prototype.count = function (storeName) {
  168. // return new Promise((resolve, reject) => {
  169. // const store = this.getStore(storeName);
  170. // const request = store.count();
  171. // request.onerror = reject;
  172. // request.onsuccess = () => resolve(request.result);
  173. // });
  174. // }
  175.  
  176. // TTQDB.prototype.close = function () {
  177. // this.db && this.db.close();
  178. // }
  179.  
  180. // TTQDB.prototype.deleteDB = function () {
  181. // return new Promise((resolve, reject) => {
  182. // const request = indexedDB.deleteDatabase(this.dbName);
  183. // request.onerror = reject;
  184. // request.onsuccess = () => resolve(request.result);
  185. // });
  186. // }
  187.  
  188. const ttqDB = new TTQDB();
  189. window.unsafeWindow.ttqDB = ttqDB;
  190.  
  191. async function backupRes(res) {
  192. if (!ttqDB.isReady) await ttqDB.init();
  193. res.entities && ['posts', 'comments', 'likes', 'users'].forEach(storeName =>
  194. res.entities[storeName] && Object.values(res.entities[storeName]).forEach(data => {
  195. ttqDB.put(storeName, data);
  196. console.log('put: ', storeName, data.id, data);
  197. })
  198. );
  199. res.relationships && ['item_comments', 'item_likes'].forEach(storeName =>
  200. res.relationships[storeName]
  201. && Object.entries(res.relationships[storeName])
  202. .forEach(
  203. ([id, data]) => ttqDB.put(storeName, { ...data, id: parseInt(id, 10) })
  204. )
  205. );
  206. }
  207. // endregion
  208.  
  209. // region main
  210. // region main - hack fetch
  211. const originFetch = fetch;
  212. window.unsafeWindow.fetch = (url, options) => {
  213. return originFetch(url, options).then(async (response) => {
  214. console.log('hack: ', url, options)
  215. if (url.includes('/malaita/v2/user/settings/')) {
  216. // 获取用户设置
  217. if (getMenuValue(MENU_KEY_TIME_ORDER)) {
  218. console.log('hit settings api');
  219. const responseClone = response.clone();
  220. let res = await responseClone.json();
  221. res.feed_type = 1;
  222. console.log(res);
  223. return new Response(JSON.stringify(res), response);
  224. }
  225. } else if (
  226. url.includes('/malaita/feeds/enter/') // 首屏
  227. || url.includes('/malaita/feeds/time_sequential/') // 下拉刷新 feed
  228. || /\/malaita\/v2\/1\/items\/\d+\/detail\//.exec(url) // 展开评论
  229. || /\/malaita\/v2\/1\/items\/\d+\/likes\//.exec(url) // 展开点赞
  230. || /\/malaita\/v2\/users\/\d+\/\?email_prefix=/.exec(url) // 他人信息
  231. || /\/malaita\/users\/\d+\/posts\//.exec(url) // 主页 posts
  232. ) {
  233. // feed 与 展开点赞 / 评论列表
  234. const responseClone = response.clone();
  235. let res = await responseClone.json();
  236.  
  237. // backup
  238. if (getMenuValue(MENU_KEY_AUTO_BACKUP)) {
  239. console.log('hit backup posts: ', url);
  240. backupRes(res);
  241. }
  242. return response;
  243. } else {
  244. return response;
  245. }
  246. });
  247. };
  248. // endregion
  249.  
  250. // region main - hack xhr
  251. const originXhrOpen = XMLHttpRequest.prototype.open;
  252. XMLHttpRequest.prototype.open = function (method, url, ...args) {
  253. // 屏蔽埋点上报
  254. console.log(url)
  255. if (url.includes('snssdk.com/') || url.includes('https://ee.bytedance.net/sentry/api/')) {
  256. console.log('已拦截埋点上报: ', url);
  257. this.abort();
  258. return;
  259. }
  260. return originXhrOpen.call(this, method, url, ...args);
  261. };
  262. // endregion
  263.  
  264. // region main - backup post
  265.  
  266. // endregion
  267.  
  268. // region main - register menu
  269. registerMenuCommand();
  270. // endregion
  271. // endregion
  272. })();