您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Automatically download CSV statements from Wise
// ==UserScript== // @name Wise CSV Statement Downloader // @namespace Violentmonkey Scripts // @license MIT // @version 1.1 // @description Automatically download CSV statements from Wise // @match https://*.wise.com/* // @grant GM_download // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // ==/UserScript== (function () { 'use strict'; // src/common.ts async function getElement(selector, POLL_INTERVAL) { return new Promise((resolve) => { const check = setInterval(() => { let el; if (typeof selector === "string") { el = document.querySelector(selector); } else { el = Array.from(selector).map((s) => document.querySelector(s)).find(Boolean); } if (el) { clearInterval(check); resolve(el); } }, POLL_INTERVAL); }); } async function getWindowProperty(property, POLL_INTERVAL) { return new Promise((resolve) => { const check = setInterval(() => { const el = property(window.unsafeWindow); if (el) { clearInterval(check); resolve(el); } }, POLL_INTERVAL); }); } function formatDateYYYYMMDD(date) { return `${date.getFullYear()}${(date.getMonth() + 1).toString().padStart(2, "0")}${date.getDate().toString().padStart(2, "0")}`; } function getDateRange(today, endOnBusiness = false) { let date = new Date(today); if (endOnBusiness) { if (date.getDay() === 0) { date.setDate(date.getDate() - 2); } else if (date.getDay() === 6) { date.setDate(date.getDate() - 1); } else { date.setDate(date.getDate() - 1); if (date.getDay() === 0) { date.setDate(date.getDate() - 2); } else if (date.getDay() === 6) { date.setDate(date.getDate() - 1); } } } let start; if (date.getMonth() >= 2) { start = new Date(date.getFullYear(), 0, 1); } else { start = new Date(date.getFullYear() - 1, 6, 1); } return [start, date]; } function GM_xmlhttpRequest_promise(pack) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ ...pack, onload: resolve, onerror: reject }); }); } async function easyRequest({ url, method, payload, headers }) { function helper() { if (method === "GET") { const U = new URL(url); payload?.forEach((value, key) => U.searchParams.append(decodeURIComponent(key), decodeURIComponent(value))); return GM_xmlhttpRequest_promise({ method: "GET", url: U.toString(), headers }); } else if (method === "POST.url") { return GM_xmlhttpRequest_promise({ method: "POST", url, data: payload?.toString(), headers: { "Content-Type": "application/x-www-form-urlencoded", ...headers } }); } else if (method === "POST.form") { const formData = new FormData; payload?.forEach((value, key) => formData.append(decodeURIComponent(key), decodeURIComponent(value))); return GM_xmlhttpRequest_promise({ method: "POST", url, data: formData, headers }); } else if (method === "POST.json") { const _headers = new Map(Object.entries(headers || {})); if (!(_headers.get("Content-Type") || _headers.has("content-type"))) { _headers.set("Content-Type", "application/json"); } return GM_xmlhttpRequest_promise({ method: "POST", url, data: JSON.stringify(payload), headers: Object.fromEntries(_headers) }); } throw new Error(`Unsupported method: ${method}`); } const { status, responseText } = await helper(); if (status !== 200) { throw new Error(`Request failed: ${status} ${responseText}`); } return responseText; } async function easyDownload({ content, name, saveAs = true }) { let type; if (name.endsWith(".qfx")) type = "application/x-qfx"; else if (name.endsWith(".csv")) type = "text/csv"; else type = "application/octet-stream"; await GM_download_promise({ url: URL.createObjectURL(new Blob([content.trim()], { type })), name, saveAs }); } function GM_download_promise(option) { const { url, name, saveAs } = option; return new Promise((resolve, reject) => { GM_download({ url, name, saveAs, onload: resolve, onerror: reject }); }); } function easySetValue(key, value) { GM_setValue(key, value); } function easyGetValue(key) { return GM_getValue(key); } function trimAccountName(name) { return name.trim().replace(/[\s-.]+/g, "").replace(/[\u2122\u00AE\u00A9]/g, ""); } // src/wise/lib.ts var BANK_ID = "wise"; var LOGGER_prefix = `[${BANK_ID} Downloader]`; var POLL_INTERVAL = 500; async function hookOneTimeToken() { const unsafeWindow = await getWindowProperty((w) => w, POLL_INTERVAL); const origFetch = unsafeWindow.fetch; unsafeWindow.fetch = async (...args) => { const response = await origFetch(...args); const cloned = response.clone(); cloned.json().then((text) => { if (typeof args[0] === "string" && args[0].startsWith("/gateway/identity/api/v1/one-time-token/status")) { easySetValue("wise_one_time_token", text.oneTimeTokenProperties.oneTimeToken); console.log(`${LOGGER_prefix} Hooked one-time-token: ${text.oneTimeTokenProperties.oneTimeToken}`); const accessToken = args[1].headers["X-Access-Token"]; if (accessToken) { easySetValue("wise_xaccess_token", accessToken); console.log(`${LOGGER_prefix} Hooked x-access-token: ${accessToken}`); } } }); return response; }; } async function addDownloadButton() { const container = await getElement(".header-container", POLL_INTERVAL); const CLASS = "my-download-btn"; if (container.querySelector(`.${CLASS}`)) return; const passwordInput = document.createElement("input"); passwordInput.id = "random-input-password"; passwordInput.type = "password"; passwordInput.placeholder = "input password to"; passwordInput.style.cssText = ` padding: 4px 4px; margin: 0px 4px 0px 4px; `; container.appendChild(passwordInput); const btn = document.createElement("button"); btn.textContent = "Download CSV"; btn.className = CLASS; btn.style.cssText = ` padding: 4px 4px; margin: 0px 4px 0px 4px; background-color: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; width: fit-content; `; btn.disabled = true; btn.addEventListener("click", async () => { try { await fireDownloadProcess(); } catch (err) { console.error(`[${BANK_ID} Downloader] Error:`, err); } }); passwordInput.addEventListener("input", () => { btn.disabled = !passwordInput.value; }); container.appendChild(btn); } async function verify(password) { const token = easyGetValue("wise_one_time_token"); await easyRequest({ url: "https://wise.com/gateway/identity/api/v1/one-time-token/status", method: "GET", headers: { "one-time-token": token } }); await easyRequest({ url: "https://wise.com/gateway/identity/api/v1/one-time-token/password/verify", method: "POST.json", payload: { password }, headers: { "one-time-token": token } }); return token; } async function fireDownloadProcess() { const password = (await getElement("#random-input-password", POLL_INTERVAL)).value; console.log(`Password: ${password}`); const account_id = await getWindowProperty((w) => w?.__NEXT_DATA__?.props?.templateInitialProps?.selectedProfile?.id, POLL_INTERVAL); const account_type = await getWindowProperty((w) => w?.__NEXT_DATA__?.props?.templateInitialProps?.selectedProfile?.type, POLL_INTERVAL); console.log(`Account ID: ${account_id}, Account Type: ${account_type}`); const token = await verify(password); const [content, endDate] = await routine(account_id, token); await easyDownload({ content, name: `${BANK_ID}_${trimAccountName(account_type)}_${account_id}_${endDate}_YTD.csv`, saveAs: true }); } async function routine(account_id, token) { const [startDate, endDate] = getDateRange(new Date); const eStr = formatDateYYYYMMDD(endDate); const payload = { size: "10000", since: new Date(startDate).toISOString(), until: new Date(endDate).toISOString() }; const response = await easyRequest({ url: `https://wise.com/gateway/v1/profiles/${account_id}/activities/list/export/`, method: "POST.json", payload, headers: { "x-2fa-approval": token, "x-access-token": easyGetValue("wise_xaccess_token"), "x-visual-context": "personal::light", Referer: "https://wise.com/all-transactions" } }); return [response, eStr]; } // src/wise/index.ts try { hookOneTimeToken(); addDownloadButton(); const observer = new MutationObserver(addDownloadButton); observer.observe(document.body, { childList: true, subtree: true }); } catch (err) { console.error("[Wise Downloader] Error:", err); } })();