Monkey Requests

Useful library for sending requests.

当前为 2020-06-21 提交的版本,查看 最新版本

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

  1. // ==UserScript==
  2. // @name Monkey Requests
  3. // @namespace https://rafaelgssa.gitlab.io/monkey-scripts
  4. // @author rafaelgssa
  5. // @version 1.0.0
  6. // @description Useful library for sending requests.
  7. // @require https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js
  8. // @require https://greasyfork.org/scripts/405802-monkey-dom/code/Monkey%20DOM.js?version=818854
  9. // @require https://greasyfork.org/scripts/405813-monkey-utils/code/Monkey%20Utils.js?version=818827
  10. // @grant GM.xmlHttpRequest
  11. // @grant GM_xmlhttpRequest
  12. // ==/UserScript==
  13.  
  14. /* global MonkeyDom, MonkeyUtils */
  15.  
  16. /**
  17. * @typedef {'CONNECT' | 'DELETE' | 'GET' | 'HEAD' | 'OPTIONS' | 'PATCH' | 'POST' | 'PUT' | 'TRACE'} MonkeyMethod
  18. * @typedef {Object} MonkeyResponse
  19. * @property {string} url
  20. * @property {string} text
  21. * @property {Object} [json]
  22. * @property {Document} [dom]
  23. * @typedef {{ [K in MonkeyMethod]: (url: string, options?: RequestInit | GM.Request) => Promise<MonkeyResponse> }} ShorthandSend
  24. */
  25.  
  26. // eslint-disable-next-line
  27. const MonkeyRequests = (() => {
  28. /** @type {MonkeyMethod[]} */
  29. const methods = ['CONNECT', 'DELETE', 'GET', 'HEAD', 'OPTIONS', 'PATCH', 'POST', 'PUT', 'TRACE'];
  30.  
  31. /**
  32. * @param {MonkeyMethod} method
  33. * @param {string} url
  34. * @param {RequestInit | GM.Request} [options]
  35. * @returns {Promise<MonkeyResponse>}
  36. */
  37. const _sendWithMethod = (method, url, options = {}) => {
  38. const optionsWithMethod = { ...options, method };
  39. return send(url, optionsWithMethod);
  40. };
  41.  
  42. /**
  43. * @param {string} url
  44. * @param {RequestInit | GM.Request} [options]
  45. * @returns {Promise<MonkeyResponse>}
  46. */
  47. const send = (url, options = {}) => {
  48. if (_isInternal(url, options)) {
  49. return _sendInternal(url, options);
  50. }
  51. return _sendExternal(url, options);
  52. };
  53.  
  54. /**
  55. * Checks if the request is internal (uses window.fetch) or external (uses GM.xmlHttpRequest to bypass CORS).
  56. * @param {string} url
  57. * @param {RequestInit | GM.Request} options
  58. * @returns {options is RequestInit}
  59. */
  60. // eslint-disable-next-line
  61. const _isInternal = (url, options) => {
  62. return url.includes(window.location.host);
  63. };
  64.  
  65. /**
  66. * @param {string} url
  67. * @param {RequestInit} options
  68. * @returns {Promise<MonkeyResponse>}
  69. */
  70. const _sendInternal = async (url, options) => {
  71. const [internalFetch, internalOptions] = _getInternalObjects(options);
  72. const fetchResponse = await internalFetch(url, internalOptions);
  73. /** @type {MonkeyResponse} */
  74. const response = {
  75. url: fetchResponse.url,
  76. text: await fetchResponse.text(),
  77. };
  78. return _processResponse(response);
  79. };
  80.  
  81. /**
  82. * Allows the request to be made in the current Firefox container.
  83. * @param {RequestInit} options
  84. * @returns {[Function, RequestInit]}
  85. */
  86. const _getInternalObjects = (options) => {
  87. if ('wrappedJSObject' in window) {
  88. window.wrappedJSObject.options = cloneInto(options, window);
  89. return [window.wrappedJSObject.fetch, window.wrappedJSObject.options];
  90. }
  91. return [window.fetch, options];
  92. };
  93.  
  94. /**
  95. * @param {string} url
  96. * @param {Partial<GM.Request>} options
  97. * @returns {Promise<MonkeyResponse>}
  98. */
  99. const _sendExternal = (url, options) => {
  100. return new Promise((resolve) => {
  101. GM.xmlHttpRequest({
  102. url,
  103. method: 'GET',
  104. ...options,
  105. onload: (gmResponse) => {
  106. /** @type {MonkeyResponse} */
  107. const response = {
  108. url: gmResponse.finalUrl,
  109. text: gmResponse.responseText,
  110. };
  111. resolve(_processResponse(response));
  112. },
  113. });
  114. });
  115. };
  116.  
  117. /**
  118. * @param {MonkeyResponse} response
  119. * @returns {MonkeyResponse}
  120. */
  121. const _processResponse = (response) => {
  122. const processedResponse = { ...response };
  123. try {
  124. processedResponse.dom = MonkeyDom.parse(response.text);
  125. } catch (err) {
  126. // Response is not a DOM, just ignore.
  127. }
  128. try {
  129. processedResponse.json = JSON.parse(response.text);
  130. } catch (err) {
  131. // Response is not a JSON, just ignore.
  132. }
  133. return processedResponse;
  134. };
  135.  
  136. /**
  137. * Parses a query string into an object.
  138. * @param {string} query The query to parse.
  139. * @returns {Record<string, string>} The parsed object.
  140. */
  141. const parseQuery = (query) => {
  142. /** @type {Record<string, string>} */
  143. const params = {};
  144. const rawQuery = query.startsWith('?') ? query.slice(1) : query;
  145. const parts = rawQuery.split('&').filter(MonkeyUtils.isSet);
  146. for (const part of parts) {
  147. const [key, value] = part.split('=').filter(MonkeyUtils.isSet);
  148. params[key] = value;
  149. }
  150. return params;
  151. };
  152.  
  153. /** @type {ShorthandSend} */
  154. // @ts-ignore
  155. const shorthandSend = {};
  156. for (const method of methods) {
  157. shorthandSend[method] = _sendWithMethod.bind(null, method);
  158. }
  159.  
  160. return {
  161. ...shorthandSend,
  162. parseQuery,
  163. send,
  164. };
  165. })();