From 634a8c2d6eda53230b77b0af61747f670c0fb1b7 Mon Sep 17 00:00:00 2001 From: portbusy Date: Sat, 23 Jul 2022 19:02:52 +0200 Subject: [PATCH 1/4] UPDATE: .gitignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 4fac93c..72d702b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ token.txt *.sqlite -master.txt +.idea +venv From d4fa3aaa3b8d1513e23e70fb0b8d59bf0530f299 Mon Sep 17 00:00:00 2001 From: portbusy Date: Sat, 23 Jul 2022 19:03:11 +0200 Subject: [PATCH 2/4] UPDATE: major code refactoring --- .idea/dbnavigator.xml | 458 +++++++++++++++++++++++++++ .idea/misc.xml | 2 +- .idea/moneyTrackerBot.iml | 4 +- README.md | 7 +- __main__.py | 36 +++ bot.py | 123 ------- config/application.ini | 2 + config/log.yaml | 25 ++ configurator/__init__.py | 1 + configurator/config.py | 134 ++++++++ configurator/exceptions.py | 31 ++ db_manager/__init__.py | 1 + dbHelper.py => db_manager/manager.py | 10 +- messageHandler.py | 134 -------- models/replies.py | 40 +++ requirements.txt | 6 +- telegram/__init__.py | 0 telegram/bot.py | 127 ++++++++ telegram/configs.py | 3 + 19 files changed, 877 insertions(+), 267 deletions(-) create mode 100644 .idea/dbnavigator.xml create mode 100644 __main__.py delete mode 100644 bot.py create mode 100644 config/application.ini create mode 100644 config/log.yaml create mode 100644 configurator/__init__.py create mode 100644 configurator/config.py create mode 100644 configurator/exceptions.py create mode 100644 db_manager/__init__.py rename dbHelper.py => db_manager/manager.py (94%) delete mode 100644 messageHandler.py create mode 100644 models/replies.py create mode 100644 telegram/__init__.py create mode 100644 telegram/bot.py create mode 100644 telegram/configs.py diff --git a/.idea/dbnavigator.xml b/.idea/dbnavigator.xml new file mode 100644 index 0000000..bb7748f --- /dev/null +++ b/.idea/dbnavigator.xml @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index a2e120d..c2e401f 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/.idea/moneyTrackerBot.iml b/.idea/moneyTrackerBot.iml index d0876a7..74d515a 100644 --- a/.idea/moneyTrackerBot.iml +++ b/.idea/moneyTrackerBot.iml @@ -1,7 +1,9 @@ - + + + diff --git a/README.md b/README.md index db90296..bb59779 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ # Description + This is the first version of my telegram bot which allows a simple expenses tracker. The bot can now take income and outcome and display the current month resume; here is a list of commands: + * `/listoutcome` print the list of current month outcome * `/listincome` print the list of current month income * `/delete +/- ` delete the specified entry @@ -10,8 +12,9 @@ The bot can now take income and outcome and display the current month resume; he * `/outcome ` add to the outcome table the amount and the comment associated ## Installation -* First create the `master.txt` and `token.txt` files, filled respectively with your master chat id - and telegram bot token + +* First create the `master.txt` and `token.txt` files, filled respectively with your master chat id + and telegram bot token * `Install docker` * Go into the clone folder and run `docker build moneytrakerBot/` * Copy the container id diff --git a/__main__.py b/__main__.py new file mode 100644 index 0000000..baf2a9a --- /dev/null +++ b/__main__.py @@ -0,0 +1,36 @@ +import argparse +import logging.config +import os + +import yaml + +from configurator import Config + +LOGGER = logging.getLogger(__name__) + +if __name__ == '__main__': + parser = argparse.ArgumentParser(prog='vlc-backend', description='vlc-backend application') + + parser.add_argument('-c', '--config_dir', metavar='configuration_directory', + required=False, default="config", type=str, + help='Configuration directory to use; ' + 'if not specified, default config directory will be used') + + # Configuration file + parser.add_argument('-f', '--file_app', metavar='configuration_file', + required=False, default="application.ini", type=str, + help='Configuration file to use; ' + 'if not specified, default configs will be used') + + cmd_args = parser.parse_args() + + with open(os.path.join(cmd_args.config_dir, "log.yaml"), 'rt') as f: + log_file = yaml.safe_load(f.read()) + logging.config.dictConfig(log_file) + LOGGER.info(f"Loaded logging configuration from log.yaml") + + Config.init(cmd_args.config_dir, cmd_args.file_app) + + from telegram.bot import MoneyTrackerBot + bot = MoneyTrackerBot() + bot.start() \ No newline at end of file diff --git a/bot.py b/bot.py deleted file mode 100644 index c2ba427..0000000 --- a/bot.py +++ /dev/null @@ -1,123 +0,0 @@ -from datetime import datetime -import logging -from dbHelper import DBHelper -from messageHandler import MessageHandler - - -logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.DEBUG) - -db = DBHelper() - - -handler = MessageHandler() -messages = [ - "/start", - "/delete" -] -reply = { - "/start": "Hi! I will help you keep track of your expenses :) \nYou can simply add an income just typing " - "'+value comment' or an outcome typing '-value comment'", - "/delete": "With this command you can delete a wrong entry, \nSimply type /delete +/-value comment." - "\nIf you do not remember the comment just type /listincome or /listoutcome" -} - -# i found those code here https://apps.timwhitlock.info/emoji/tables/unicode :) -emoji = { - "moneybag": u'\U0001F4B0', - "moneywings": u'\U0001F4B8', - "openhands": u'\U0001F450' -} - - -def add_income(value, comment): - date = datetime.now().strftime("%Y-%m-%d") - db.add_income(date, value, comment) - - -def delete_entry(text): - value = text[1:text.index(" ")] - comment = text[text.index(" ") + 1:len(text)] - if text.startswith("+"): - db.delete_income(value, comment) - elif text.startswith("-"): - db.delete_outcome(value, comment) - - -def add_outcome(value, comment): - date = datetime.now().strftime("%Y-%m-%d") - db.add_outcome(date, value, comment) - - -def text_handler(text, chat_id): - message = "" # to avoid reference before assignment error - if text in messages: - handler.send_message(reply[text], chat_id) - elif text.startswith("+"): - add_income(text[1:text.index(" ")], text[text.index(" ")+1:len(text)]) - message = "Income successfully registered "+emoji["moneybag"] - elif text.startswith("-"): - add_outcome(text[1:text.index(" ")], text[text.index(" ")+1:len(text)]) - message = "Outcome successfully registered " + emoji["moneywings"] - elif text.startswith("/delete"): - text = text.replace('/delete ', '') - delete_entry(text) - message = "Entry removed successfully" - elif text == "/listincome": - month = datetime.now().strftime("%m") - rows = db.get_income(month) - logging.info(rows) - if rows: - message = "Current month income list:\n\n" - for r in rows: - message = message + str(r).replace("(", "").replace(")", "").replace("'", "") + "\n" - total_income = db.get_total_income(month) - message = message+"\n\nTotal income: "+str(total_income)+" €" - else: - message = "No income to be displayed here " + emoji["openhands"] - elif text == "/listoutcome": - month = datetime.now().strftime("%m") - rows = db.get_outcome(month) - logging.info(rows) - if rows: - message = "Current month outcome list:\n\n" - for r in rows: - message = message+str(r).replace("(", "").replace(")", "").replace("'", "")+"\n" - total_outcome = db.get_total_outcome(month) - message = message+"\n\nTotal outcomecome: "+str(total_outcome)+" €" - else: - message = "No income to be displayed here " + emoji["openhands"] - elif text == "/balance": - month = datetime.now().strftime("%m") - total_income = db.get_total_income(month) - if total_income is None: - total_income = 0.0 - total_outcome = db.get_total_outcome(month) - if total_outcome is None: - total_outcome = 0.0 - logging.info(total_outcome) - balance = total_income - total_outcome - message = "Current month balance: "+str(balance)+" €\n\n\nTotal income: "+str(total_income)+" €\n\nTotal outcome: "+str(total_outcome)+" €" - handler.send_message(message, chat_id) - - -def main(): - db.setup() - last_update_id = None - while True: - updates = handler.get_updates(last_update_id) - if not updates: - logging.info("no updates found") - else: - if len(updates["result"]) > 0: - last_update_id = handler.get_last_update_id(updates) + 1 - logging.debug(last_update_id) - if handler.id_check(updates): - text, chat_id = handler.get_text_and_chat(updates) - name = handler.get_name(updates) - logging.debug("Message: {} From: {}".format(text, chat_id)) - text_handler(text, chat_id) - - -if __name__ == '__main__': - main() - diff --git a/config/application.ini b/config/application.ini new file mode 100644 index 0000000..208c937 --- /dev/null +++ b/config/application.ini @@ -0,0 +1,2 @@ +[TELEGRAM] +#token_position=config/token.txt \ No newline at end of file diff --git a/config/log.yaml b/config/log.yaml new file mode 100644 index 0000000..272507f --- /dev/null +++ b/config/log.yaml @@ -0,0 +1,25 @@ +version: 1 +disable_existing_loggers: False +formatters: + simple: + format: "%(levelname) -10s %(asctime)s %(name) -30s %(funcName) -20s %(lineno) -5d: %(message)s" + +handlers: + console: + class: logging.StreamHandler + formatter: simple + stream: ext://sys.stdout + +loggers: + __main__: + level: DEBUG + handlers: [ console ] + propagate: no + telegram: + level: DEBUG + handlers: [ console ] + propagate: no + db_manager: + level: DEBUG + handlers: [console] + propagate: no \ No newline at end of file diff --git a/configurator/__init__.py b/configurator/__init__.py new file mode 100644 index 0000000..cca5d9b --- /dev/null +++ b/configurator/__init__.py @@ -0,0 +1 @@ +from .config import Config diff --git a/configurator/config.py b/configurator/config.py new file mode 100644 index 0000000..d04a70b --- /dev/null +++ b/configurator/config.py @@ -0,0 +1,134 @@ +import collections +import configparser +import distutils.util +import logging +import os + +import expandvars + +from . import exceptions + +LOGGER = logging.getLogger(__name__) + +_UNSET = object() + + +class Config: + class ConfigInterpolation(configparser.BasicInterpolation): + + def before_get(self, parser, section, option, value, defaults): + return expandvars.expandvars(value) + + instances = list() + parser = configparser.ConfigParser(interpolation=ConfigInterpolation()) + directory = None + file_names = None + + def __init__(self, data_type, section, option=None, fallback=_UNSET): + self.__class__.instances.append(self) + self._data_type = data_type + self._is_none = False + self._section = section + self._option = option + self._fallback = fallback + self._gen_value = _UNSET + + @staticmethod + def init(directory, *file_names, ignore_error=False): + # Set the directory of the configuration files to use + Config.directory = directory + # Load the configuration files + Config.load(*file_names, ignore_error=ignore_error) + + @staticmethod + def path(target=""): + yield os.path.join(Config.directory, target) + + @staticmethod + def dir(target=""): + return os.path.join(Config.directory, target) + + @staticmethod + def load(*file_names, encoding=None, ignore_error=False): + # If configuration should only be reloaded + if not file_names: file_names = Config.file_names + # Set file names + Config.file_names = file_names + # Build the real path of each configuration file + config_file_paths = [os.path.join(Config.directory, file_name) for file_name in file_names] + # Check if the file exists + valid_file_paths = [] + for config_file_path in config_file_paths: + if not os.path.isfile(config_file_path): + if not ignore_error: + raise exceptions.ConfigFileDoesNotExists(config_file_path) + else: + valid_file_paths.append(config_file_path) + # Parse and read the configurations + Config.parser.read(valid_file_paths, encoding) + + def get(self, option=None, **kwargs): + if self._is_none: + return None + option = self._option if option is None else option + if self._fallback is _UNSET: + value = Config.parser.get(self._section, option) + else: + value = Config.parser.get(self._section, option, fallback=self._fallback) + if value == '': + value = self._fallback + # Check the value + if value is None: + return None + if isinstance(value, collections.Generator): + if self._gen_value is _UNSET: + self._gen_value = next(value) + return self._gen_value + # Check data type + if isinstance(self._data_type, type): + if self._data_type == bool: + if isinstance(value, bool): + return value + elif isinstance(value, str): + return bool(distutils.util.strtobool(value)) + elif self._data_type == str and kwargs: + value = str(value).format(**kwargs) + elif isinstance(self._data_type, tuple) and isinstance(value, str): + collection, data_type = self._data_type + if collection == list: + return [data_type(v) for v in value.split(",")] + # Return the casted value + return self._data_type(value) + + def set(self, value): + if value is not None: + if not isinstance(value, self._data_type): + raise exceptions.ValueTypeNotAllowed(self._option, type(self._data_type).__name__) + self._is_none = False + value = self._data_type(value) + else: + self._is_none = True + return + if self._section not in Config.parser.sections(): + Config.parser.add_section(self._section) + try: + Config.parser.set(self._section, self._option, str(value)) + except Exception as e: + raise exceptions.SetValueError(value, str(e)) + + @staticmethod + def update(data): + for section, data in data.items(): + if section not in Config.parser.sections(): + Config.parser.add_section(section) + for option, value in data: + Config.parser.set(section, option, value) + + @staticmethod + def save(filename): + with open(Config.dir(filename), 'w') as f: + Config.parser.write(f) + + @staticmethod + def data(): + return {s: Config.parser.items(s) for s in Config.parser.sections()} diff --git a/configurator/exceptions.py b/configurator/exceptions.py new file mode 100644 index 0000000..e10bb06 --- /dev/null +++ b/configurator/exceptions.py @@ -0,0 +1,31 @@ +class ConfiguratorError(Exception): + """Raised when an Configurator error occurs.""" + + def __init__(self, message="An unspecified Configurator error has occurred"): + super().__init__(message) + + +class ConfigFileDoesNotExists(ConfiguratorError): + + def __init__(self, config_file_path): + super().__init__("Config file '{c}' does not exist".format(**{ + "c": config_file_path + })) + + +class ValueTypeNotAllowed(ConfiguratorError): + + def __init__(self, option, data_type): + super().__init__("Value of '{o}' must be None or type '{t}'".format(**{ + "o": option, + "t": data_type + })) + + +class SetValueError(ConfiguratorError): + + def __init__(self, value, reason): + super().__init__("Could not set the value '{v}'. Reason: {r}".format(**{ + "v": value, + "r": reason + })) diff --git a/db_manager/__init__.py b/db_manager/__init__.py new file mode 100644 index 0000000..decafc3 --- /dev/null +++ b/db_manager/__init__.py @@ -0,0 +1 @@ +from .manager import DatabaseManager diff --git a/dbHelper.py b/db_manager/manager.py similarity index 94% rename from dbHelper.py rename to db_manager/manager.py index 759f5be..d974101 100644 --- a/dbHelper.py +++ b/db_manager/manager.py @@ -1,7 +1,8 @@ import sqlite3 +import typing -class DBHelper: +class DatabaseManager: def __init__(self, dbname="expenses.sqlite"): self.dbname = dbname self.conn = sqlite3.connect(dbname) @@ -37,9 +38,9 @@ def delete_outcome(self, value, comment): self.conn.execute(stmt, args) self.conn.commit() - def get_income(self, month): + def get_income(self, month) -> typing.List: cur = self.conn.cursor() - stmt = "SELECT * FROM income WHERE strftime('%m', date) = '" +month+ "'" + stmt = "SELECT * FROM income WHERE strftime('%m', date) = '" + month + "'" cur.execute(stmt) rows = cur.fetchall() return rows @@ -53,7 +54,7 @@ def get_total_income(self, month): def get_outcome(self, month): cur = self.conn.cursor() - stmt = "SELECT * FROM outcome WHERE strftime('%m', date) = '"+month+"'" + stmt = "SELECT * FROM outcome WHERE strftime('%m', date) = '" + month + "'" cur.execute(stmt) rows = cur.fetchall() return rows @@ -64,4 +65,3 @@ def get_total_outcome(self, month): cur.execute(stmt) total = cur.fetchone() return total[0] - diff --git a/messageHandler.py b/messageHandler.py deleted file mode 100644 index 2e0a02a..0000000 --- a/messageHandler.py +++ /dev/null @@ -1,134 +0,0 @@ -import requests -import json -import logging -from datetime import datetime -from urllib import parse -from sys import exit - -logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.INFO) - -f = open("token.txt", "r") -TOKEN = f.readline() -if not TOKEN: - logging.error("Error occurred, have you filled the token.txt file with your bot token?") - exit() - -URL = "https://api.telegram.org/bot{}/".format(TOKEN) - -f = open("master.txt", "r") -master = int(f.readline()) -if not master: - logging.error("Error occurred, have you filled the master.txt file with your master id?") - exit() - - -class MessageHandler: - - def __init__(self): - self.master = master - self.allowed = [self.master] - - # - def get_url(self, url): - try: - response = requests.get(url) - content = response.content.decode("utf8") - except requests.exceptions.ConnectionError: - logging.info("Max retries exceed") - content = "" - return content - - # - def get_json_from_url(self, url): - try: - content = self.get_url(url) - js = json.loads(content) - except AttributeError: - event_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') - logging.error("\nFailed to load json content at {}, content was {}\n".format(event_time, self.get_url(url))) - js = [] - return js - - # - def get_updates(self, offset=None): - url = URL + "getUpdates?timeout=1" - if offset: - url += "&offset={}".format(offset) - js = self.get_json_from_url(url) - return js - - # - def get_last_update_id(self, updates): - update_ids = [] - for update in updates["result"]: - update_ids.append(int(update["update_id"])) - return max(update_ids) - - # - def send_message(self, text, chat_id, reply_markup=None): - text = parse.quote_plus(text) - url = URL + "SendMessage?text={}&chat_id={}&parse_mode=Markdown".format(text, chat_id) - if reply_markup: - url += "&reply_markup={}".format(reply_markup) - self.get_url(url) - - # - def get_text_and_chat(self, updates): - len_updates = len(updates["result"]) - last_update = len_updates - 1 - try: - text = updates["result"][last_update]["message"]["text"] - except: - text = "no valid text" - logging.error("no valid text") - chat_id = updates["result"][last_update]["message"]["chat"]["id"] - return text, chat_id - - # - def get_name(self, updates): - for update in updates["result"]: - chat = update["message"]["chat"]["id"] - try: - name = update["message"]["chat"]["first_name"] - except: - # write_log2("no_name", time) - name = "n/a" - try: - surname = update["message"]["chat"]["last_name"] - except: - # write_log2("no_surname", time) - surname = "n/a" - return name - - # - def id_check(self, updates): - for update in updates["result"]: - chat = update["message"]["chat"]["id"] - logging.info("chat: {}, allowed: {}".format(chat, self.allowed)) - date = update["message"]["date"] - time = datetime.fromtimestamp(date) - time = time.strftime('%Y-%m-%d at %H:%M:%S') - try: - name = update["message"]["chat"]["first_name"] - except: - name = "n/a" - try: - surname = update["message"]["chat"]["last_name"] - except: - surname = "n/a" - try: - username = update["message"]["chat"]["username"] - except: - username = "n/a" - - if chat in self.allowed: - #logging.info("\nconnection from: {} ... \nconnection successful".format(chat)) - return 1 - else: - self.send_message("Unknown user, access denied. Contact system admin", chat) - message = [name, " ", surname, "\nUsername: ", username, "\nID: ", chat, "\nAt: ", str(time), - "Concedere i privilegi all'utente?"] - message = ''.join(map(str, message)) - keyboard = [[chat], ["Home"]] - self.send_message(message, self.master, keyboard) - return 0 \ No newline at end of file diff --git a/models/replies.py b/models/replies.py new file mode 100644 index 0000000..2da79e7 --- /dev/null +++ b/models/replies.py @@ -0,0 +1,40 @@ +import typing +from typing import Dict + + +class Replies: + def __init__(self): + self.emojis = None + self.balance = None + self.no_outcome_found = None + self.no_income_found = None + self.add_outcome_success = None + self.list_income = None + self.add_income_success = None + self.delete = None + self.start = None + + def init(self): + self.start: str = "Hi! I will help you keep track of your expenses :) \nYou can simply add an income just " \ + "typing '+value comment' or an outcome typing '-value comment'" + self.delete: str = "With this command you can delete a wrong entry, \nSimply type /delete +/-value comment." \ + "\nIf you do not remember the comment just type /listincome or /listoutcome" + + self.add_income_success: str = f'Income successfully registered {self.get_emoji_unicode("moneybag")}' + self.add_outcome_success: str = f'Outcome successfully registered {self.get_emoji_unicode("moneywings")}' + self.list_income: str = f'Current month income list:\n\n' + self.no_income_found: str = f'No income to be displayed here {self.get_emoji_unicode("openhands")}' + self.no_outcome_found: str = f'No outcome to be displayed here {self.get_emoji_unicode("openhands")}' + self.balance: str = "Current month balance: {0} €\n\n\nTotal income: {1} €\n\nTotal outcome: {2} €" + self.emojis: Dict = { + "moneybag": u'\U0001F4B0', + "moneywings": u'\U0001F4B8', + "openhands": u'\U0001F450' + } + + @property + def emoji(self): + return self.emojis + + def get_emoji_unicode(self, emoji_name: str) -> typing.Union[str, None]: + return self.emoji.get(emoji_name, None) diff --git a/requirements.txt b/requirements.txt index 898b488..401cff0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,6 @@ requests==2.22.0 -urllib3==1.24.2 \ No newline at end of file +urllib3==1.24.2 +pyTelegramBotAPI +telebot~=0.0.4 +PyYAML~=6.0 +expandvars \ No newline at end of file diff --git a/telegram/__init__.py b/telegram/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/telegram/bot.py b/telegram/bot.py new file mode 100644 index 0000000..62e1321 --- /dev/null +++ b/telegram/bot.py @@ -0,0 +1,127 @@ +import logging +import sys +from datetime import datetime + +from telebot import TeleBot +from telebot.types import Message + +from db_manager import DatabaseManager +from models.replies import Replies +from . import configs + +LOGGER = logging.getLogger(__name__) + + +def _retrieve_token(token_path: str = configs.DEFAULT_TOKEN_POSITION.get()) -> str: + try: + with open(token_path, "r") as token_file: + _token = token_file.readline() + if not _token: + LOGGER.error(f'The provided file was empty, check the file again') + sys.exit(0) + return _token + except FileNotFoundError: + LOGGER.error(f'File {token_path} not found') + sys.exit(0) + + +class MoneyTrackerBot: + + _bot = TeleBot(token=_retrieve_token()) + + def __init__(self): + self._replies = Replies() + self._db = DatabaseManager() + self._last_update_id = None + + @staticmethod + def split_expense_message(message: str): + """Split the message sent by the user with the format '+/-expense_value comment'""" + value = message[1:message.index(" ")] + comment = message[message.index(" ") + 1:len(message)] + return value, comment + + @staticmethod + def format_expense_row(row): + row = f"{row}".replace("(", "").replace(")", "").replace("'", "") + return f"{row}\n" + + @_bot.message_handler(commands=['start', 'help']) + def welcome_message(self, message: Message): + """Handles 'start' and 'help' commands""" + self._bot.reply_to(message, self._replies.start) + + @_bot.message_handler(regexp="+\d{1,}\s\w{1,}") + def add_income(self, message: Message): + """Handles messages that are income-related""" + income_value, income_comment = self.split_expense_message(message.text) + self._db.add_income(message.date, income_value, income_comment) + self._bot.reply_to(message, self._replies.add_income_success) + + @_bot.message_handler(regexp="-\d{1,}\s\w{1,}") + def add_outcome(self, message: Message): + """Handles messages that are outcome-related""" + outcome_value, outcome_comment = self.split_expense_message(message.text) + self._db.add_outcome(message.date, outcome_value, outcome_comment) + self._bot.reply_to(message, self._replies.add_outcome_success) + + @_bot.message_handler(commands=['listincome']) + def list_income(self, message: Message): + """List all the income for the current month""" + # TODO: add a method to list the income for a specific month + current_month = datetime.now().strftime("%m") + rows = self._db.get_income(current_month) + LOGGER.debug(f'Fetched entries: {rows}') + if rows: + income_list: str = "" + for row in rows: + income_list += self.format_expense_row(row) + month_total_income = self._db.get_total_income(current_month) + LOGGER.debug(f'Month total: {month_total_income}, formatted income list: {income_list}') + self._bot.reply_to(message, f'{self._replies.list_income}\n\n{income_list}\n\n' + f'Total income: {month_total_income} €') + else: + self._bot.reply_to(message, self._replies.no_income_found) + + @_bot.message_handler(commands=['listoutcome']) + def list_outcome(self, message: Message): + """List all the outcome for the current month""" + # TODO: add a method to list the outcome for a specific month + current_month = datetime.now().strftime("%m") + rows = self._db.get_outcome(current_month) + LOGGER.debug(f'Fetched entries: {rows}') + if rows: + outcome_list: str = "" + for row in rows: + outcome_list += self.format_expense_row(row) + month_total_outcome = self._db.get_total_outcome(current_month) + LOGGER.debug(f'Month total: {month_total_outcome}, formatted income list: {outcome_list}') + self._bot.reply_to(message, f'{self._replies.list_income}\n\n{outcome_list}\n\n' + f'Total income: {month_total_outcome} €') + else: + self._bot.reply_to(message, self._replies.no_outcome_found) + + @_bot.message_handler(commands=['balance']) + def balance(self, message: Message): + current_month = datetime.now().strftime("%m") + month_total_outcome = self._db.get_total_outcome(current_month) + month_total_income = self._db.get_total_income(current_month) + if month_total_income is None: + month_total_income = 0.0 + elif month_total_outcome is None: + month_total_outcome = 0.0 + + LOGGER.debug(f'Total income: {month_total_income}, Total outcome: {month_total_outcome}') + month_balance = month_total_income - month_total_outcome + self._bot.reply_to( + message, + self._replies.balance.format( + month_balance, + month_total_income, + month_total_outcome + ) + ) + + def start(self): + LOGGER.info(f'Starting bot') + self._bot.infinity_polling() diff --git a/telegram/configs.py b/telegram/configs.py new file mode 100644 index 0000000..d3f98b2 --- /dev/null +++ b/telegram/configs.py @@ -0,0 +1,3 @@ +from configurator import Config + +DEFAULT_TOKEN_POSITION = Config(str, "TELEGRAM", "token_position", "config/token.txt") From 515f7bd419a6a4379dce7360f9a0da7f86598de3 Mon Sep 17 00:00:00 2001 From: portbusy Date: Sat, 23 Jul 2022 19:06:06 +0200 Subject: [PATCH 3/4] UPDATE: README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index bb59779..4ad030b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # Description -This is the first version of my telegram bot which allows a simple expenses tracker. +This is the first version of my telegram bot which allows a simple expenses tracker. It is based on +[this library](https://github.com/eternnoir/pyTelegramBotAPI) The bot can now take income and outcome and display the current month resume; here is a list of commands: From 9d2d2e2b151e370a9658a356c0e4af493798a997 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 23 Jul 2022 17:07:05 +0000 Subject: [PATCH 4/4] Bump urllib3 from 1.24.2 to 1.26.5 Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.24.2 to 1.26.5. - [Release notes](https://github.com/urllib3/urllib3/releases) - [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst) - [Commits](https://github.com/urllib3/urllib3/compare/1.24.2...1.26.5) --- updated-dependencies: - dependency-name: urllib3 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 401cff0..bac7b8b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ requests==2.22.0 -urllib3==1.24.2 +urllib3==1.26.5 pyTelegramBotAPI telebot~=0.0.4 PyYAML~=6.0