Compare commits
15 commits
Author | SHA1 | Date | |
---|---|---|---|
f50fff0df9 | |||
9aa3422be9 | |||
5350fef36d | |||
e800282667 | |||
eee96d14b3 | |||
f1bc9408db | |||
d13ef5dfd8 | |||
715b0a2fbf | |||
f2a5ec3990 | |||
340ff5270e | |||
4a1eccc3a2 | |||
bbfffb383d | |||
71781ae3be | |||
daf5eb07ce | |||
91c5ec3f79 |
9 changed files with 305 additions and 9 deletions
44
README.md
44
README.md
|
@ -32,3 +32,47 @@ Or use the prebuilt docker image:
|
|||
docker run -e FIREFLY_API_HOST="https://your-firefly-installation" -e FIREFLY_PERSONAL_ACCESS_TOKEN="abcd....1234" -p 9449:5000 zknt/firefly-exporter
|
||||
|
||||
and scrape port 9449 on your docker host.
|
||||
|
||||
## XMPP Bot
|
||||
|
||||
There is a simple XMPP / Jabber bot which responds to budget queries.
|
||||
|
||||
### Installation
|
||||
|
||||
Install requirements:
|
||||
|
||||
pip install -r requirements-xmppbot.txt
|
||||
|
||||
Use your personal access token from above, setting the following variables
|
||||
to your environment:
|
||||
|
||||
export FIREFLY_JABBER_ID="firefly@example.com" # The bots JID
|
||||
export FIREFLY_JABBER_PASSWORD="abcd...1234" # The bots xmpp password
|
||||
export FIREFLY_API_HOST="https://yout-firefly-installation"
|
||||
export FIREFLY_PERSONAL_ACCESS_TOKEN="1234...abcd"
|
||||
|
||||
### Preparation
|
||||
|
||||
Register a XMPP / Jabber account for your bot.
|
||||
*Caveat*: The bot does not support E2E and handles your financial data, so make
|
||||
sure the account resides on the same server as your personal account which you will
|
||||
use for querying.
|
||||
|
||||
Log into your bot account with a XMPP client and add your personal JID to your roster.
|
||||
Querying will only work with a two-way subscription.
|
||||
|
||||
## Bot usage
|
||||
|
||||
Run `python xmppbot.py`.
|
||||
|
||||
From your personal account, send a normal chat message to your bot.
|
||||
Available commands:
|
||||
|
||||
* `budgets` - list all budgets names
|
||||
* `budget <budget>` - query data for given budget
|
||||
|
||||
### Docker
|
||||
|
||||
Run the prebuilt image:
|
||||
|
||||
docker run -e FIREFLY_JABBER_ID="firefly@example.com" -e FIREFLY_JABBER_PASSWORD="abcd...1234" -e FIREFLY_API_HOST="https://yout-firefly-installation" -e FIREFLY_PERSONAL_ACCESS_TOKEN="1234...abcd" zknt/firefly-xmppbot
|
||||
|
|
39
budgetmail.py
Normal file
39
budgetmail.py
Normal file
|
@ -0,0 +1,39 @@
|
|||
import calendar
|
||||
import datetime
|
||||
|
||||
import firefly.budgets
|
||||
|
||||
|
||||
def _get_remaining_days():
|
||||
today = datetime.datetime.combine(datetime.date.today(), datetime.datetime.min.time())
|
||||
endofmonth = today.replace(day=calendar.monthrange(today.year, today.month)[1])
|
||||
return (endofmonth - today).days + 1
|
||||
|
||||
|
||||
def _collect_budget_data():
|
||||
budgets = []
|
||||
for budget in firefly.budgets._get_budgets():
|
||||
budgets.append({
|
||||
"id": budget.get('id'),
|
||||
"name": budget.get('attributes').get('name'),
|
||||
"limit": firefly.budgets._get_current_limit(budget.get('id')).get('attributes').get('amount'),
|
||||
"spent": firefly.budgets._get_current_spent_amount(budget.get('id')),
|
||||
})
|
||||
currbudget = budgets[-1]
|
||||
currbudget['remaining'] = (float(currbudget.get('limit')) - float(currbudget.get('spent')))
|
||||
remaining_per_day = currbudget.get('remaining') / _get_remaining_days()
|
||||
currbudget['remaining_per_day'] = 0 if remaining_per_day < 0 else remaining_per_day
|
||||
return budgets
|
||||
|
||||
|
||||
def main():
|
||||
for budget in _collect_budget_data():
|
||||
print("Budget: " + budget.get('name'))
|
||||
print(" Limit: " + budget.get('limit'))
|
||||
print(" Spent: {:.2f}".format(budget.get('spent')))
|
||||
print(" Remaining: {:.2f}".format(budget.get('remaining')))
|
||||
print(" Remaining per day: {:.2f}".format(budget.get('remaining_per_day')))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
59
budgetxmpp.py
Normal file
59
budgetxmpp.py
Normal file
|
@ -0,0 +1,59 @@
|
|||
import calendar
|
||||
import datetime
|
||||
import xmpp
|
||||
import time
|
||||
import os
|
||||
|
||||
import firefly.budgets
|
||||
|
||||
|
||||
def _get_remaining_days():
|
||||
today = datetime.datetime.combine(datetime.date.today(), datetime.datetime.min.time())
|
||||
endofmonth = today.replace(day=calendar.monthrange(today.year, today.month)[1])
|
||||
return (endofmonth - today).days + 1
|
||||
|
||||
|
||||
def _collect_budget_data():
|
||||
budgets = []
|
||||
for budget in firefly.budgets._get_budgets():
|
||||
budgets.append({
|
||||
"id": budget.get('id'),
|
||||
"name": budget.get('attributes').get('name'),
|
||||
"limit": firefly.budgets._get_current_limit(budget.get('id')).get('attributes').get('amount'),
|
||||
"spent": firefly.budgets._get_current_spent_amount(budget.get('id')),
|
||||
})
|
||||
currbudget = budgets[-1]
|
||||
currbudget['remaining'] = (float(currbudget.get('limit')) - float(currbudget.get('spent')))
|
||||
remaining_per_day = currbudget.get('remaining') / _get_remaining_days()
|
||||
currbudget['remaining_per_day'] = 0 if remaining_per_day < 0 else remaining_per_day
|
||||
return budgets
|
||||
|
||||
|
||||
def main():
|
||||
jid = xmpp.protocol.JID(os.environ.get("FIREFLY_JABBER_ID"))
|
||||
connection = xmpp.Client(server=jid.getDomain())
|
||||
connection.connect()
|
||||
connection.auth(
|
||||
user=jid.getNode(),
|
||||
password=os.environ.get("FIREFLY_JABBER_PASSWORD"),
|
||||
resource=jid.getResource(),
|
||||
)
|
||||
|
||||
for budget in _collect_budget_data():
|
||||
message = "Budget: " + budget.get('name')
|
||||
message += "\n Limit: " + budget.get('limit')
|
||||
message += "\n Spent: {:.2f}".format(budget.get('spent'))
|
||||
message += "\n Remaining: {:.2f}".format(budget.get('remaining'))
|
||||
message += "\n Remaining per day: {:.2f}".format(budget.get('remaining_per_day'))
|
||||
|
||||
connection.send(
|
||||
xmpp.protocol.Message(
|
||||
to=os.environ.get('FIREFLY_JABBER_RECEIVER'),
|
||||
body=message,
|
||||
),
|
||||
)
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -0,0 +1,14 @@
|
|||
"""Firefly3 API modules"""
|
||||
import datetime
|
||||
import calendar
|
||||
|
||||
|
||||
def get_remaining_days():
|
||||
"""Get number of days remaining in current month"""
|
||||
today = datetime.datetime.combine(
|
||||
datetime.date.today(), datetime.datetime.min.time()
|
||||
)
|
||||
endofmonth = today.replace(
|
||||
day=calendar.monthrange(today.year, today.month)[1]
|
||||
)
|
||||
return (endofmonth - today).days + 1
|
|
@ -3,18 +3,22 @@ import os
|
|||
import calendar
|
||||
import datetime
|
||||
|
||||
import pytz
|
||||
import requests
|
||||
import dateutil.parser
|
||||
|
||||
import firefly
|
||||
|
||||
|
||||
header = {"Authorization": "Bearer " + os.environ.get('FIREFLY_PERSONAL_ACCESS_TOKEN')}
|
||||
host = os.environ.get('FIREFLY_API_HOST')
|
||||
|
||||
|
||||
def _get_budgets():
|
||||
budgets_json = requests.get(
|
||||
budgets_req = requests.get(
|
||||
host + "/api/v1/budgets", headers=header
|
||||
).json()
|
||||
)
|
||||
budgets_json = budgets_req.json()
|
||||
budgets = budgets_json.get('data')
|
||||
while "next" in budgets_json.get('links'):
|
||||
budgets_json = requests.get(
|
||||
|
@ -35,7 +39,7 @@ def _get_current_limit(budget_id):
|
|||
).json()
|
||||
budgets.extend(budget_json.get('data'))
|
||||
|
||||
today = datetime.datetime.combine(datetime.date.today(), datetime.datetime.min.time())
|
||||
today = datetime.datetime.combine(datetime.date.today(), datetime.datetime.min.time()).replace(tzinfo=pytz.UTC)
|
||||
return [
|
||||
budget for budget in budgets
|
||||
if today >= dateutil.parser.parse(budget.get('attributes').get('start')) and today <= dateutil.parser.parse(budget.get('attributes').get('end'))
|
||||
|
@ -44,11 +48,13 @@ def _get_current_limit(budget_id):
|
|||
|
||||
def _get_current_transactions(budget_id):
|
||||
limit_id = _get_current_limit(budget_id).get('id')
|
||||
transactions_json = requests.get(
|
||||
host + "/api/v1/budgets/limits/{}/transactions".format(
|
||||
transactions_req = requests.get(
|
||||
host + "/api/v1/budgets/{}/limits/{}/transactions".format(
|
||||
budget_id,
|
||||
limit_id
|
||||
), headers=header
|
||||
).json()
|
||||
)
|
||||
transactions_json = transactions_req.json()
|
||||
transactions = transactions_json.get('data')
|
||||
while 'next' in transactions_json:
|
||||
transactions_json = requests.get(
|
||||
|
@ -70,6 +76,24 @@ def _get_current_spent_amount(budget_id):
|
|||
return spent_amount
|
||||
|
||||
|
||||
def _collect_budget_data():
|
||||
budgets = []
|
||||
for budget in _get_budgets():
|
||||
budgets.append({
|
||||
"id": budget.get('id'),
|
||||
"name": budget.get('attributes').get('name'),
|
||||
"limit": float(_get_current_limit(budget.get('id')).get('attributes').get('amount')),
|
||||
"spent": _get_current_spent_amount(budget.get('id')),
|
||||
})
|
||||
currbudget = budgets[-1]
|
||||
currbudget['remaining'] = (float(currbudget.get('limit')) - float(currbudget.get('spent')))
|
||||
remaining_per_day = currbudget.get('remaining') / firefly.get_remaining_days()
|
||||
currbudget['remaining_per_day'] = 0 if remaining_per_day < 0 else remaining_per_day
|
||||
currbudget['spent_per_day'] = currbudget['spent'] / datetime.datetime.today().day
|
||||
currbudget['average_per_day'] = currbudget['limit'] / (datetime.datetime.today().day + firefly.get_remaining_days() - 1)
|
||||
return budgets
|
||||
|
||||
|
||||
def get_budget_metrics():
|
||||
out = ""
|
||||
for budget in _get_budgets():
|
||||
|
|
3
requirements-xmpp.txt
Normal file
3
requirements-xmpp.txt
Normal file
|
@ -0,0 +1,3 @@
|
|||
python-dateutil==2.8.1
|
||||
requests==2.24.0
|
||||
xmpppy==0.6.1
|
3
requirements-xmppbot.txt
Normal file
3
requirements-xmppbot.txt
Normal file
|
@ -0,0 +1,3 @@
|
|||
aioxmpp==0.11.0
|
||||
python-dateutil==2.8.2
|
||||
requests==2.28.1
|
|
@ -1,3 +1,4 @@
|
|||
Flask==1.1.2
|
||||
python-dateutil==2.8.1
|
||||
requests==2.24.0
|
||||
Flask==2.1.2
|
||||
python-dateutil==2.8.2
|
||||
requests==2.28.1
|
||||
pytz==2022.1
|
||||
|
|
109
xmppbot.py
Normal file
109
xmppbot.py
Normal file
|
@ -0,0 +1,109 @@
|
|||
import os
|
||||
import signal
|
||||
import asyncio
|
||||
|
||||
import aioxmpp
|
||||
import firefly.budgets
|
||||
|
||||
|
||||
|
||||
class BudgetBot(object):
|
||||
def message_received(self, msg):
|
||||
if msg.type_ != aioxmpp.MessageType.CHAT:
|
||||
return
|
||||
if not msg.body:
|
||||
return
|
||||
if str(msg.from_).split('/')[0] not in self.controller:
|
||||
print("Ignored message from: {}".format(msg.from_))
|
||||
return
|
||||
print("msg from: {}".format(msg.from_))
|
||||
print("got: {}".format(list(msg.body.values())[0]))
|
||||
|
||||
budget_data = firefly.budgets._collect_budget_data()
|
||||
|
||||
command = list(msg.body.values())[0]
|
||||
if command.lower() == "budgets":
|
||||
reply = "Budgets:\n"
|
||||
for budget in budget_data:
|
||||
reply += "- *{}*\n".format(budget.get('name'))
|
||||
elif command.lower().startswith("budget "):
|
||||
cmd_budget = command.split(' ')[1].lower()
|
||||
budget = [b for b in budget_data if b['name'].lower() == cmd_budget]
|
||||
if len(budget) != 1:
|
||||
reply = "Invalid budget name"
|
||||
else:
|
||||
reply = "Budget *{}*\n".format(budget[0]['name'])
|
||||
reply += " Available current month: {:.2f}\n".format(budget[0].get('limit'))
|
||||
reply += " Average per day: {:.2f}\n".format(budget[0].get('average_per_day'))
|
||||
reply += " Spent: {:.2f}\n".format(budget[0].get('spent'))
|
||||
reply += " Spent per day: {:.2f}\n".format(budget[0].get('spent_per_day'))
|
||||
reply += " Remaining: {:.2f}\n".format(budget[0].get('remaining'))
|
||||
reply += " Remaining per day: {:.2f}".format(budget[0].get('remaining_per_day'))
|
||||
else:
|
||||
reply = "Please send command:\n"
|
||||
reply += "- *budgets* list all budgets"
|
||||
|
||||
reply_msg = aioxmpp.Message(
|
||||
type_=msg.type_,
|
||||
to=msg.from_,
|
||||
)
|
||||
|
||||
reply_msg.body[None] = reply
|
||||
self.client.enqueue(reply_msg)
|
||||
|
||||
async def run_simple_example(self):
|
||||
stop_event = self.make_sigint_event()
|
||||
self.client.stream.register_message_callback(
|
||||
aioxmpp.MessageType.CHAT,
|
||||
None,
|
||||
self.message_received,
|
||||
)
|
||||
print("listening... (press Ctrl-C or send SIGTERM to stop)")
|
||||
await stop_event.wait()
|
||||
|
||||
async def run_example(self):
|
||||
self.client = self.make_simple_client()
|
||||
async with self.client.connected():
|
||||
await self.run_simple_example()
|
||||
|
||||
def make_simple_client(self):
|
||||
client = aioxmpp.PresenceManagedClient(
|
||||
aioxmpp.JID.fromstr(os.environ.get('FIREFLY_JABBER_ID')),
|
||||
aioxmpp.make_security_layer(os.environ.get('FIREFLY_JABBER_PASSWORD')),
|
||||
)
|
||||
self.roster = client.summon(aioxmpp.RosterClient)
|
||||
self.roster.on_initial_roster_received.connect(
|
||||
self._read_roster,
|
||||
)
|
||||
return client
|
||||
|
||||
def _read_roster(self):
|
||||
self.controller = []
|
||||
for item in self.roster.items.values():
|
||||
self.controller.append(str(item.jid))
|
||||
print("Roster entry: {}".format(item.jid))
|
||||
print("Subscription: {}".format(item.subscription))
|
||||
print("Approved: {}".format(item.approved))
|
||||
print("Allowed controllers: {}".format(self.controller))
|
||||
|
||||
def make_sigint_event(self):
|
||||
event = asyncio.Event()
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.add_signal_handler(
|
||||
signal.SIGINT,
|
||||
event.set,
|
||||
)
|
||||
return event
|
||||
|
||||
|
||||
def main():
|
||||
bot = BudgetBot()
|
||||
loop = asyncio.get_event_loop()
|
||||
try:
|
||||
loop.run_until_complete(bot.run_example())
|
||||
finally:
|
||||
loop.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
Loading…
Reference in a new issue