🏠 返回首頁 

Greasy Fork is available in English.

PTT Web Image Fix

修復PTT網頁板自動開圖、嘗試修復被截斷的網址、阻擋黑名單ID的推文/圖片

  1. // ==UserScript==
  2. // @name PTT Web Image Fix
  3. // @namespace https://github.com/x94fujo6rpg/SomeTampermonkeyScripts
  4. // @version 0.11
  5. // @description 修復PTT網頁板自動開圖、嘗試修復被截斷的網址、阻擋黑名單ID的推文/圖片
  6. // @author x94fujo6
  7. // @include https://www.ptt.cc/*
  8. // @grant GM_xmlhttpRequest
  9. // @grant GM_getValue
  10. // @grant GM_setValue
  11. // @grant GM_registerMenuCommand
  12. // @require https://openuserjs.org/src/libs/sizzle/GM_config.js
  13. // @run-at document-start
  14. // ==/UserScript==
  15. /*
  16. 0.11
  17. 新增阻擋關鍵字功能
  18. 自行修改key_word內容 (regex)
  19. F12控制台會顯示整批被阻擋之ID跟推文內容
  20. ID阻擋>圖片阻擋>關鍵字阻擋
  21. */
  22. (function () {
  23. let
  24. blacklist_id = ["s910408", "ig49999", "bowen5566", "sos976431"],
  25. blacklist_img = ["Dey10PF", "WfOR5a8", "wsG5vrZ", "Q7hvcZw", "7h9s0iC", "g28oNwO", "y9arWAn", "9QnqRM3", "UeImoq1", "snzmE7h", "cJXK0nM", "jWy4BKY", "feMElhb", "CpGkeGb", "txz4iGW", "W2i4y4k", "aVXa6GN", "Mni1ayO"],
  26. blocked_id = new Set([]),
  27. blocked_img = new Set([]);
  28. let key_word = `五樓|覺青|莫斯科|演員|司機|小丑|嘻嘻`;
  29. key_word = new RegExp(key_word);
  30. const
  31. script_name = "fix ptt img", fixed = "fix_by_script",
  32. rd_text = (text = "") => {
  33. if (text.length <= 11) return " ██REDACTED██";
  34. let rp = (len) => "█".repeat(len),
  35. side = (text.length - 11) / 2;
  36. side = (side > 1) ? rp(side) : "█";
  37. return ` ${side}REDACTED${side}`;
  38. },
  39. remove_blacklist_target = async () => {
  40. let user, text, ele,
  41. ck_id, ck_img,
  42. push_content = document.querySelectorAll("div.push"),
  43. reg = /(?<=\/)(\w+)(?:\.\w{3,4})*$/;
  44. let ck_kw, kw_list = [];
  45. push_content.forEach(div => {
  46. user = div.querySelector(".push-userid").textContent.trim();
  47. ele = div.querySelector(".push-content");
  48. text = ele.textContent;
  49. ck_id = blacklist_id.find(id => id == user);
  50. ck_img = blacklist_img.find(img => text.includes(`/${img}`));
  51. //ck_kw = key_word.find(key => text.toLowerCase().match(`${key}`));
  52. ck_kw = text.toLowerCase().match(key_word);
  53. if (ck_id || ck_img || ck_kw) {
  54. ele.title = text;
  55. ele.innerHTML = rd_text(text);
  56. ele.style = "color: darkred;";
  57. slog_c(`%cblock by id blacklist %c${user}:%c${ele.title.replace(":", "").trim()}`, "#FF0000;#FFFF00;"); //.replace(/:[\s]*(https|https)*(:\/\/)*/, "")
  58. if (ck_kw && !ck_id && !ck_img) {
  59. kw_list.push(
  60. {
  61. user,
  62. text
  63. }
  64. );
  65. }
  66. if (ck_id && !ck_img) {
  67. text = text.match(reg);
  68. if (text) {
  69. slog_c(`%cblock by id blacklist %c${user}:%c${ele.title.replace(":", "").trim()}%c img [%c${text[1]}%c] not in list`, "#40E0D0;#FFFF00;;#40E0D0;;#40E0D0");
  70. blocked_img.add(text[1]);
  71. }
  72. }
  73. if (!ck_id && ck_img) {
  74. slog_c(`%cblock by img blacklist %c${user}:%c${ele.title.replace(":", "").trim()}%c user [%c${user}%c] not in list`, "#FFA500;#FFFF00;;#FFA500;;#FFA500");
  75. blocked_id.add(user);
  76. }
  77. }
  78. });
  79. if (kw_list.length > 0) {
  80. let _list = kw_list.map(data => data.user);
  81. _list = new Set(_list);
  82. _list = [..._list];
  83. slog(`block by key word`);
  84. slog(`\n` + _list.join("\n"));
  85. _list = {};
  86. kw_list.forEach(data => {
  87. if (!_list[data.user]) {
  88. _list[data.user] = [];
  89. }
  90. _list[data.user].push(data.text);
  91. });
  92. Object.keys(_list).forEach(id => {
  93. _list[id].forEach(t => {
  94. console.log(`${id}${t}`);
  95. });
  96. });
  97. }
  98. return true;
  99. },
  100. slog = (...any) => console.log(`[${script_name}]`, ...any),
  101. slog_c = (s = "", c = "") => console.log(`[${script_name}] ${s}`, ...c.split(";").map(_c => `color:${_c};`)),
  102. wait_tab = () => {
  103. return new Promise(resolve => {
  104. if (document.visibilityState === "visible") return resolve();
  105. slog("tab in background, script paused");
  106. document.addEventListener("visibilitychange", () => {
  107. if (document.visibilityState === "visible") { slog("script unpaused"); return resolve(); }
  108. });
  109. });
  110. },
  111. remove_richcontent = async () => {
  112. let eles = document.querySelectorAll(".richcontent");
  113. if (!eles.length) { slog(`no richcontent found`); return false; }
  114. slog(`remove ${eles.length} richcontent`);
  115. eles.forEach(e => { if (!e.innerHTML.match(/(youtube.com|youtu.be|-player")/)) e.remove(); });
  116. return true;
  117. },
  118. async_push = async (list, item) => list.push(item),
  119. extractor = async (e, adv = false) => {
  120. let url = e.href, extract = false,
  121. reg_list = [
  122. /imgur\.com\/gallery\/\w{5,7}/,
  123. /imgur\.com\/a\/\w{5,7}/,
  124. /imgur\.com\/\w{5,7}/,
  125. /pbs\.twimg\.com\/media\/[\w-]+/,
  126. /(?<=https:\/\/|http:\/\/).*\.\w{3,4}$/,
  127. ],
  128. twitter_format = /(?<=media[^\.\n]+\.|format=)\w{3,4}/,
  129. format_check = /\.(jpg|jpeg|png|webp|gif|gifv|mp4|webm)$/;
  130. if (e.getAttribute(fixed)) return false;
  131. if (adv) {
  132. if (e.nextSibling) {
  133. if (e.nextSibling.nodeType !== 3) return false;
  134. url += e.nextSibling.textContent.trim();
  135. }
  136. if (reg_list.find(reg => e.textContent.match(reg))) return false;
  137. }
  138. if (!url) return false; // no link
  139. reg_list = reg_list.map(reg => url.match(reg));
  140. extract = reg_list.findIndex(reg => Boolean(reg));
  141. if (extract == -1) return false; //no reg match
  142. if ((extract == reg_list.length - 1) && !(url.match(format_check))) return false; //match the last reg but not in format list
  143. extract = `https://${reg_list[extract][0]}`;
  144. if (extract.includes("pbs.twimg.com")) extract += `.${url.match(twitter_format)[0]}`;
  145. return extract;
  146. },
  147. extract_in_text = (eles) => extract_url(eles, true),
  148. extract_url = async (eles, in_text = false) => {
  149. let list = [], url;
  150. for (let e of eles) {
  151. url = await extractor(e, in_text);
  152. if (!url) continue;
  153. await async_push(list, { e, url });
  154. }
  155. return list;
  156. },
  157. get_imgur_image = (url) => {
  158. return new Promise(reslove => {
  159. GM_xmlhttpRequest({
  160. method: "GET", url,
  161. onload: async (rs) => {
  162. let full_url = rs.responseText.match(/(https:\/\/i.imgur\.com\/\w+\.\w{3,4})\W[\w#]+">/);
  163. full_url = full_url ? full_url[1] : false;
  164. slog(!full_url ? `${url} has no data` : `GET ${url} done`);
  165. return reslove([full_url, rs.status]);
  166. },
  167. });
  168. });
  169. },
  170. create_img_ele = (url, get_img) => {
  171. let box = Object.assign(document.createElement("div"), { className: "richcontent" }),
  172. a = Object.assign(document.createElement("a"), {
  173. href: url,
  174. target: "_blank",
  175. style: "text-decoration: none; box-shadow: none; background: none;",
  176. innerHTML: `<img src="${url}" referrerpolicy="no-referrer" rel="noreferrer noopener nofollow">`, //loading="lazy"
  177. referrerPolicy: "no-referrer",
  178. rel: "noreferrer noopener nofollow",
  179. });
  180. if (!get_img) {
  181. a.style = a.innerHTML = "";
  182. a.textContent = `${url} (分段修復)`;
  183. }
  184. a.setAttribute(fixed, 1);
  185. box.appendChild(a);
  186. return box;
  187. },
  188. create_rd_ele = (text = "") => {
  189. return Object.assign(document.createElement("div"), {
  190. className: "richcontent",
  191. style: "color: darkred;",
  192. title: text,
  193. textContent: rd_text(text),
  194. });
  195. },
  196. fix_image = async (obj, get_img) => {
  197. let url, status;
  198. if (obj.url.includes("imgur")) {
  199. for (let retry = 3; retry >= 0; retry--) {
  200. if (retry < 3) slog(`retry ${obj.url}, remain ${retry}`);
  201. [url, status] = await get_imgur_image(obj.url);
  202. if (status == 200) break;
  203. }
  204. } else {
  205. url = obj.url;
  206. }
  207. if (!url) url = "https://i.imgur.com/removed.png";
  208. url = (blacklist_img.find(img => url.includes(img))) ? create_rd_ele(url) : create_img_ele(url, get_img);
  209. obj.e.insertAdjacentElement("afterend", url);
  210. obj.e.target = "_blank";
  211. obj.e.setAttribute(fixed, 1);
  212. return;
  213. },
  214. process_ele = async (eles, extractor, log, get_img = true) => {
  215. if (!eles.length) return;
  216. eles = await extractor(eles);
  217. if (!eles?.length) return;
  218. slog(log, eles);
  219. eles.forEach(e => fix_image(e, get_img));
  220. return;
  221. },
  222. main = async () => {
  223. let eles = document.querySelectorAll("a[href]");
  224. /*
  225. await process_ele(eles, extract_url, "try fix");
  226. await sleep(1000);
  227. */
  228. await process_ele(eles, extract_in_text, "try fix spaced", GM_config.get("fix_segment"));
  229. },
  230. sleep = (ms = 100) => new Promise(resolve => setTimeout(resolve, ms)),
  231. start_script = async () => {
  232. slog("script start");
  233. await wait_tab();
  234. await ini_config();
  235. await load_value();
  236. await remove_blacklist_target();
  237. //await remove_richcontent();
  238. await main();
  239. GM_config.onOpen(); // update ui
  240. },
  241. load_value = async () => {
  242. blacklist_id = GM_config.get("blacklist_id").split("\n");
  243. blacklist_img = GM_config.get("blacklist_img").split("\n");
  244. slog("load blacklist, id", blacklist_id.length, "img", blacklist_img.length);
  245. return true;
  246. },
  247. ini_config = async () => {
  248. GM_registerMenuCommand("設定(alt+Q)", () => GM_config.open());
  249. document.addEventListener("keydown", (e) => { if (e.altKey && e.key == "q") GM_config.open(); });
  250. GM_config.init({
  251. id: "settings", // The id used for this instance of GM_config
  252. title: "腳本設定",
  253. fields: {
  254. // This is the id of the field
  255. fix_segment: {
  256. label: "嘗試對分段網址開圖 (小心使用)",
  257. section: "功能設定",
  258. type: "checkbox",
  259. default: true,
  260. },
  261. blacklist_id: {
  262. label: "ID",
  263. section: ["黑名單", "每行一個ID/圖片檔名"],
  264. type: "textarea",
  265. default: blacklist_id.join("\n")
  266. },
  267. blocked_id: {
  268. label: "由於圖片被阻擋,但ID未在名單中",
  269. type: "textarea",
  270. default: [...blocked_id].join("\n")
  271. },
  272. blacklist_img: {
  273. label: "圖片名稱",
  274. type: "textarea",
  275. default: blacklist_img.join("\n")
  276. },
  277. blocked_img: {
  278. label: "由於ID被阻擋,但圖片未在名單中",
  279. type: "textarea",
  280. default: [...blocked_img].join("\n")
  281. },
  282. },
  283. css: `
  284. #settings_fix_segment_var {
  285. display: inline-flex;
  286. margin: 0.5rem !important;
  287. border: 0.1rem solid;
  288. padding: 0.5rem;
  289. }
  290. #settings_blacklist_id_var,#settings_blacklist_img_var,#settings_blocked_id_var,#settings_blocked_img_var {
  291. width: calc(90% / 4) !important;
  292. height: 60% !important;
  293. margin: 1rem !important;
  294. display: inline-block;
  295. }
  296. #settings_blacklist_id_field_label,#settings_blacklist_img_field_label,#settings_blocked_id_field_label,#settings_blocked_img_field_label,#settings_fix_segment_field_label {
  297. font-size: 1rem !important;
  298. text-align: center;
  299. display: block;
  300. }
  301. #settings_field_blacklist_id,#settings_field_blacklist_img,#settings_field_blocked_id,#settings_field_blocked_img {
  302. width: 100% !important;
  303. height: 90% !important;
  304. }
  305. `,
  306. });
  307. const
  308. blacklist = ["blacklist_id", "blacklist_img",],
  309. load_list = (list_id = "") => { return GM_config.get(list_id); };
  310. GM_config.onOpen = () => {
  311. let ui_id = [
  312. { id: "blocked_id", data: blocked_id },
  313. { id: "blocked_img", data: blocked_img },
  314. ];
  315. ui_id.forEach(o => {
  316. slog("load", o.id, o.data.size);
  317. GM_config.fields[o.id].value = [...o.data].join("\n");
  318. });
  319. blacklist.forEach(id => {
  320. let list = load_list(id);
  321. slog("load", id, list.split("\n").length);
  322. GM_config.fields[id].value = list;
  323. });
  324. };
  325. GM_config.onSave = () => {
  326. blacklist.forEach(id => {
  327. let list = load_list(id);
  328. slog("save", id, list.split("\n").length);
  329. });
  330. };
  331. return true;
  332. };
  333. document.body.onload = start_script;
  334. })();