From 9c324573a3b512e5edef45b8b8ac5ec1a1719e05 Mon Sep 17 00:00:00 2001 From: Benjamin Kraft Date: Wed, 21 Aug 2024 22:08:26 +0200 Subject: [PATCH] Initial Bot --- .gitignore | 4 ++ .nvmrc | 1 + package-lock.json | 171 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 27 ++++++++ src/index.ts | 115 +++++++++++++++++++++++++++++++ tsconfig.json | 13 ++++ 6 files changed, 331 insertions(+) create mode 100644 .gitignore create mode 100644 .nvmrc create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/index.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9916fec --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.idea +node_modules +.env +out diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..790e110 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +v20.10.0 diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..1180f9a --- /dev/null +++ b/package-lock.json @@ -0,0 +1,171 @@ +{ + "name": "transactionparser", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "transactionparser", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "dotenv": "^16.4.5", + "grammy": "^1.29.0" + }, + "devDependencies": { + "@types/node": "^22.4.2", + "typescript": "^5.5.4" + } + }, + "node_modules/@grammyjs/types": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@grammyjs/types/-/types-3.13.0.tgz", + "integrity": "sha512-Oyq6fBuVPyX6iWvxT/0SxJvNisC9GHUEkhZ60qJBHRmwNX4hIcOfhrNEahicn3K9SYyreGPVw3d9wlLRds83cw==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.4.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.4.2.tgz", + "integrity": "sha512-nAvM3Ey230/XzxtyDcJ+VjvlzpzoHwLsF7JaDRfoI0ytO0mVheerNmM45CtA0yOILXwXXxOrcUWH3wltX+7PSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/grammy": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/grammy/-/grammy-1.29.0.tgz", + "integrity": "sha512-lj/6K6TGmVAdOpHj0PVFK7N37EGe76bpkbgvN+yqCqXYBIwuQosTe7qLhCls7/4pbDxf2+UVSqSXcOILgGGKWQ==", + "license": "MIT", + "dependencies": { + "@grammyjs/types": "3.13.0", + "abort-controller": "^3.0.0", + "debug": "^4.3.4", + "node-fetch": "^2.7.0" + }, + "engines": { + "node": "^12.20.0 || >=14.13.1" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "license": "MIT" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..80d75ce --- /dev/null +++ b/package.json @@ -0,0 +1,27 @@ +{ + "name": "transactionparser", + "version": "1.0.0", + "description": "Telegram Bot to parse transactions and insert into Firefly III", + "repository": { + "type": "git", + "url": "https://git.benjamin-kraft.eu/benjamin/TransactionParser.git" + }, + "keywords": [ + "bot", + "telegram", + "finance" + ], + "scripts": { + "start": "node out/index.js" + }, + "author": "Benjamin Kraft", + "license": "ISC", + "devDependencies": { + "@types/node": "^22.4.2", + "typescript": "^5.5.4" + }, + "dependencies": { + "dotenv": "^16.4.5", + "grammy": "^1.29.0" + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..ddd567e --- /dev/null +++ b/src/index.ts @@ -0,0 +1,115 @@ +import {Bot} from "grammy"; +import {BotCommand, User} from "@grammyjs/types" +import {configDotenv} from "dotenv"; + +configDotenv() + + +const botToken = process.env["BOT_TOKEN"]!; +const fireflyToken = process.env["FIREFLY_TOKEN"]!; +const fireflyURL = process.env["FIREFLY_URL"]!; +const sourceAccountId = process.env["SOURCE_ACCOUNT_ID"]!; +const expenseAccountId = process.env["EXPENSE_ACCOUNT_ID"]!; + +const bot = new Bot(botToken); + +function isSenderAllowed(user: User){ + return user.username === process.env["TELEGRAM_USER"]; +} + +async function fetchBudgets(){ + const budgets: Record = {}; + + const res = await fetch(`${fireflyURL}/api/v1/budgets`, { + method: "GET", + headers: [ + ["Authorization", `Bearer ${fireflyToken}`], + ["Accept", "application/vnd.api+json"] + ] + }); + const result = await res.json(); + for (const budget of result.data) + budgets[budget.id] = budget.attributes.name; + + console.log("Budgets: ", budgets); + + return budgets; +} + +async function start(){ + const budgets = await fetchBudgets(); + + let currentBudgetID = ""; + + const budgetCommands: BotCommand[] = [] + for (const [id, name] of Object.entries(budgets)){ + bot.command(`budget${name.toLowerCase()}`, async ctx => { + if (!isSenderAllowed(ctx.message?.from!)) + return; + currentBudgetID = id; + console.log(`Changed current Budget to ${budgets[id]}`) + }); + budgetCommands.push({ + command: `budget${name.toLowerCase()}`, + description: "Change budget" + }); + } + budgetCommands.push({ + command: "currentbudget", + description: "Show currently selected budget" + }); + + bot.command("currentbudget", async ctx => { + if (!isSenderAllowed(ctx.message?.from!)) + return; + await ctx.reply(currentBudgetID || "No budget selected!"); + }); + + await bot.api.setMyCommands(budgetCommands); + + bot.on("message", async ctx => { + if (!isSenderAllowed(ctx.message.from!)) + return; + + if (currentBudgetID === ""){ + await ctx.reply("Select a budget first!"); + return; + } + + const text = ctx.message.text!; + const parts = text.split(" "); + const amount = parts[0]; + + let description = parts.slice(1).join(" ") || budgets[currentBudgetID]; + + const res = await fetch(`${fireflyURL}/api/v1/transactions`, { + method: "POST", + headers: [ + ["Authorization", `Bearer ${fireflyToken}`], + ["Content-Type", "application/json"] + ], + body: JSON.stringify({ + transactions: [ + { + type: "withdrawal", + date: new Date().toISOString(), + amount: amount, + description: description, + source_id: sourceAccountId, + destination_id: expenseAccountId, + budget_id: currentBudgetID + } + ] + }) + }); + + if (res.status === 200){ + console.log(`Added ${amount} to ${budgets[currentBudgetID]}`); + } + + }); + + await bot.start(); +} + +start(); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..15b68e3 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "es2023", + "module": "commonjs", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "sourceMap": true, + "outDir": "out" + }, + "include": ["src/**/*.ts"] +}