commit b063bbd35039216cc14a5a5419b9c56b61d65d1b Author: Roger Gonzalez Date: Mon Aug 1 21:52:56 2022 -0300 Initial commit diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..2912d3c --- /dev/null +++ b/.env.example @@ -0,0 +1,21 @@ +MATRIX_URL="https://my-matrix-url" +MATRIX_USER="@botusername:my.server" +MATRIX_PASSWORD="mybotpassword" +MATRIX_USERNAME="myownusername" +BANK_ACCOUNT_NUMBERS="bankaccount1,bankaccount2" +BROU_USERNAME="brou_username" +BROU_PASSWORD="mybroupassword69420" +ITAU_USERNAME="itau_username" +ITAU_PASSWORD="myitaupassword69420" + +EXPENSES_FILENAME="my/expenses/filename.org" + +NEXTCLOUD_URL="https://my-nextcloud-url.com" +NEXTCLOUD_USERNAME="mynextcloudusername" +NEXTCLOUD_PASSWORD="mynextcloudpassword" + +ORG_CAPTURE_FILENAME="my/capture/filename.org" +ORG_PLAN_FILENAME="my/plan-{filename}.org" + +PROMETEO_API_KEY="myprometeoapikey" +PROMETEO_URL='https://myprometeourl.com' diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..c836869 --- /dev/null +++ b/.flake8 @@ -0,0 +1,2 @@ +[flake8] +max-line-length = 110 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d213028 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +.DS_Store +.idea +*.log +tmp/ + +*.py[cod] +*.egg +build +htmlcov + +session.txt +.env diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..d22621d --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,14 @@ +repos: +- repo: https://github.com/timothycrosley/isort + rev: 5.9.3 + hooks: + - id: isort +- repo: https://github.com/ambv/black + rev: 22.3.0 + hooks: + - id: black + language_version: python3 +- repo: https://gitlab.com/pycqa/flake8 + rev: 3.9.2 + hooks: + - id: flake8 diff --git a/bofa.py b/bofa.py new file mode 100644 index 0000000..3d47411 --- /dev/null +++ b/bofa.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 + +from pyexcel_ods3 import get_data + +from nextcloud import NextCloudConnection +from settings import EXPENSES_FILENAME + + +class BofaData(NextCloudConnection): + def get(self) -> None: + with self.client.open(EXPENSES_FILENAME, mode="rb") as expenses: + data = get_data(expenses) + main_page = data.get("Main") + + headlines = main_page[0] + amounts = main_page[1] + + formatted_data = {} + + for i in range(len(headlines)): + formatted_data[headlines[i]] = amounts[i] + + return formatted_data diff --git a/bot.py b/bot.py new file mode 100644 index 0000000..6480c24 --- /dev/null +++ b/bot.py @@ -0,0 +1,155 @@ +import simplematrixbotlib as botlib + +from bofa import BofaData +from org import OrgData +from prometeo import get_bank_information +from settings import ( + BANK_ACCOUNT_NUMBERS, + BROU_PASSWORD, + BROU_USERNAME, + ITAU_PASSWORD, + ITAU_USERNAME, + MATRIX_PASSWORD, + MATRIX_URL, + MATRIX_USER, + MATRIX_USERNAME, +) + +creds = botlib.Creds(MATRIX_URL, MATRIX_USER, MATRIX_PASSWORD) +bot = botlib.Bot(creds) + +PREFIX = "!" + + +@bot.listener.on_message_event +async def todo(room, message): + """ + Function that adds new TODOs + Usage: + user: !todo title - objective - extra (optional) + bot: TODO added! + """ + match = botlib.MessageMatch(room, message, bot, PREFIX) + if match.is_not_from_this_bot() and match.prefix() and match.command("todo"): + user = message.sender + + if user == MATRIX_USERNAME: + message = " ".join(message.body.split(" ")[1:]) + room_id = room.room_id + splitted_message = message.split("-") + + try: + todo_title = splitted_message[0].strip() + todo_objective = splitted_message[1].strip() + except IndexError: + return await bot.api.send_text_message( + room_id, + "An objective is needed. The correct format is 'Title - Objective - Extra (optional)'", + ) + + try: + todo_extra = splitted_message[2].strip() + except IndexError: + todo_extra = "" + + print(f"Room: {room_id}, User: {user}, Message: {message}") + OrgData().add_new_todo(todo_title, todo_objective, todo_extra) + await bot.api.send_text_message(room_id, "TODO added!") + + +@bot.listener.on_message_event +async def list_todos(room, message): + """ + Function that lists today's plan + Usage: + user: !list [free,work] + bot: [prints a list with today's todos] + """ + match = botlib.MessageMatch(room, message, bot, PREFIX) + if match.is_not_from_this_bot() and match.prefix() and match.command("list"): + user = message.sender + + if user == MATRIX_USERNAME: + message = " ".join(message.body.split(" ")[1:]).strip() or "free" + room_id = room.room_id + + if message not in ["free", "work"]: + return await bot.api.send_text_message( + room_id, + f'"{message}" not accepted. Accepted options are "free" and "work"', + ) + + print(f"Room: {room_id}, User: {user}, Message: {message}") + plan = OrgData().list_plan(message) + await bot.api.send_text_message(room_id, plan) + + +@bot.listener.on_message_event +async def list_bofa_status(room, message): + """ + Function that lists bofa status + Usage: + user: !bofa + bot: [prints bofa current status] + """ + match = botlib.MessageMatch(room, message, bot, PREFIX) + if match.is_not_from_this_bot() and match.prefix() and match.command("bofa"): + user = message.sender + + if user == MATRIX_USERNAME: + room_id = room.room_id + + print(f"Room: {room_id}, User: {user}, Message: bofa") + + bofa_data = BofaData().get() + + return_data = "" + for person, amount in bofa_data.items(): + if amount != "0 USD": + return_data += f"{person}: {amount}\n" + + await bot.api.send_text_message(room_id, return_data) + + +def generate_return_data_from_account(bank: str, accounts: list) -> str: + bank_message = f"{bank.upper()}\nNot available. Try again later!\n" + + for account in accounts: + if account.get("number", "") in BANK_ACCOUNT_NUMBERS: + account_number = account.get("number", "") + balance = account.get("balance", "") + + bank_message = f"{bank.upper()}\nAccount Number: {account_number}\nBalance: {balance} USD\n" + return bank_message + + +@bot.listener.on_message_event +async def list_bank_information(room, message): + """ + Function that lists banks information + Usage: + user: !banks + bot: [prints current status of banks] + """ + match = botlib.MessageMatch(room, message, bot, PREFIX) + if match.is_not_from_this_bot() and match.prefix() and match.command("banks"): + user = message.sender + + if user == MATRIX_USERNAME: + room_id = room.room_id + + print(f"Room: {room_id}, User: {user}, Message: banks") + await bot.api.send_text_message(room_id, "Looking for bank data, just a sec...") + + brou_accounts = get_bank_information("brou", BROU_USERNAME, BROU_PASSWORD) + itau_accounts = get_bank_information("itau", ITAU_USERNAME, ITAU_PASSWORD) + + return_data = "" + return_data += generate_return_data_from_account("brou", brou_accounts) + return_data += "\n" + return_data += generate_return_data_from_account("itau", itau_accounts) + + await bot.api.send_text_message(room_id, return_data) + + +bot.run() diff --git a/nextcloud.py b/nextcloud.py new file mode 100644 index 0000000..5a530df --- /dev/null +++ b/nextcloud.py @@ -0,0 +1,8 @@ +from webdav4.client import Client + +from settings import NEXTCLOUD_PASSWORD, NEXTCLOUD_URL, NEXTCLOUD_USERNAME + + +class NextCloudConnection: + def __init__(self): + self.client = Client(NEXTCLOUD_URL, auth=(NEXTCLOUD_USERNAME, NEXTCLOUD_PASSWORD)) diff --git a/org.py b/org.py new file mode 100644 index 0000000..c3a802b --- /dev/null +++ b/org.py @@ -0,0 +1,51 @@ +import locale +import os +from datetime import datetime + +from orgparse import loads + +from nextcloud import NextCloudConnection +from settings import ORG_CAPTURE_FILENAME, ORG_PLAN_FILENAME + +locale.setlocale(locale.LC_ALL, "es_ES.utf8") + + +class OrgData(NextCloudConnection): + def _generate_today(self): + today = datetime.today() + today_ymd = today.strftime("%Y-%m-%d") + today_day = today.strftime("%a").lower() + today_hour = today.strftime("%H:%M") + return f"{today_ymd} {today_day} {today_hour}" + + def add_new_todo(self, description: str, outcome: str, extra: str) -> None: + + self.client.download_file(ORG_CAPTURE_FILENAME, "./capture.org") + + today = self._generate_today() + + todo_template = f""" +** TODO {description} :NEW: :BOT: + Desired outcome: {outcome} + :LOGBOOK: + - Added: [{today}] + :END: + + {extra} + +""" + + with open("./capture.org", "a") as capture_file: + capture_file.write(todo_template) + + self.client.upload_file("./capture.org", ORG_CAPTURE_FILENAME, overwrite=True) + + os.remove("./capture.org") + + def list_plan(self, filename: str) -> str: + with self.client.open(ORG_PLAN_FILENAME.replace("{filename}", filename), mode="r") as agenda: + plan = agenda.read() + + plan = loads(plan) + + return plan[-1].get_body() diff --git a/prometeo.py b/prometeo.py new file mode 100644 index 0000000..58bd4f8 --- /dev/null +++ b/prometeo.py @@ -0,0 +1,28 @@ +import requests + +from settings import PROMETEO_API_KEY, PROMETEO_URL + + +def get_bank_information(bank: str, username: str, password: str) -> list: + headers = { + "Accept": "application/json", + "Content-Type": "application/x-www-form-urlencoded", + "X-API-Key": PROMETEO_API_KEY, + } + + login = requests.post( + f"{PROMETEO_URL}/login/", + data={ + "provider": bank, + "username": username, + "password": password, + }, + headers=headers, + ) + + key = login.json().get("key") + + account_data = requests.get(f"{PROMETEO_URL}/account/?key={key}", headers=headers).json() + requests.get(f"{PROMETEO_URL}/logout/?key={key}", headers=headers) + + return account_data.get("accounts") diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..d119c32 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,40 @@ +[tool.black] +line-length = 121 +target-version = ['py37', 'py38', 'py310'] +include = '\.pyi?$' +# regex below includes default list from isort, for parity +exclude = ''' +/( + \.bzr + | \.direnv + | \.eggs + | \.git + | \.hg + | \.mypy_cache + | \.nox + | \.pants\.d + | \.svn + | \.tox + | \.venv + | _build + | buck-out + | build + | dist + | node_modules + | venv + | \.idea + | dockerdata + | static +)/ +''' + +[tool.isort] +# these are black-compatible settings +multi_line_output = 3 +include_trailing_comma = true +force_grid_wrap = 0 +use_parentheses = true +ensure_newline_before_comments = true +line_length = 121 +skip = "dockerdata,.idea,static" +filter_files = true diff --git a/settings.py b/settings.py new file mode 100644 index 0000000..1f3819a --- /dev/null +++ b/settings.py @@ -0,0 +1,29 @@ +import os + +from dotenv import load_dotenv + +load_dotenv() + +MATRIX_URL = os.environ.get("MATRIX_URL") +MATRIX_USER = os.environ.get("MATRIX_USER") +MATRIX_PASSWORD = os.environ.get("MATRIX_PASSWORD") +MATRIX_USERNAME = os.environ.get("MATRIX_USERNAME") + +BANK_ACCOUNT_NUMBERS = os.environ.get("BANK_ACCOUNT_NUMBERS").split(",") + +BROU_USERNAME = os.environ.get("BROU_USERNAME") +BROU_PASSWORD = os.environ.get("BROU_PASSWORD") +ITAU_USERNAME = os.environ.get("ITAU_USERNAME") +ITAU_PASSWORD = os.environ.get("ITAU_PASSWORD") + +EXPENSES_FILENAME = os.environ.get("EXPENSES_FILENAME") + +NEXTCLOUD_URL = os.environ.get("NEXTCLOUD_URL") +NEXTCLOUD_USERNAME = os.environ.get("NEXTCLOUD_USERNAME") +NEXTCLOUD_PASSWORD = os.environ.get("NEXTCLOUD_PASSWORD") + +ORG_CAPTURE_FILENAME = os.environ.get("ORG_CAPTURE_FILENAME") +ORG_PLAN_FILENAME = os.environ.get("ORG_PLAN_FILENAME") + +PROMETEO_API_KEY = os.environ.get("PROMETEO_API_KEY") +PROMETEO_URL = os.environ.get("PROMETEO_URL")