Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Updated to latest Firefly API #6

Merged
merged 14 commits into from
Jan 12, 2023
Merged
2 changes: 2 additions & 0 deletions Earthfile
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
VERSION 0.6

FROM node:16.9-alpine3.11
WORKDIR /home/node/app

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ More info [here](https://docs.earthly.dev/docs/guides/multi-platform).
- [x] Accounts management
- [x] List transactions
- [x] Add math equations when creating transactions
- [x] Allow selecting of Liabilities accounts in transactions
- [ ] Configure CI/CD so that it builds and pushes docker images on merges to master
- [ ] Reports
- [ ] Proper error handling
Expand Down
7,198 changes: 5,284 additions & 1,914 deletions package-lock.json

Large diffs are not rendered by default.

44 changes: 24 additions & 20 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "firefly-iii-telegram-bot",
"description": "A Telegram bot for working with Firefly III with a supersonic speed",
"version": "0.0.1",
"version": "0.0.2",
"homepage": "https://github.com/cyxou/firefly-iii-telegram-bot#readme",
"license": "GPL-3.0-or-later",
"repository": {
Expand All @@ -16,6 +16,7 @@
"start": "nodemon src/index.ts",
"lint": "eslint . --ext .ts",
"fix": "eslint . --ext .ts --fix",
"update:i": "ncu --interactive",
"build": "tsc",
"postbuild": "npm run copylocales",
"copylocales": "cp -r src/locales dist/",
Expand All @@ -25,28 +26,31 @@
"codegen": "openapi-generator-cli generate -i https://api-docs.firefly-iii.org/firefly-iii-1.5.4.yaml -o src/lib/firefly -g typescript-axios -c .openapi-generator-config.yaml"
},
"devDependencies": {
"@openapitools/openapi-generator-cli": "2.4.12",
"@openapitools/openapi-generator-cli": "2.5.2",
"@types/debug": "4.1.7",
"@types/node": "16.7.2",
"@typescript-eslint/eslint-plugin": "4.31.2",
"@typescript-eslint/parser": "4.31.2",
"eslint": "7.32.0",
"nodemon": "2.0.12",
"ts-node": "10.2.1",
"typescript": "4.3.5"
"@types/lodash.flatten": "4.4.7",
"@types/lodash.isempty": "4.4.7",
"@types/node": "18.7.15",
"@typescript-eslint/eslint-plugin": "5.36.2",
"@typescript-eslint/parser": "5.36.2",
"eslint": "8.23.0",
"nodemon": "2.0.19",
"npm-check-updates": "16.1.0",
"ts-node": "10.9.1",
"typescript": "4.8.2"
},
"dependencies": {
"@grammyjs/i18n": "0.3.0",
"@grammyjs/router": "1.1.1",
"@types/lodash.isempty": "4.4.6",
"axios": "0.21.4",
"dayjs": "1.10.6",
"debug": "4.3.2",
"dotenv": "10.0.0",
"grammy": "1.3.4",
"@grammyjs/i18n": "0.5.1",
"@grammyjs/router": "2.0.0",
"axios": "0.27.2",
"dayjs": "1.11.5",
"debug": "4.3.4",
"dotenv": "16.0.2",
"grammy": "1.11.0",
"lodash.flatten": "4.4.0",
"lodash.isempty": "4.4.0",
"mathjs": "9.5.1",
"node-fetch": "3.0.0",
"table": "6.7.2"
"mathjs": "11.1.0",
"node-fetch": "3.2.10",
"table": "6.8.0"
}
}
2 changes: 1 addition & 1 deletion src/composers/categories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ async function doDeleteCategoryCbQH(ctx: MyContext) {

return replyWithListOfCategories(ctx)

} catch (err) {
} catch (err: any) {
log('Error: %O', err)
console.error(err)
ctx.reply('Error occurred deleting a category: ', err.message)
Expand Down
169 changes: 75 additions & 94 deletions src/composers/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import dayjs from 'dayjs'
import Debug from 'debug'
import flatten from 'lodash.flatten'
import { evaluate } from 'mathjs'
import { ParseMode } from '@grammyjs/types'
import { Keyboard, InlineKeyboard } from 'grammy'
Expand All @@ -13,6 +14,7 @@ import { TransactionSplitTypeEnum } from '../lib/firefly/model/transaction-split
import { TransactionSplit } from '../lib/firefly/model/transaction-split'
import { AccountTypeFilter } from '../lib/firefly/model/account-type-filter'
import { AccountTypeEnum } from '../lib/firefly/model/account'
import { AccountRead } from '../lib/firefly/model/account-read'

const debug = Debug('bot:transactions:helpers')

Expand All @@ -28,7 +30,6 @@ export {
formatTransactionKeyboard,
createCategoriesKeyboard,
createAccountsKeyboard,
createExpenseAccountsKeyboard,
createEditMenuKeyboard,
createMainKeyboard,
generateWelcomeMessage
Expand Down Expand Up @@ -57,48 +58,59 @@ const addTransactionsMapper = {
}

const editTransactionsMapper = {
assignCategory: new Mapper('ASSIGN_TRANSACTION_CATEGORY_ID=${trId}'),
editMenu: new Mapper('EDIT_TRANSACTION_ID=${trId}'),
done: new Mapper('DONE_EDIT_TRANSACTION_ID=${trId}'),
editDate: new Mapper('CHANGE_TRANSACTION_DATE_ID=${trId}'),
editAmount: new Mapper('CHANGE_TRANSACTION_AMOUNT_ID=${trId}'),
editDesc: new Mapper('CHANGE_TRANSACTION_DESCRIPTION_ID=${trId}'),
editCategory: new Mapper('CHANGE_TRANSACTION_CATEGORY_ID=${trId}'),
setCategory: new Mapper('SET_TRANSACTION_CATEGORY_ID=${categoryId}'),
editAssetAccount: new Mapper('CHANGE_TRANSACTION_SOURCE_ID=${trId}'),
setAssetAccount: new Mapper('SET_TRANSACTION_ASSET_ID=${accountId}'),
editDepositAssetAccount: new Mapper('CHANGE_DEPOSIT_TRANSACTION_SOURCE_ID=${trId}'),
setDepositAssetAccount: new Mapper('SET_DEPOSIT_TRANSACTION_ASSET_ID=${accountId}'),
editExpenseAccount: new Mapper('CHANGE_TRANSACTION_EXPENSE_ID=${trId}'),
setExpenseAccount: new Mapper('SET_TRANSACTION_EXPENSE_ID=${accountId}'),
editRevenueAccount: new Mapper('CHANGE_TRANSACTION_REVENUE_ID=${trId}'),
setRevenueAccount: new Mapper('SET_TRANSACTION_REVENUE_ID=${accountId}'),
editSourceAccount: new Mapper('CHANGE_SOURCE_ASSET_ACCOUNT_ID=${trId}'),
setSourceAccount: new Mapper('SET_SOURCE_ASSET_ACCOUNT_ID=${accountId}'),
editDestinationAccount: new Mapper('CHANGE_DESTINATION_ASSET_ACCOUNT_ID=${trId}'),
setDestinationAccount: new Mapper('SET_DESTINATION_ASSET_ACCOUNT_ID=${accountId}'),
editSourceAccount: new Mapper('CHANGE_SOURCE_ACCOUNT_ID=${trId}'),
setSourceAccount: new Mapper('SET_SOURCE_ACCOUNT_ID=${accountId}'),
editDestinationAccount: new Mapper('CHANGE_DESTINATION_ACCOUNT_ID=${trId}'),
setDestinationAccount: new Mapper('SET_DESTINATION_ACCOUNT_ID=${accountId}'),
}

function parseAmountInput(amount: string): number | null {
const validInput = /^\d{1,}(?:[.,]\d+)?([-+/*^]\d{1,}(?:[.,]\d+)?)*$/
if (validInput.exec(amount)) return Math.abs(evaluate(amount))
else return null
function parseAmountInput(amount: string, oldAmount?: string): number | null {
const validInput = /^[-+/*]?\d{1,}(?:[.,]\d+)?([-+/*^]\d{1,}(?:[.,]\d+)?)*$/
if (!validInput.exec(amount)) return null

if (oldAmount && (amount.startsWith('+') || amount.startsWith('-')
|| amount.startsWith('/') || amount.startsWith('*'))) {
return Math.abs(evaluate(`${oldAmount}${amount}`))
}

return Math.abs(evaluate(amount))
}

function formatTransactionKeyboard(ctx: MyContext, tr: TransactionRead) {
const trSplit = tr.attributes.transactions[0]
const inlineKeyboard = new InlineKeyboard()
const log = debug.extend('formatTransactionKeyboard')
const trKeyboard = new InlineKeyboard()
// If transaction does not have a category, show button to specify one
if (!tr.attributes.transactions[0].category_name) {
trKeyboard
.text(
ctx.i18n.t('labels.CHANGE_CATEGORY'),
editTransactionsMapper.assignCategory.template({ trId: tr.id })
)
}

trKeyboard
.text(
ctx.i18n.t('labels.EDIT_TRANSACTION'),
editTransactionsMapper.editMenu.template({ trId: trSplit.transaction_journal_id as string })
editTransactionsMapper.editMenu.template({ trId: tr.id })
)
.text(
ctx.i18n.t('labels.DELETE'),
addTransactionsMapper.delete.template({ trId: trSplit.transaction_journal_id as string })
addTransactionsMapper.delete.template({ trId: tr.id })
)

log('trKeyboard: %O', trKeyboard.inline_keyboard)

return {
parse_mode: 'Markdown' as ParseMode,
reply_markup: inlineKeyboard
reply_markup: trKeyboard
}
}

Expand Down Expand Up @@ -160,15 +172,30 @@ async function createCategoriesKeyboard(userId: number, mapper: Mapper) {

async function createAccountsKeyboard(
userId: number,
accountType: AccountTypeFilter,
accountType: AccountTypeFilter | AccountTypeFilter[],
mapper: Mapper,
opts?: { skipAccountId: string }
) {
const log = debug.extend('createAccountKeyboard')
try {
let accounts = (await firefly(userId).Accounts.listAccount(
1, dayjs().format('YYYY-MM-DD'), accountType)).data.data
// log('accounts: %O', accounts)
let accounts: AccountRead[] = []
const now = dayjs().format('YYYY-MM-DD')

if (Array.isArray(accountType)) {
const promises: any = []
accountType.forEach(at => promises.push(firefly(userId).Accounts.listAccount(1, now, at)))
const responses = await Promise.all(promises)

log('Responses length: %s', responses.length)

accounts = flatten(responses.map(r => {
return r.data.data
}))
} else {
accounts = (await firefly(userId).Accounts.listAccount(1, now, accountType)).data.data
}

log('accounts: %O', accounts)
const keyboard = new InlineKeyboard()

// Prevent from choosing same account when doing transfers
Expand All @@ -186,34 +213,7 @@ async function createAccountsKeyboard(
if (i % 2 !== 0 || i === last) keyboard.row()
})

// log('keyboard.inline_keyboard: %O', keyboard.inline_keyboard)

return keyboard
} catch (err) {
log('Error: %O', err)
console.error('Error occurred creating acounts keyboard: ', err)
throw err
}
}

async function createExpenseAccountsKeyboard(userId: number) {
const log = debug.extend('createAssetsAccountKeyboard')
try {
const accounts = (await firefly(userId).Accounts.listAccount(
1, dayjs().format('YYYY-MM-DD'), AccountTypeFilter.Expense)).data.data
// log('accounts: %O', accounts)
const keyboard = new InlineKeyboard()

for (let i = 0; i < accounts.length; i++) {
const c = accounts[i]
const last = accounts.length - 1
const cbData = `SET_TRANSACTION_EXPENSE_ID=${c.id}`

keyboard.text(c.attributes.name, cbData)
// Split accounts keyboard into two columns so that every odd indexed
// account starts from new row as well as the last account in the list.
if (i % 2 !== 0 || i === last) keyboard.row()
}
log('keyboard.inline_keyboard: %O', keyboard.inline_keyboard)

return keyboard
} catch (err) {
Expand Down Expand Up @@ -263,54 +263,35 @@ function formatTransactionUpdate(

return `${formatTransaction(ctx, trRead)}\n${diffPart}`

} catch (err) {
} catch (err: any) {
console.error(err)
return err.message
}
}

function createEditWithdrawalTransactionKeyboard(ctx: MyContext, trId: string | number) {
return new InlineKeyboard()
.text(ctx.i18n.t('labels.CHANGE_DESCRIPTION'), editTransactionsMapper.editDesc.template({ trId }))
.text(ctx.i18n.t('labels.CHANGE_CATEGORY'), editTransactionsMapper.editCategory.template({ trId })).row()
.text(ctx.i18n.t('labels.CHANGE_ASSET_ACCOUNT'), editTransactionsMapper.editAssetAccount.template({ trId }))
.text(ctx.i18n.t('labels.CHANGE_EXPENSE_ACCOUNT'), editTransactionsMapper.editExpenseAccount.template({ trId })).row()
.text(ctx.i18n.t('labels.CHANGE_DATE'), editTransactionsMapper.editDate.template({ trId }))
.text(ctx.i18n.t('labels.CHANGE_AMOUNT'), editTransactionsMapper.editAmount.template({ trId })).row()
.text(ctx.i18n.t('labels.DONE'), editTransactionsMapper.done.template({ trId })).row()
}
function createEditMenuKeyboard(ctx: MyContext, tr: TransactionRead) {
const keyboard = new InlineKeyboard()
const trId = tr.id
const userId = ctx.from!.id
const { fireflyUrl } = getUserStorage(userId)

function createEditDepositTransactionKeyboard(ctx: MyContext, trId: string | number) {
return new InlineKeyboard()
.text(ctx.i18n.t('labels.CHANGE_DESCRIPTION'), editTransactionsMapper.editDesc.template({ trId })).row()
.text(ctx.i18n.t('labels.CHANGE_REVENUE_ACCOUNT'), editTransactionsMapper.editRevenueAccount.template({ trId }))
.text(ctx.i18n.t('labels.CHANGE_ASSET_ACCOUNT'), editTransactionsMapper.editDepositAssetAccount.template({ trId })).row()
.text(ctx.i18n.t('labels.CHANGE_DATE'), editTransactionsMapper.editDate.template({ trId }))
.text(ctx.i18n.t('labels.CHANGE_AMOUNT'), editTransactionsMapper.editAmount.template({ trId })).row()
.text(ctx.i18n.t('labels.DONE'), editTransactionsMapper.done.template({ trId })).row()
}
// Only withdrawal transactions may have category assigned
if (tr.attributes.transactions[0].type === 'withdrawal') {
keyboard
.text(ctx.i18n.t('labels.CHANGE_CATEGORY'), editTransactionsMapper.editCategory.template({trId})).row()
}

function createEditTransferTransactionKeyboard(ctx: MyContext, trId: string | number) {
return new InlineKeyboard()
.text(ctx.i18n.t('labels.CHANGE_DESCRIPTION'), editTransactionsMapper.editDesc.template({ trId })).row()
.text(ctx.i18n.t('labels.CHANGE_ASSET_ACCOUNT'), editTransactionsMapper.editSourceAccount.template({ trId }))
.text(ctx.i18n.t('labels.CHANGE_ASSET_ACCOUNT'), editTransactionsMapper.editDestinationAccount.template({ trId })).row()
.text(ctx.i18n.t('labels.CHANGE_DATE'), editTransactionsMapper.editDate.template({ trId }))
.text(ctx.i18n.t('labels.CHANGE_AMOUNT'), editTransactionsMapper.editAmount.template({ trId })).row()
.text(ctx.i18n.t('labels.DONE'), editTransactionsMapper.done.template({ trId })).row()
}
keyboard
.text(ctx.i18n.t('labels.CHANGE_SOURCE_ACCOUNT'), editTransactionsMapper.editSourceAccount.template({trId}))
.text(ctx.i18n.t('labels.CHANGE_DEST_ACCOUNT'), editTransactionsMapper.editDestinationAccount.template({trId})).row()
.text(ctx.i18n.t('labels.CHANGE_DESCRIPTION'), editTransactionsMapper.editDesc.template({trId}))
// TODO Add functionality to change the date of a transaction
// .text(ctx.i18n.t('labels.CHANGE_DATE'), editTransactionsMapper.editDate.template({trId}))
.text(ctx.i18n.t('labels.CHANGE_AMOUNT'), editTransactionsMapper.editAmount.template({trId})).row()
.url(ctx.i18n.t('labels.OPEN_IN_BROWSER'), `${fireflyUrl}/transactions/show/${trId}`).row()
.text(ctx.i18n.t('labels.DONE'), editTransactionsMapper.done.template({trId})).row()

function createEditMenuKeyboard(ctx: MyContext, tr: TransactionRead) {
switch (tr.attributes.transactions[0].type) {
case 'withdrawal':
return createEditWithdrawalTransactionKeyboard(ctx, tr.id)
case 'deposit':
return createEditDepositTransactionKeyboard(ctx, tr.id)
case 'transfer':
return createEditTransferTransactionKeyboard(ctx, tr.id)
default:
return new InlineKeyboard().text('👻 Unexpected transaction type')
}
return keyboard
}

function createAccountsMenuKeyboard( ctx: MyContext, accType: AccountTypeEnum) {
Expand Down
Loading