Clipboard image upload patch in Bahamut forum

Automatically upload image to Bahamut when you paste images in forum editor.

  1. // ==UserScript==
  2. // @name Clipboard image upload patch in Bahamut forum
  3. // @name:zh-TW 巴哈姆特論壇區剪貼簿圖片上傳補丁
  4. // @namespace https://jtdjdu6868.com/
  5. // @version 1.2
  6. // @description Automatically upload image to Bahamut when you paste images in forum editor.
  7. // @description:zh-TW 在編輯討論區文章時貼上支援的圖片格式,自動上傳至巴哈圖庫
  8. // @author jtdjdu6868
  9. // @match https://forum.gamer.com.tw/post1.php*
  10. // @match https://forum.gamer.com.tw/C.php*
  11. // @match https://forum.gamer.com.tw/Co.php*
  12. // @icon https://www.google.com/s2/favicons?sz=64&domain=gamer.com.tw
  13. // @grant none
  14. // @run-at document-idle
  15. // @license BY
  16. // ==/UserScript==
  17.  
  18. (function() {
  19. 'use strict';
  20. function pasteHandler(e)
  21. {
  22. const editor = this;
  23. const items = (e.clipboardData || e.originalEvent.clipboardData).items;
  24. for(let i = 0; i < items.length; ++i)
  25. {
  26. if("image/gif,image/jpeg,image/png,image/webp".split(",").includes(items[i].type))
  27. {
  28. e.preventDefault();
  29. console.log("paste image");
  30. toastr.info("上傳中...");
  31.  
  32. const blob = items[i].getAsFile();
  33. const bsn = +new URL(window.location.href).searchParams.get("bsn");
  34.  
  35. // get upload token (piccToken)
  36. fetch(`https://api.gamer.com.tw/forum/v1/image_token.php?bsn=${bsn}`, {
  37. credentials: "include",
  38. }).then((res) => res.json()).then((res) => {
  39. if(res.code !== void 0 && res.message || res.error)
  40. {
  41. throw res.message || res.error.message;
  42. }
  43. const piccToken = res.token || res.data.token;
  44. return piccToken;
  45. }).then((piccToken) => {
  46. // pack payload
  47. const payload = new FormData();
  48. payload.append("token", piccToken);
  49. payload.append("dzfile", blob);
  50.  
  51. return payload;
  52. }).then((payload) => {
  53.  
  54. // upload payoad with piccToken
  55. // use xhr because server returns a readablestream, xhr is more simpler
  56. return new Promise((resolve, reject) => {
  57. const xhr = new XMLHttpRequest();
  58. xhr.open("POST", "https://picc.gamer.com.tw/ajax/truth_image_upload.php", true);
  59. xhr.withCredentials = true;
  60. xhr.onload = (e) => {
  61. // return image token
  62. resolve(e.target.responseText);
  63. };
  64.  
  65. const headers = {
  66. "Accept": "application/json",
  67. "Cache-Control": "no-cache",
  68. "X-Requested-With": "XMLHttpRequest"
  69. };
  70. Object.keys(headers).forEach((key) => xhr.setRequestHeader(key, headers[key]));
  71.  
  72. xhr.send(payload);
  73. });
  74. }).then((res) => JSON.parse(res)).then((res) => {
  75. if(res.token === void 0)
  76. {
  77. throw res;
  78. }
  79.  
  80. // get image url by token
  81. return fetch(`https://api.gamer.com.tw/forum/v1/image_upload.php?token=${res.token}&bsn=${bsn}`, {
  82. credentials: "include",
  83. });
  84. }).then((res) => res.json()).then((res) => {
  85. // receive url
  86. if(res.code !== void 0 && res.message || res.error)
  87. {
  88. throw res.message || res.error.message;
  89. }
  90. let urlList = res.data ? res.data.list : res;
  91.  
  92. // extract first url
  93. return urlList[0];
  94. }).then((url) => {
  95. toastr.clear();
  96. toastr.success("上傳成功");
  97.  
  98. // insert
  99. if(editor.isContentEditable)
  100. {
  101. // event from RTF or source code
  102. if(!bahaRte.isPlainText)
  103. {
  104. // RTF
  105. bahaRte.doc.execCommand("insertImage", false, url);
  106. Forum.Editor.detectThumbnail();
  107. }
  108. }
  109. else if(editor.id === "source")
  110. {
  111. // source code in post1.php editor
  112. if(bahaRte.isPlainText)
  113. {
  114. document.execCommand("insertText", false, `[img=${url}]`);
  115. Forum.Editor.detectThumbnail();
  116. }
  117. }
  118. else if(editor.tagName === "TEXTAREA" || editor.tagName === "INPUT" && editor.type === "text")
  119. {
  120. // event from comment or some unknown input text
  121.  
  122. // insertText supports native undo, but baha offcial insert uses set content
  123. // document.execCommand("insertText", false, url);
  124. const selStart = editor.selectionStart,
  125. selEnd = editor.selectionEnd;
  126. const before = editor.value.substring(0, selStart),
  127. after = editor.value.substring(selEnd);
  128. editor.value = before + url + after;
  129. editor.focus();
  130. editor.selectionStart = selStart;
  131. editor.selectionEnd = selStart + url.length;
  132. }
  133. else
  134. {
  135. console.log("editor unknown");
  136. }
  137. }).catch((err) => {
  138. toastr.clear();
  139. toastr.error(err);
  140. });
  141. }
  142. else if(items[i].type.indexOf('image') === 0)
  143. {
  144. // unsupported image type
  145. toastr.warning("目前巴哈圖庫只支援png, jpg, gif, webp");
  146. e.preventDefault();
  147. }
  148. }
  149. };
  150.  
  151. // bind event after bahaRte loaded
  152. jQuery(window).on('load', () => {
  153. bahaRte.doc.body.addEventListener("paste", pasteHandler);
  154. document.getElementById("source")?.addEventListener?.("paste", pasteHandler);
  155. document.querySelectorAll(".reply-input textarea").forEach((canEdit) => canEdit.addEventListener("paste", pasteHandler));
  156. });
  157. })();