futaba-add-uploader

ふたばちゃんねるの投稿フォームに「あぷ小」へのアップロード機能を追加します

当前为 2024-09-12 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name futaba-add-uploader
  3. // @namespace http://2chan.net/
  4. // @version 0.2.0
  5. // @description ふたばちゃんねるの投稿フォームに「あぷ小」へのアップロード機能を追加します
  6. // @author ame-chan
  7. // @match http://*.2chan.net/b/res/*
  8. // @match https://*.2chan.net/b/res/*
  9. // @icon https://www.google.com/s2/favicons?sz=64&domain=2chan.net
  10. // @grant GM_xmlhttpRequest
  11. // @license MIT
  12. // @run-at document-idle
  13. // @connect 2chan.net
  14. // @connect *.2chan.net
  15. // @connect img.2chan.net
  16. // @connect dec.2chan.net
  17. // ==/UserScript==
  18. (() => {
  19. 'use strict';
  20. const addStyle = `<style id="userjs-add-uploader">
  21. .ftbl {
  22. width: 510px;
  23. }
  24. [target="futaba_viewer_postcontents"] .ftbl {
  25. margin: 0 0 32px !important;
  26. }
  27. .ftdc {
  28. width: 100px;
  29. }
  30. #up2input + #fileselector-button-clear {
  31. display: none !important;
  32. }
  33. #up2error {
  34. display: none;
  35. color: #ff0000;
  36. }
  37. #up2error.is-visible {
  38. display: block;
  39. }
  40. .userjs-uploadcell:has(#file_control) {
  41. display: flex;
  42. align-items: flex-start;
  43. flex-wrap: wrap;
  44. }
  45. .userjs-loading {
  46. display: flex;
  47. gap: 4px;
  48. align-items: center;
  49. justify-content: center;
  50. }
  51. </style>`;
  52. const targetUploader = {
  53. 'あぷ小': {
  54. name: 'あぷ小',
  55. max_file_size: 3000000,
  56. max_file_size_text: '※あぷ小は3MBまで',
  57. post_url: '//dec.2chan.net/up2/up.php',
  58. get_url: '//dec.2chan.net/up2/up.htm',
  59. },
  60. 'あぷ': {
  61. name: 'あぷ',
  62. max_file_size: 10000000,
  63. max_file_size_text: '※あぷは10MBまで',
  64. post_url: '//dec.2chan.net/up/up.php',
  65. get_url: '//dec.2chan.net/up/up.htm',
  66. },
  67. };
  68. const showErrorText = () => document.querySelector('#up2error')?.classList.add('is-visible');
  69. const hideErrorText = () => document.querySelector('#up2error')?.classList.remove('is-visible');
  70. const addUploader = () => {
  71. const inputAreaElm = document.querySelector('.ftbl tbody');
  72. const html = `<tr>
  73. <td class="ftdc">
  74. <b><span data-uploader-name>あぷ小</span>にUP</b>
  75. </td>
  76. <td class="userjs-uploadcell">
  77. <input id="up2input" name="userjs-uploader" type="file" size="40">
  78. <button id="up2submit" type="button">アップロード</button>
  79. <span id="up2error" data-uploader-text>※あぷ小は3MBまで</span>
  80. </td>
  81. </tr>`;
  82. const replaceHtml = (uploader) => {
  83. const nameElm = document.querySelector('[data-uploader-name]');
  84. const errorTextElm = document.querySelector('[data-uploader-text]');
  85. if (nameElm) {
  86. nameElm.textContent = uploader.name;
  87. }
  88. if (errorTextElm) {
  89. errorTextElm.textContent = uploader.max_file_size_text;
  90. }
  91. };
  92. const arrayBufferToHex = (arrayBuffer) =>
  93. Array.from(new Uint8Array(arrayBuffer))
  94. .map((b) => b.toString(16).padStart(2, '0'))
  95. .join('');
  96. const calculateSHA1 = async (file) => {
  97. const buffer = await file.arrayBuffer();
  98. const message = new TextEncoder().encode(arrayBufferToHex(buffer));
  99. const hashBuffer = await crypto.subtle.digest('SHA-1', message);
  100. return arrayBufferToHex(hashBuffer);
  101. };
  102. const setInputState = {
  103. disabled(up2inputElm, up2submit) {
  104. up2inputElm.disabled = true;
  105. up2submit.innerHTML = `<div class="userjs-loading">
  106. <svg width="18px" height="18px" display="block" shape-rendering="auto" style="background:none;margin:auto" preserveAspectRatio="xMidYMid" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
  107. <circle cx="50" cy="50" r="35" fill="none" stroke="#aaa" stroke-dasharray="164.93361431346415 56.97787143782138" stroke-width="10">
  108. <animateTransform attributeName="transform" dur="1s" keyTimes="0;1" repeatCount="indefinite" type="rotate" values="0 50 50;360 50 50"/>
  109. </circle>
  110. </svg>
  111. <span>アップロード中...</span>
  112. </div>`;
  113. up2submit.disabled = true;
  114. },
  115. enabled(up2inputElm, up2submit) {
  116. up2inputElm.disabled = false;
  117. up2submit.innerHTML = 'アップロード';
  118. up2submit.disabled = false;
  119. },
  120. };
  121. const setErrorText = (text) => {
  122. const errorElm = document.querySelector('#up2error');
  123. if (errorElm) {
  124. errorElm.textContent = text;
  125. }
  126. };
  127. const setError = (text) => {
  128. const up2inputElm = document.querySelector('#up2input');
  129. const up2submitElm = document.querySelector('#up2submit');
  130. if (up2inputElm instanceof HTMLInputElement && up2submitElm instanceof HTMLButtonElement) {
  131. setErrorText(text);
  132. showErrorText();
  133. setInputState.enabled(up2inputElm, up2submitElm);
  134. }
  135. };
  136. const uploadFile = async (file, uploader) => {
  137. const formData = new FormData();
  138. const sha1 = await calculateSHA1(file);
  139. formData.append('MAX_FILE_SIZE', String(uploader.max_file_size));
  140. formData.append('mode', 'reg');
  141. formData.append('up', file);
  142. formData.append('com', sha1);
  143. try {
  144. await fetch(`${location.protocol}${uploader.post_url}`, {
  145. method: 'POST',
  146. body: formData,
  147. });
  148. } catch (e) {
  149. } finally {
  150. hideErrorText();
  151. return sha1;
  152. }
  153. };
  154. const getUploaderHTML = (uploader) => {
  155. return new Promise((resolve) => {
  156. GM_xmlhttpRequest({
  157. method: 'GET',
  158. url: `${location.protocol}${uploader.get_url}`,
  159. responseType: 'arraybuffer',
  160. headers: {
  161. 'Cache-Control': 'no-cache',
  162. },
  163. onload: (response) => {
  164. if (response.status === 200) {
  165. resolve(response.response);
  166. } else {
  167. resolve(false);
  168. }
  169. },
  170. onerror: () => resolve(false),
  171. });
  172. });
  173. };
  174. const isAllowExtension = (up2inputElm) => {
  175. const allowExtension =
  176. /\.(3g2|3gp|7z|ai|aif|asf|avi|bmp|c|doc|eps|exe|f4v|flv|gca|gif|htm|html|jpeg|jpg|lzh|m4a|mgx|mht|mid|mkv|mmf|mov|mp3|mp4|mpeg|mpg|mpo|mqo|ogg|pdf|pls|png|ppt|psd|ram|rar|rm|rpy|sai|swf|tif|tiff|txt|wav|webm|webp|wma|wmv|xls|zip)$/;
  177. return allowExtension.test(up2inputElm.value);
  178. };
  179. const selectedUploader = (size) => {
  180. const up2MaxSize = targetUploader['あぷ小'].max_file_size;
  181. return size > up2MaxSize ? targetUploader['あぷ'] : targetUploader['あぷ小'];
  182. };
  183. const uploadHandler = async (up2inputElm, up2submitElm) => {
  184. const htmlParser = (uploaderHTML) => {
  185. const textDecoder = new TextDecoder('Shift_JIS');
  186. const html = textDecoder.decode(uploaderHTML);
  187. const parser = new DOMParser();
  188. const dom = parser.parseFromString(html, 'text/html');
  189. if (dom) {
  190. return dom;
  191. }
  192. return false;
  193. };
  194. hideErrorText();
  195. setInputState.disabled(up2inputElm, up2submitElm);
  196. if (!up2inputElm.value || up2inputElm.files === null) {
  197. setError('ファイルが選択されていません');
  198. return;
  199. }
  200. const file = up2inputElm.files[0];
  201. const uploader = selectedUploader(file.size);
  202. if (file.size > uploader.max_file_size) {
  203. setError(uploader.max_file_size_text);
  204. return;
  205. }
  206. if (!isAllowExtension(up2inputElm)) {
  207. setError('アップロードが許可されていない拡張子です');
  208. return;
  209. }
  210. // ファイルのアップロードとSHA-1の取得
  211. const sha1 = await uploadFile(file, uploader);
  212. if (!sha1) {
  213. setError('アップロードファイルのSHA-1取得に失敗しました');
  214. return;
  215. }
  216. // uploaderのHTML取得
  217. const uploaderHTML = await getUploaderHTML(uploader);
  218. if (!uploaderHTML) {
  219. setError(`${uploader.name}のHTML取得に失敗しました`);
  220. return;
  221. }
  222. // uploaderのDOM取得
  223. const uploaderDocument = htmlParser(uploaderHTML);
  224. if (!uploaderDocument) {
  225. setError(`${uploader.name}のDOM取得に失敗しました`);
  226. return;
  227. }
  228. const files = uploaderDocument.querySelector('.files tbody');
  229. let uploadFileName = '';
  230. for (const el of [...(files?.children || [])]) {
  231. const comment = (el.querySelector('.fco')?.textContent || '').replace(/[\s\n\t]+/g, '');
  232. if (comment === sha1) {
  233. uploadFileName = el.querySelector('.fnm a')?.textContent || '';
  234. break;
  235. }
  236. }
  237. if (!uploadFileName) {
  238. setError(`${uploader.name}にアップロードしたファイルが見つかりませんでした`);
  239. return;
  240. }
  241. const textareaElm = document.querySelector('#ftxa');
  242. if (textareaElm) {
  243. textareaElm.value = textareaElm.value.length ? `${textareaElm.value}\n${uploadFileName}` : uploadFileName;
  244. up2inputElm.value = '';
  245. hideErrorText();
  246. setInputState.enabled(up2inputElm, up2submitElm);
  247. // ふたクロでプレビュー表示していた場合削除
  248. const previewElm = document.querySelector('#upfile_preview_wrap');
  249. if (previewElm) {
  250. previewElm.innerHTML = '';
  251. }
  252. }
  253. };
  254. if (inputAreaElm) {
  255. inputAreaElm.insertAdjacentHTML('beforeend', html);
  256. const up2inputElm = document.querySelector('#up2input');
  257. const up2submitElm = document.querySelector('#up2submit');
  258. if (up2inputElm instanceof HTMLInputElement && up2submitElm instanceof HTMLButtonElement) {
  259. up2submitElm.addEventListener('click', () => {
  260. void uploadHandler(up2inputElm, up2submitElm);
  261. });
  262. up2inputElm.addEventListener('change', (event) => {
  263. const files = event.target.files;
  264. if (files && files.length) {
  265. const uploader = selectedUploader(files[0].size);
  266. replaceHtml(uploader);
  267. }
  268. });
  269. }
  270. }
  271. };
  272. const initialize = () => {
  273. const styleElm = document.querySelector('#userjs-add-uploader');
  274. if (styleElm === null) {
  275. document.head.insertAdjacentHTML('beforeend', addStyle);
  276. addUploader();
  277. }
  278. };
  279. initialize();
  280. window.addEventListener('load', initialize);
  281. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址