Monkey Requests

Useful library for sending requests.

此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.cn-greasyfork.org/scripts/405822/821708/Monkey%20Requests.js

  1. // ==UserScript==
  2. // @name Requests
  3. // @namespace https://rafaelgssa.gitlab.io/monkey-scripts
  4. // @version 2.0.3
  5. // @author rafaelgssa
  6. // @description Useful library for sending requests.
  7. // @match *://*/*
  8. // @require https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js
  9. // @require https://greasyfork.org/scripts/405813-monkey-utils/code/Monkey%20Utils.js
  10. // @require https://greasyfork.org/scripts/405802-monkey-dom/code/Monkey%20DOM.js
  11. // @grant GM.xmlHttpRequest
  12. // @grant GM_xmlhttpRequest
  13. // ==/UserScript==
  14.  
  15. /* global DOM, Utils */
  16.  
  17. /**
  18. * @typedef {'CONNECT' | 'DELETE' | 'GET' | 'HEAD' | 'OPTIONS' | 'PATCH' | 'POST' | 'PUT' | 'TRACE'} RequestMethod
  19. */
  20.  
  21. /**
  22. * @template T
  23. * @typedef {Object} ExtendedResponse
  24. * @property {string} url
  25. * @property {string} text
  26. * @property {T} [json]
  27. * @property {Document} [dom]
  28. */
  29.  
  30. // eslint-disable-next-line
  31. const Requests = (() => {
  32. /**
  33. * Sends a request to a URL with a specific method.
  34. * @template T
  35. * @param {RequestMethod} method The method to use.
  36. * @param {string} url The URL where to send the request.
  37. * @param {RequestInit | GM.Request} [options] The options for the request.
  38. * @returns {Promise<ExtendedResponse<T>>} The response of the request.
  39. */
  40. const _sendWithMethod = (method, url, options = {}) => {
  41. return send(url, { ...options, method });
  42. };
  43.  
  44. /**
  45. * Sends a request to a URL.
  46. * @template T
  47. * @param {string} url The URL where to send the request.
  48. * @param {RequestInit | GM.Request} [options] The options for the request.
  49. * @returns {Promise<ExtendedResponse<T>>} The response of the request.
  50. */
  51. const send = (url, options = {}) => {
  52. if (_isInternal(url, options)) {
  53. return _sendInternal(url, options);
  54. }
  55. return _sendExternal(url, options);
  56. };
  57.  
  58. /**
  59. * Checks if the request is internal (uses window.fetch) or external (uses GM.xmlHttpRequest to bypass CORS).
  60. * @param {string} url
  61. * @param {RequestInit | GM.Request} options
  62. * @returns {options is RequestInit} Whether the request is internal or external.
  63. */
  64. // eslint-disable-next-line
  65. const _isInternal = (url, options) => {
  66. return url.includes(window.location.host);
  67. };
  68.  
  69. /**
  70. * Sends an internal request (uses window.fetch).
  71. * @template T
  72. * @param {string} url
  73. * @param {RequestInit} options
  74. * @returns {Promise<ExtendedResponse<T>>}
  75. */
  76. const _sendInternal = async (url, options) => {
  77. const [internalFetch, internalOptions] = _getInternalVars(options);
  78. const response = await internalFetch(url, internalOptions);
  79. const extendedResponse = /** @type {ExtendedResponse<T>} */ ({
  80. url: response.url,
  81. text: await response.text(),
  82. });
  83. return _processResponse(extendedResponse);
  84. };
  85.  
  86. /**
  87. * Returns internal variables that allow the request to be made in the current Firefox container.
  88. * @param {RequestInit} options
  89. * @returns {[Function, RequestInit]} The internal variables.
  90. */
  91. const _getInternalVars = (options) => {
  92. if (!('wrappedJSObject' in window)) {
  93. return [window.fetch, options];
  94. }
  95. window.wrappedJSObject.options = cloneInto(options, window);
  96. return [window.wrappedJSObject.fetch, window.wrappedJSObject.options];
  97. };
  98.  
  99. /**
  100. * Sends an external request (uses GM.xmlHttpRequest to bypass CORS).
  101. * @template T
  102. * @param {string} url
  103. * @param {Partial<GM.Request>} options
  104. * @returns {Promise<ExtendedResponse<T>>}
  105. */
  106. const _sendExternal = (url, options) => {
  107. return new Promise((resolve, reject) => {
  108. GM.xmlHttpRequest({
  109. url,
  110. method: 'GET',
  111. ...options,
  112. onload: (response) => {
  113. const extendedResponse = /** @type {ExtendedResponse<T>} */ ({
  114. url: response.finalUrl,
  115. text: response.responseText,
  116. });
  117. resolve(_processResponse(extendedResponse));
  118. },
  119. onerror: reject,
  120. });
  121. });
  122. };
  123.  
  124. /**
  125. * Processes a response to return DOMs and JSONs already parsed.
  126. * @template T
  127. * @param {ExtendedResponse<T>} response The response to process.
  128. * @returns {ExtendedResponse<T>} The processed response.
  129. */
  130. const _processResponse = (response) => {
  131. const processedResponse = { ...response };
  132. try {
  133. processedResponse.dom = DOM.parse(response.text);
  134. } catch (err) {
  135. // Response is not a DOM, just ignore.
  136. }
  137. try {
  138. processedResponse.json = JSON.parse(response.text);
  139. } catch (err) {
  140. // Response is not a JSON, just ignore.
  141. }
  142. return processedResponse;
  143. };
  144.  
  145. /**
  146. * Parses a query string into an object.
  147. * @param {string} query The query string to parse.
  148. * @returns {Record<string, string>} The parsed object.
  149. */
  150. const parseQuery = (query) => {
  151. const params = /** @type {Record<string, string>} */ ({});
  152. const rawQuery = query.startsWith('?') ? query.slice(1) : query;
  153. const parts = rawQuery.split('&').filter(Utils.isSet);
  154. for (const part of parts) {
  155. const [key, value] = part.split('=').filter(Utils.isSet);
  156. params[key] = value;
  157. }
  158. return params;
  159. };
  160.  
  161. /**
  162. * Converts an object to a query string.
  163. * @param {Record<string, unknown>} obj The object to convert.
  164. * @returns {string} The converted query string, without '?'.
  165. */
  166. const convertToQuery = (obj) => {
  167. return Object.entries(obj)
  168. .map((entry) => entry.join('='))
  169. .join('&');
  170. };
  171.  
  172. return {
  173. CONNECT: /** @template T */ (
  174. /** @type [string] | [string, RequestInit | GM.Request] */ ...args
  175. ) => /** @type Promise<ExtendedResponse<T>> */ (_sendWithMethod('CONNECT', args[0], args[1])),
  176. DELETE: /** @template T */ (
  177. /** @type [string] | [string, RequestInit | GM.Request] */ ...args
  178. ) => /** @type Promise<ExtendedResponse<T>> */ (_sendWithMethod('DELETE', args[0], args[1])),
  179. GET: /** @template T */ (/** @type [string] | [string, RequestInit | GM.Request] */ ...args) =>
  180. /** @type Promise<ExtendedResponse<T>> */ (_sendWithMethod('GET', args[0], args[1])),
  181. HEAD: /** @template T */ (/** @type [string] | [string, RequestInit | GM.Request] */ ...args) =>
  182. /** @type Promise<ExtendedResponse<T>> */ (_sendWithMethod('HEAD', args[0], args[1])),
  183. OPTIONS: /** @template T */ (
  184. /** @type [string] | [string, RequestInit | GM.Request] */ ...args
  185. ) => /** @type Promise<ExtendedResponse<T>> */ (_sendWithMethod('OPTIONS', args[0], args[1])),
  186. PATCH: /** @template T */ (
  187. /** @type [string] | [string, RequestInit | GM.Request] */ ...args
  188. ) => /** @type Promise<ExtendedResponse<T>> */ (_sendWithMethod('PATCH', args[0], args[1])),
  189. POST: /** @template T */ (/** @type [string] | [string, RequestInit | GM.Request] */ ...args) =>
  190. /** @type Promise<ExtendedResponse<T>> */ (_sendWithMethod('POST', args[0], args[1])),
  191. PUT: /** @template T */ (/** @type [string] | [string, RequestInit | GM.Request] */ ...args) =>
  192. /** @type Promise<ExtendedResponse<T>> */ (_sendWithMethod('PUT', args[0], args[1])),
  193. TRACE: /** @template T */ (
  194. /** @type [string] | [string, RequestInit | GM.Request] */ ...args
  195. ) => /** @type Promise<ExtendedResponse<T>> */ (_sendWithMethod('TRACE', args[0], args[1])),
  196. send,
  197. parseQuery,
  198. convertToQuery,
  199. };
  200. })();