您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Automatically download CSV statements from Sofi
// ==UserScript== // @name Sofi CSV Statement Downloader // @namespace Violentmonkey Scripts // @license MIT // @version 1.1 // @description Automatically download CSV statements from Sofi // @match https://*.sofi.com/* // @grant GM_download // @grant GM_xmlhttpRequest // ==/UserScript== (function () { 'use strict'; // src/common.ts function formatDateYYYYdMMdDD(date) { return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, "0")}-${date.getDate().toString().padStart(2, "0")}`; } function formatDateYYYYMMDD(date) { return `${date.getFullYear()}${(date.getMonth() + 1).toString().padStart(2, "0")}${date.getDate().toString().padStart(2, "0")}`; } function getRelDateRange(today, monthPushBack, 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; start = new Date(date.getFullYear(), date.getMonth() - monthPushBack, 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 }); }); } // src/sofi/lib.ts var BANK_ID = "sofi"; var LOGGER_prefix = `[${BANK_ID} Downloader]`; async function addDownloadButton() { const CLASS = "my-download-btn"; if (document.querySelector(`.${CLASS}`)) return; const btn = document.createElement("button"); btn.textContent = "Download CSV"; btn.className = CLASS; btn.style.cssText = ` padding: 8px 8px; margin: 8px 4px 8px 4px; background-color: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; width: fit-content; `; const nav = document.querySelector("nav"); if (!nav) return; nav.insertBefore(btn, nav.lastChild); btn.addEventListener("click", async () => { try { await fireDownloadProcess(); } catch (err) { console.error(`[${BANK_ID} Downloader] Error:`, err); } }); } function getInArray(d, needs, to_be) { for (const item of d) { if (item[needs] === to_be) return item; } return null; } async function fireDownloadProcess() { const script = Array.from(document.scripts).find((s) => s.textContent?.startsWith("window.REACT_QUERY_STATE")); if (!script) throw new Error("REACT_QUERY_STATE script not found"); const json = script.textContent?.slice(script.textContent.indexOf("{"), script.textContent.lastIndexOf("}") + 1); if (!json) throw new Error("REACT_QUERY_STATE JSON not found"); const data = JSON.parse(json); const rows = getInArray(getInArray(getInArray(getInArray(data.queries, "queryHash", '["GET_HOME_ZONES"]').state.data.content, "type", "SURFACE_OVERLAY").data.zones, "type", "ZONE_GNG").data.sections, "type", "SECTION_LIST_EXPANDABLE").data.rows; const accounts = []; for (const row of rows) { for (const item of row.expanded?.data?.items || []) { const url = item.data.dynamicAction.data.url; const accountId = url.match(/\/(\d+)\/account-detail/); if (accountId) accounts.push(accountId[1]); } } for (const account_id of accounts) { console.log(`${LOGGER_prefix} Account id: ${account_id}`); const [content, endDate] = await routine(account_id); await easyDownload({ content, name: `${BANK_ID}_${account_id}_${endDate}_YTD.csv`, saveAs: true }); } } async function routine(account_id) { const [startDate, endDate] = getRelDateRange(new Date, 2); const eStr = formatDateYYYYMMDD(endDate); const payload = new URLSearchParams; payload.append("startDate", formatDateYYYYdMMdDD(startDate)); payload.append("endDate", formatDateYYYYdMMdDD(endDate)); console.log(`Payload:`, payload.toString()); const response = await easyRequest({ url: `https://www.sofi.com/money-transactions-hist-service/api/public/v1/accounts/transactions/export/${account_id}`, method: "GET", payload, headers: { Referer: "https://www.sofi.com/my/money/account/more/export-transaction-history" } }); return [response, eStr]; } // src/sofi/index.ts try { addDownloadButton(); const observer = new MutationObserver(addDownloadButton); observer.observe(document.body, { childList: true, subtree: true }); } catch (err) { console.error("[Sofi Downloader] Error:", err); } })();