PoExport

Export CN POE data.

  1. // ==UserScript==
  2. // @name PoExport
  3. // @namespace https://github.com/cn-poe-community/cn-poe-export-monkey
  4. // @version 0.1.8
  5. // @description Export CN POE data.
  6. // @author me1ting
  7. // @match https://poe.game.qq.com/my-account
  8. // @match https://poe.game.qq.com/account/view-profile/*
  9. // @match https://poe.game.qq.com/forum
  10. // @icon https://poecdn.game.qq.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvQ3VycmVuY3kvU2NvdXRpbmdSZXBvcnQiLCJ3IjoxLCJoIjoxLCJzY2FsZSI6MX1d/584635f3c8/ScoutingReport.png
  11. // @require https://unpkg.com/cn-poe-translator@0.4.1/dist/translator.global.js
  12. // @require https://unpkg.com/cn-poe-export-db@0.4.0/dist/db.global.js
  13. // @require https://unpkg.com/pob-building-creator@0.3.0/dist/creator.global.js
  14. // @require https://unpkg.com/pako@2.1.0/dist/pako_deflate.min.js
  15. // @require https://unpkg.com/axios@1.6.3/dist/axios.min.js
  16. // @require https://unpkg.com/vue@3.3.2/dist/vue.global.prod.js
  17. // @grant none
  18. // @license MIT
  19. // ==/UserScript==
  20.  
  21. (function(){
  22. const { defineComponent, ref, reactive, computed, onMounted, openBlock, createElementBlock, Fragment, createElementVNode, withDirectives, vModelText, renderList, toDisplayString, vModelSelect, createCommentVNode, pushScopeId, popScopeId, createBlock, createApp } = Vue;
  23.  
  24. (function() {
  25. "use strict";
  26. try {
  27. if (typeof document != "undefined") {
  28. var elementStyle = document.createElement("style");
  29. elementStyle.appendChild(document.createTextNode("#exportContainer {\n position: fixed;\n bottom: 20px;\n left: 10px;\n z-index: 99999;\n}\n\n.line-container[data-v-f270e9a2] {\n display: flex;\n margin: 3px 0;\n min-height: 25px;\n}\n.line-container select[data-v-f270e9a2] {\n min-height: 25px;\n margin-right: 4px;\n min-width: 100px;\n}\n.line-container input[data-v-f270e9a2] {\n margin-right: 4px;\n}"));
  30. document.head.appendChild(elementStyle);
  31. }
  32. } catch (e) {
  33. console.error("vite-plugin-css-injected-by-js", e);
  34. }
  35. })();
  36. (function polyfill() {
  37. const relList = document.createElement("link").relList;
  38. if (relList && relList.supports && relList.supports("modulepreload")) {
  39. return;
  40. }
  41. for (const link of document.querySelectorAll('link[rel="modulepreload"]')) {
  42. processPreload(link);
  43. }
  44. new MutationObserver((mutations) => {
  45. for (const mutation of mutations) {
  46. if (mutation.type !== "childList") {
  47. continue;
  48. }
  49. for (const node of mutation.addedNodes) {
  50. if (node.tagName === "LINK" && node.rel === "modulepreload")
  51. processPreload(node);
  52. }
  53. }
  54. }).observe(document, { childList: true, subtree: true });
  55. function getFetchOpts(link) {
  56. const fetchOpts = {};
  57. if (link.integrity)
  58. fetchOpts.integrity = link.integrity;
  59. if (link.referrerPolicy)
  60. fetchOpts.referrerPolicy = link.referrerPolicy;
  61. if (link.crossOrigin === "use-credentials")
  62. fetchOpts.credentials = "include";
  63. else if (link.crossOrigin === "anonymous")
  64. fetchOpts.credentials = "omit";
  65. else
  66. fetchOpts.credentials = "same-origin";
  67. return fetchOpts;
  68. }
  69. function processPreload(link) {
  70. if (link.ep)
  71. return;
  72. link.ep = true;
  73. const fetchOpts = getFetchOpts(link);
  74. fetch(link.href, fetchOpts);
  75. }
  76. })();
  77. const PROFILE_URL = "api/profile";
  78. const GET_CHARACTERS_URL = "/character-window/get-characters";
  79. const GET_ITEMS_URL = "/character-window/get-items";
  80. const GET_PASSIVE_SKILLS_URL = "/character-window/get-passive-skills";
  81. async function profile() {
  82. try {
  83. const { data } = await axios.get(PROFILE_URL);
  84. return data;
  85. } catch (e) {
  86. throw requestError(e);
  87. }
  88. }
  89. async function getCharacters(accountName, realm2) {
  90. const form = new URLSearchParams();
  91. form.append("accountName", accountName);
  92. form.append("realm", realm2);
  93. try {
  94. const { data } = await axios.post(GET_CHARACTERS_URL, form);
  95. return data;
  96. } catch (e) {
  97. throw requestError(e);
  98. }
  99. }
  100. async function getItems(accountName, character, realm2) {
  101. const form = new URLSearchParams();
  102. form.append("accountName", accountName);
  103. form.append("character", character);
  104. form.append("realm", realm2);
  105. try {
  106. const { data } = await axios.post(GET_ITEMS_URL, form);
  107. return data;
  108. } catch (e) {
  109. throw requestError(e);
  110. }
  111. }
  112. async function getPassiveSkills(accountName, character, realm2) {
  113. const form = new URLSearchParams();
  114. form.append("accountName", accountName);
  115. form.append("character", character);
  116. form.append("realm", realm2);
  117. try {
  118. const { data } = await axios.post(GET_PASSIVE_SKILLS_URL, form);
  119. return data;
  120. } catch (e) {
  121. throw requestError(e);
  122. }
  123. }
  124. function requestError(err) {
  125. if (err instanceof axios.AxiosError) {
  126. if (err.response) {
  127. const status = err.response.status;
  128. if (status === 401) {
  129. const rtnErr = new Error("未登陆");
  130. rtnErr.stack = String(err);
  131. return rtnErr;
  132. } else if (status === 403) {
  133. const rtnErr = new Error("账户不存在或已隐藏");
  134. rtnErr.stack = String(err);
  135. return rtnErr;
  136. } else if (status === 429) {
  137. const headers = err.response.headers;
  138. if (headers) {
  139. const limit = rateLimit(headers);
  140. if (limit.length > 0) {
  141. const rtnErr2 = new Error(`请求过于频繁,请等待 ${limit} 后再试`);
  142. rtnErr2.stack = String(err);
  143. return rtnErr2;
  144. }
  145. }
  146. const rtnErr = new Error("请求过于频繁,请稍后再试");
  147. rtnErr.stack = String(err);
  148. return rtnErr;
  149. }
  150. }
  151. } else if (err instanceof Error) {
  152. return err;
  153. }
  154. return new Error(String(err));
  155. }
  156. function rateLimit(headers) {
  157. let max = 0;
  158. for (const [key, value] of Object.entries(headers)) {
  159. if (/^x-rate-limit-.+-state$/.test(key)) {
  160. const states = value.split(",");
  161. const limits = states.map((s) => {
  162. const pieces = s.split(":");
  163. return Number(pieces[pieces.length - 1]);
  164. });
  165. for (let i = 0; i < limits.length; i++) {
  166. if (limits[i] > max) {
  167. max = limits[i];
  168. }
  169. }
  170. }
  171. }
  172. if (max > 3600) {
  173. const h = Math.floor(max / 3600);
  174. const m = Math.floor(max % 3600 / 60);
  175. const s = max % 60;
  176. return `${h}小时${m}分钟${s}秒`;
  177. }
  178. if (max > 60) {
  179. const m = Math.floor(max % 3600 / 60);
  180. const s = max % 60;
  181. return `${m}分钟${s}秒`;
  182. }
  183. if (max > 0) {
  184. return `${max}秒`;
  185. }
  186. return "";
  187. }
  188. const poeapi = {
  189. profile,
  190. getCharacters,
  191. getItems,
  192. getPassiveSkills
  193. };
  194. const _withScopeId = (n) => (pushScopeId("data-v-f270e9a2"), n = n(), popScopeId(), n);
  195. const _hoisted_1 = { class: "line-container" };
  196. const _hoisted_2 = ["disabled"];
  197. const _hoisted_3 = { class: "line-container" };
  198. const _hoisted_4 = { key: 0 };
  199. const _hoisted_5 = ["value"];
  200. const _hoisted_6 = ["value"];
  201. const _hoisted_7 = ["disabled"];
  202. const _hoisted_8 = { key: 1 };
  203. const _hoisted_9 = /* @__PURE__ */ _withScopeId(() => /* @__PURE__ */ createElementVNode("select", { disabled: "" }, null, -1));
  204. const _hoisted_10 = /* @__PURE__ */ _withScopeId(() => /* @__PURE__ */ createElementVNode("select", { disabled: "" }, null, -1));
  205. const _hoisted_11 = /* @__PURE__ */ _withScopeId(() => /* @__PURE__ */ createElementVNode("button", { disabled: "" }, "导出", -1));
  206. const _hoisted_12 = [
  207. _hoisted_9,
  208. _hoisted_10,
  209. _hoisted_11
  210. ];
  211. const _hoisted_13 = { class: "line-container" };
  212. const _hoisted_14 = ["value"];
  213. const _hoisted_15 = ["disabled"];
  214. const realm = "pc";
  215. const _sfc_main$1 = /* @__PURE__ */ defineComponent({
  216. __name: "Exporter",
  217. props: ["createBuilding", "startup"],
  218. setup(__props) {
  219. const props = __props;
  220. const accountName = ref("");
  221. const characters = ref([]);
  222. const leagues = ref([]);
  223. const leagueMap = ref(/* @__PURE__ */ new Map());
  224. const currLeague = ref("");
  225. const currCharacters = ref([]);
  226. const currCharacter = ref("");
  227. const buildingCode = ref("");
  228. const state = reactive({
  229. accountName,
  230. realm,
  231. characters,
  232. leagues,
  233. leagueMap,
  234. currLeague,
  235. currCharacters,
  236. currCharacter,
  237. buildingCode
  238. });
  239. const getCharactersReady = computed(() => {
  240. return state.accountName.length > 0;
  241. });
  242. const selectReady = computed(() => {
  243. return state.characters.length > 0;
  244. });
  245. const exportReady = computed(() => {
  246. return state.characters.length > 0 && Boolean(state.currCharacter);
  247. });
  248. function handleLeageSelect() {
  249. state.currCharacters = state.leagueMap.get(state.currLeague);
  250. state.currCharacter = state.currCharacters[0].name;
  251. }
  252. async function handleCharactersQuery() {
  253. state.characters = [];
  254. state.leagues = [];
  255. const realm2 = state.realm;
  256. const accountName2 = state.accountName;
  257. var data = null;
  258. try {
  259. data = await poeapi.getCharacters(accountName2, realm2);
  260. } catch (e) {
  261. if (e instanceof Error) {
  262. alert(e.message);
  263. } else {
  264. alert(e);
  265. }
  266. return;
  267. }
  268. const characters2 = data;
  269. state.characters = characters2;
  270. let leagueMap2 = /* @__PURE__ */ new Map();
  271. for (const character of characters2) {
  272. const leagueName = character.league;
  273. let list = leagueMap2.get(leagueName);
  274. if (list === void 0) {
  275. list = [];
  276. leagueMap2.set(leagueName, list);
  277. }
  278. list.push(character);
  279. }
  280. state.leagueMap = leagueMap2;
  281. const leagues2 = Array.from(leagueMap2.keys());
  282. state.leagues = leagues2;
  283. if (leagues2.length > 0) {
  284. state.currLeague = leagues2[0];
  285. handleLeageSelect();
  286. }
  287. }
  288. async function handleExport() {
  289. state.buildingCode = "";
  290. const accountName2 = state.accountName;
  291. const character = state.currCharacter;
  292. const realm2 = state.realm;
  293. let items = null;
  294. let passiveSkills = null;
  295. try {
  296. items = await poeapi.getItems(accountName2, character, realm2);
  297. passiveSkills = await poeapi.getPassiveSkills(accountName2, character, realm2);
  298. state.buildingCode = await props.createBuilding(items, passiveSkills);
  299. } catch (e) {
  300. if (e instanceof Error) {
  301. alert(e.message);
  302. } else {
  303. alert(e);
  304. }
  305. return;
  306. }
  307. }
  308. function handleCopy() {
  309. navigator.clipboard.writeText(state.buildingCode);
  310. }
  311. function getInitialAccountName() {
  312. let accountName2 = getAccountNameFromProfileLink(window.location.href);
  313. if (accountName2 !== null) {
  314. return accountName2;
  315. }
  316. const profileLinkNode = document.querySelector("#statusBar .profile-link a");
  317. if (profileLinkNode !== null) {
  318. accountName2 = getAccountNameFromProfileLink(profileLinkNode.href);
  319. if (accountName2 !== null) {
  320. return accountName2;
  321. }
  322. }
  323. return "";
  324. }
  325. const pattern = new RegExp("/account/view-profile/([^/?]+)");
  326. function getAccountNameFromProfileLink(link) {
  327. const match = pattern.exec(link);
  328. if (match) {
  329. return decodeURI(match[1]);
  330. }
  331. return null;
  332. }
  333. onMounted(() => {
  334. state.accountName = getInitialAccountName();
  335. if (props.startup) {
  336. props.startup();
  337. }
  338. });
  339. return (_ctx, _cache) => {
  340. return openBlock(), createElementBlock(Fragment, null, [
  341. createElementVNode("span", _hoisted_1, [
  342. withDirectives(createElementVNode("input", {
  343. type: "text",
  344. placeholder: "输入论坛账户名",
  345. maxlength: "50",
  346. "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => state.accountName = $event)
  347. }, null, 512), [
  348. [
  349. vModelText,
  350. state.accountName,
  351. void 0,
  352. { trim: true }
  353. ]
  354. ]),
  355. createElementVNode("button", {
  356. onClick: handleCharactersQuery,
  357. disabled: !getCharactersReady.value
  358. }, "开始", 8, _hoisted_2)
  359. ]),
  360. createElementVNode("span", _hoisted_3, [
  361. selectReady.value ? (openBlock(), createElementBlock("div", _hoisted_4, [
  362. selectReady.value ? withDirectives((openBlock(), createElementBlock("select", {
  363. key: 0,
  364. "onUpdate:modelValue": _cache[1] || (_cache[1] = ($event) => state.currLeague = $event),
  365. onChange: handleLeageSelect
  366. }, [
  367. (openBlock(true), createElementBlock(Fragment, null, renderList(leagues.value, (item) => {
  368. return openBlock(), createElementBlock("option", {
  369. key: item,
  370. value: item
  371. }, toDisplayString(item), 9, _hoisted_5);
  372. }), 128))
  373. ], 544)), [
  374. [vModelSelect, state.currLeague]
  375. ]) : createCommentVNode("", true),
  376. selectReady.value ? withDirectives((openBlock(), createElementBlock("select", {
  377. key: 1,
  378. "onUpdate:modelValue": _cache[2] || (_cache[2] = ($event) => state.currCharacter = $event)
  379. }, [
  380. (openBlock(true), createElementBlock(Fragment, null, renderList(state.currCharacters, (item) => {
  381. return openBlock(), createElementBlock("option", {
  382. key: item.name,
  383. value: item.name
  384. }, toDisplayString(item.name) + "," + toDisplayString(item.level) + "," + toDisplayString(item.class), 9, _hoisted_6);
  385. }), 128))
  386. ], 512)), [
  387. [vModelSelect, state.currCharacter]
  388. ]) : createCommentVNode("", true),
  389. selectReady.value ? (openBlock(), createElementBlock("button", {
  390. key: 2,
  391. disabled: !exportReady.value,
  392. onClick: handleExport
  393. }, "导出", 8, _hoisted_7)) : createCommentVNode("", true)
  394. ])) : (openBlock(), createElementBlock("div", _hoisted_8, _hoisted_12))
  395. ]),
  396. createElementVNode("span", _hoisted_13, [
  397. createElementVNode("input", {
  398. disabled: "",
  399. maxlength: "50",
  400. value: state.buildingCode
  401. }, null, 8, _hoisted_14),
  402. createElementVNode("button", {
  403. onClick: handleCopy,
  404. disabled: state.buildingCode.length === 0
  405. }, "复制", 8, _hoisted_15)
  406. ])
  407. ], 64);
  408. };
  409. }
  410. });
  411. const _export_sfc = (sfc, props) => {
  412. const target = sfc.__vccOpts || sfc;
  413. for (const [key, val] of props) {
  414. target[key] = val;
  415. }
  416. return target;
  417. };
  418. const Exporter = /* @__PURE__ */ _export_sfc(_sfc_main$1, [["__scopeId", "data-v-f270e9a2"]]);
  419. const _sfc_main = /* @__PURE__ */ defineComponent({
  420. __name: "Monkey",
  421. setup(__props) {
  422. const factory = new CnPoeTranslator.BasicTranslatorFactory(CnPoeExportDb);
  423. const jsonTranslator = factory.getJsonTranslator();
  424. async function createBuilding(items, passiveSkills) {
  425. jsonTranslator.translateItems(items);
  426. jsonTranslator.translatePassiveSkills(passiveSkills);
  427. const building = BuildingCreator.transform(items, passiveSkills);
  428. const compressed = window.pako.deflate(building.toString());
  429. const code = btoa(String.fromCharCode(...compressed)).replaceAll("+", "-").replaceAll("/", "_");
  430. return code;
  431. }
  432. return (_ctx, _cache) => {
  433. return openBlock(), createBlock(Exporter, { "create-building": createBuilding });
  434. };
  435. }
  436. });
  437. const container = document.createElement("div");
  438. container.id = "exportContainer";
  439. document.body.appendChild(container);
  440. createApp(_sfc_main).mount("#exportContainer");
  441.  
  442. })();