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
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..4ad030b 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,10 @@
# 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:
+
* `/listoutcome` print the list of current month outcome
* `/listincome` print the list of current month income
* `/delete +/- ` delete the specified entry
@@ -10,8 +13,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..bac7b8b 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.26.5
+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")