From 2491eae95fe08f0651c96e5c173a08ade80005e0 Mon Sep 17 00:00:00 2001 From: devaine Date: Mon, 2 Feb 2026 19:17:44 -0600 Subject: [PATCH] feat(integration): integration is fully finished, will refractor later --- .gitignore | 2 + actual-api/src/index.js | 94 +++++++++++++++++++++++++++++------ package.json | 3 +- webscrape-bank/src/extract.py | 25 ++++++++-- webscrape-bank/src/main.py | 94 ++++++++++++++++++++++++----------- 5 files changed, 171 insertions(+), 47 deletions(-) diff --git a/.gitignore b/.gitignore index c3d38e3..5e15c45 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ **/My-Finances* notes **/*qfx +**/2fa +**/output # Others node_modules/ diff --git a/actual-api/src/index.js b/actual-api/src/index.js index 9a78fe0..d2d6c38 100644 --- a/actual-api/src/index.js +++ b/actual-api/src/index.js @@ -1,11 +1,13 @@ require("dotenv").config(); + +const fs = require("node:fs"); let api = require("@actual-app/api"); // Constants -const ACTUAL_URL = process.env.ACTUAL_URL -const ACTUAL_PASSWORD = process.env.ACTUAL_PASSWORD -const ACTUAL_SYNC_ID = process.env.ACTUAL_SYNC_ID -const ACTUAL_CHECKING_ID = process.env.ACTUAL_CHECKING_ID +const ACTUAL_URL = process.env.ACTUAL_URL; +const ACTUAL_PASSWORD = process.env.ACTUAL_PASSWORD; +const ACTUAL_SYNC_ID = process.env.ACTUAL_SYNC_ID; +const ACTUAL_CHECKING_ID = process.env.ACTUAL_CHECKING_ID; (async () => { await api.init({ @@ -17,16 +19,80 @@ const ACTUAL_CHECKING_ID = process.env.ACTUAL_CHECKING_ID console.log("Downloading the Budget Data") await api.downloadBudget(ACTUAL_SYNC_ID); - console.log("Getting Account Info") - let accountInfo = await api.getAccounts() - console.log(accountInfo) + while(true) { + console.log("Getting the most recent transactions") + var latestTrans = await api.getTransactions(ACTUAL_CHECKING_ID) - function inspectBankFile() { - + var actual_date = new Date(latestTrans[0].date) + var actual_amount = Number(latestTrans[0].amount) + + fs.readFile("./webscrape-bank/data/output", "utf8", async (err, data) => { + if (err) { + console.error(err); + return; + } + + var start = [] + var end = [] + + var lines = data.split("\n"); + + + // Parse to grab indexes of each transaction + for (var i = 0; i < lines.length; i++) { + if(lines[i] == "") { + start.push(i) + } else if (lines[i] == "") { + end.push(i) + } + } + + // Search through every section of output file + for (var i = 0; i < end.length; i++) { + for(var j = start[i]; j < end[i]; j++) { + var date + var type + var amount + var notes + var name + + if(lines[j].includes("")) { + type = lines[j].substring(9) + } else if(lines[j].includes("")) { + var dt_string = lines[j].substring(10).substring(0, 8) + var year = dt_string.substring(0, 4) + var month = dt_string.substring(5, 6) - 1 + var day = dt_string.substring(6, 8) + + date = new Date(year, month, day) + } else if(lines[j].includes("")) { + amount = Number(lines[j].substring(8).replace(".", "")) + } else if(lines[j].includes("")) { + name = lines[j].substring(6) + if(lines[j+1].includes("")) { + notes = lines[j+1].substring(6) + } + } + + // If everything is defined and we are at the last line. + if(date, type, amount, notes !== undefined && j + 1 == end[i]) { + console.log("Everything is defined") + if((actual_date < date && actual_date !== date) && amount !== actual_amount) { + console.log("NEW TRANSACTION!") + api.importTransactions(ACTUAL_CHECKING_ID, [ + { + date: date, + amount: amount, + payee_name: name, + notes: notes + } + ]) + } + } + } + } + }); + await new Promise(r => setTimeout(r, 10000)); } - - - //await api.shutdown(); - - })(); + diff --git a/package.json b/package.json index 817167d..167d662 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,8 @@ "license": "ISC", "main": "index.js", "scripts": { - "actual": "node actual-api/src/index.js" + "actual": "node actual-api/src/index.js", + "start": "concurrently 'npm run actual' 'cd ./webscrape-bank && ./run.sh'" }, "dependencies": { "@actual-app/api": "^26.1.0", diff --git a/webscrape-bank/src/extract.py b/webscrape-bank/src/extract.py index 3ab746f..e00117c 100644 --- a/webscrape-bank/src/extract.py +++ b/webscrape-bank/src/extract.py @@ -12,7 +12,7 @@ LOGIN_LINK = os.getenv("LOGIN_LINK") ACCOUNT_LINK = os.getenv("ACCOUNT_LINK") def main(playwright: Playwright) -> None: - browser = playwright.chromium.launch(headless=False) + browser = playwright.chromium.launch(headless=True) context = browser.new_context() page = context.new_page() @@ -28,10 +28,27 @@ def main(playwright: Playwright) -> None: page.get_by_role("button", name="Text").click() page.get_by_test_id("text-field").click() - # Bank prompts 2FA (hopefully only once) - two_fac = input("2FA Code: ") + # Bank prompts 2FA (hopefully only once) in this meantime - page.get_by_test_id("text-field").fill(two_fac) + two_fac_code = "" + + # Write/Overwrite an empty 2FA file + with open("./2fa", "w") as two_fac: + two_fac.write("") + + with open("./2fa", "r") as two_fac: + line = two_fac.readline() + print(line) + while line == "": + print("2FA code hasn't been entered, make sure to enter code in ./2fa !") + print("Retrying in 5 seconds...") + sleep(5) + line = two_fac.readline() + + two_fac_code = line + print("2FA Code Found: " + two_fac_code) + + page.get_by_test_id("text-field").fill(two_fac_code) page.get_by_test_id("private-button").click() # Wait for everything to load... diff --git a/webscrape-bank/src/main.py b/webscrape-bank/src/main.py index 3b53bd3..da6dd99 100644 --- a/webscrape-bank/src/main.py +++ b/webscrape-bank/src/main.py @@ -1,49 +1,87 @@ from playwright.sync_api import sync_playwright -from threading import Thread import os import extract -#from datetime import datetime -import difflib +from datetime import datetime +from time import sleep +from multiprocessing import Process # Playwright in the background def playwright(): + print("Starting Playwright") with sync_playwright() as playwright: extract.main(playwright) def revise(): - # Removes the OLDEST file on the list () - file_count = 0 - for file in os.scandir("./qfx"): - print(file_count) - if file_count > 1: - #print(os.listdir("./qfx")) - print("Removed: " + os.listdir("./qfx")[0]) - os.remove("./qfx/" + os.listdir("./qfx")[0]) + print("Starting Revision") + while (True): + with open("./data/output", "w") as output: + output.write("") - if file.is_file(): - file_count += 1 + # Removes the OLDEST file on the list () + file_count = 0 + for file in os.scandir("./qfx"): + #print(file_count) + if file_count > 1: + #print(os.listdir("./qfx")) + print("Removed: " + os.listdir("./qfx")[0]) + os.remove("./qfx/" + os.listdir("./qfx")[0]) - # Get the last two files - oldest_file = open("./qfx/" + os.listdir("./qfx")[0]) - newest_file = open("./qfx/" + os.listdir("./qfx")[-1]) - # Differs the two files - diff = difflib.ndiff(oldest_file.readlines(), newest_file.readlines()) + if file.is_file(): + file_count += 1 + + # Thanks to: https://stackoverflow.com/a/33657921 + # Find the for the newest files + # Grabs all starting and ending transaction indexes aswell as the time of the latest transaction + start = [] + end = [] + latest_trans_time = datetime.now() + with open("./qfx/" + os.listdir("./qfx")[-1]) as newest_file: + num = 0 + # Find the number and line under a object that supports iteration + for num, line in enumerate(newest_file, 1): + if "DTEND" in line: + time_string = line.strip()[7:-8] + + year = int(time_string[0:4]) + month = int(time_string[5:6]) + day = int(time_string[6:8]) + + latest_trans_time.replace(year, month, day) + num = str(num) + + # TODO: The idea is to find a way to grab all data relating to latest_trans_time WITHIN + if "" in line: + start.append(num-1) + + if "" in line: + end.append(num) + + # Seperates all transactions to the latest ones to ./data/output + with open("./qfx/" + os.listdir("./qfx")[-1]) as newest_file: + lines = newest_file.read().splitlines() + + for i in range(len(start)): + ST_BEGINS = start[i] + ST_ENDS = end[i] + + with open("./data/output", "a") as output: + # NOTE: Puts all data from to into its individual line + output.write("\n".join(lines[ST_BEGINS:ST_ENDS]).replace(" ", "").replace("\t", "")) + output.write("\n") + + # Wait a minute before executing again + print("Done with revision!") + sleep(60) - # Grabs only changes - # Thanks to: https://stackoverflow.com/a/15864920 - changes = [l for l in diff if l.startswith("+ ") or l.startswith('- ')] - print("RESULT:") - for change in changes: - print(change[2:]) def main(): - revise_thread = Thread(target=revise()) - #pw_thread = Thread(target=playwright()) - #pw_thread.start() - revise_thread.start() + pw_proc = Process(target=playwright) + pw_proc.start() + revise_proc = Process(target=revise) + revise_proc.start() if __name__ == "__main__":