From f2a5ec399012938facbcab1af2afe6ea594bbd30 Mon Sep 17 00:00:00 2001 From: chris Date: Sun, 1 Nov 2020 23:22:13 +0100 Subject: [PATCH] first bot draft --- README.md | 44 ++++++++++++++++ requirements-xmppbot.txt | 1 + xmppbot.py | 107 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 152 insertions(+) create mode 100644 xmppbot.py diff --git a/README.md b/README.md index 61c8072..80942b1 100644 --- a/README.md +++ b/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 ` - 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 diff --git a/requirements-xmppbot.txt b/requirements-xmppbot.txt index c54999a..3a888b2 100644 --- a/requirements-xmppbot.txt +++ b/requirements-xmppbot.txt @@ -1,2 +1,3 @@ aioxmpp==0.11.0 requests==2.24.0 +python-dateutil==2.8.1 diff --git a/xmppbot.py b/xmppbot.py new file mode 100644 index 0000000..6d047cf --- /dev/null +++ b/xmppbot.py @@ -0,0 +1,107 @@ +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 += " Limit: {}\n".format(budget[0].get('limit')) + reply += " Spent: {:.2f}\n".format(budget[0].get('spent')) + 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()