mirror of
https://github.com/may-cat/firefly-iii-telegram-bot.git
synced 2025-12-17 12:15:14 -06:00
347 lines
15 KiB
Python
347 lines
15 KiB
Python
#!/usr/bin/python3
|
|
import re
|
|
import telebot
|
|
import schedule
|
|
import time
|
|
import logging
|
|
import oauth2 as oauth
|
|
import logging
|
|
import firefly
|
|
import users
|
|
import traceback
|
|
import json
|
|
|
|
#########################################################################################
|
|
# Basic config
|
|
#
|
|
|
|
# Load configs
|
|
content_file = open("config.json", 'r')
|
|
content = content_file.read()
|
|
content_file.close()
|
|
CONFIGS = json.loads(content) #TODO: make error message if configs file doesn't exist or is corrupted
|
|
|
|
MESSAGES = {
|
|
"welcome": "Welcome!",
|
|
"asking_to_verify_money_in_pocket": "Let's check money in your pocket. How much do you have?",
|
|
"you_are_my_user_already": "You are my user already",
|
|
"you_are_my_master": "You are my first user. I choose you as my master.",
|
|
#
|
|
"choose_your_pocket_prefix": "choose pocket ",
|
|
"choose_your_pocket_account": "choose your pocket account",
|
|
"choose_your_pocket_account_retry": "choose your pocket account. I didn't get what you've said.",
|
|
#
|
|
"excuses_for_bothering": "Okey, I do not bother you",
|
|
"where_did_you_get_money": "Whoa, that's more than you got before. Where did you get the money?",
|
|
"no_amount_sent": "You didn't send amount of money you took. I can't handle such messages for the moment.",
|
|
"thankyou": "Thank you",
|
|
"choose_budget": "Choose budget:",
|
|
#
|
|
"no_connection": "No connection to your firefly server, sorry. Check your server and api key and try again.",
|
|
"request_for_server": "Please, tell me your firefly server url (for example `http://152.12.51.224` or `http://myfirefly.com`)",
|
|
"request_for_server_failed_validation": "Doesn't look like server url",
|
|
"request_for_oauth_key": "Please, tell me firefly access token (for example `eyJ0eXAiOiJKV1QiLCJZboci9iJSUzI1NiIsImp0aSI6ImY1YWY0Yzc2ZTBkNDliNjA2ZTAwZjcyYTc0YjQ4YzM4MTc1Y2JjNWI4MjU1MWU3NDMwNTM5MWJkNGRiYmU0NDk2ODE1MGRmYThhYjg0NzM2In0`)",
|
|
#
|
|
"rules_introduction": "You can send me spent money at any time (for example `123 tea`). Once a day I will ask you, how much money do you have in your pocket.",
|
|
#
|
|
"money_in_pocket_update_transaction": "updating amount of money"
|
|
}
|
|
firefly = firefly.Firefly()
|
|
|
|
#########################################################################################
|
|
# Basic classes
|
|
#
|
|
|
|
class ScheduledTeleBot(telebot.TeleBot):
|
|
def __non_threaded_polling(self, schedule, none_stop=False, interval=0, timeout=3):
|
|
logger.info('Started polling.')
|
|
self._TeleBot__stop_polling.clear()
|
|
error_interval = .25
|
|
|
|
while not self._TeleBot__stop_polling.wait(interval):
|
|
try:
|
|
schedule.run_pending()
|
|
self._TeleBot__retrieve_updates(timeout)
|
|
error_interval = .25
|
|
except apihelper.ApiException as e:
|
|
logger.error(e)
|
|
if not none_stop:
|
|
self._TeleBot__stop_polling.set()
|
|
logger.info("Exception occurred. Stopping.")
|
|
else:
|
|
logger.info("Waiting for {0} seconds until retry".format(error_interval))
|
|
time.sleep(error_interval)
|
|
error_interval *= 2
|
|
except KeyboardInterrupt:
|
|
logger.info("KeyboardInterrupt received.")
|
|
self._TeleBot__stop_polling.set()
|
|
break
|
|
|
|
logger.info('Stopped polling.')
|
|
|
|
def polling(self, schedule, none_stop=False, interval=0, timeout=20):
|
|
self.__non_threaded_polling(schedule, none_stop, interval, timeout)
|
|
|
|
#########################################################################################
|
|
# Main variables
|
|
#
|
|
|
|
bot = ScheduledTeleBot(CONFIGS["telegram_token"])
|
|
logger = logging.getLogger('TeleBot')
|
|
users=users.User()
|
|
|
|
#########################################################################################
|
|
# Chatting callbacks
|
|
#
|
|
|
|
# cronjob, that sends message to users
|
|
def cronjob():
|
|
logger.info('cronjob function')
|
|
for chat_id in users.getUsersIds():
|
|
markup = telebot.types.ForceReply(selective=False)
|
|
bot.send_message(chat_id, MESSAGES["asking_to_verify_money_in_pocket"], reply_markup=markup)
|
|
|
|
########################################
|
|
# Init communication
|
|
|
|
# replies for `/start`
|
|
@bot.message_handler(commands=['start'])
|
|
def send_welcome(message):
|
|
# TODO: security
|
|
try:
|
|
bot.reply_to(message, MESSAGES["welcome"])
|
|
if users.exists(message.from_user.username):
|
|
bot.send_message(message.chat.id, MESSAGES["you_are_my_user_already"])
|
|
else:
|
|
users.add(message.from_user.username, message.chat.id)
|
|
# send message for function _check_if_reply_to_server_request(msg)
|
|
markup = telebot.types.ForceReply(selective=False)
|
|
bot.send_message(message.chat.id, MESSAGES["request_for_server"], reply_markup=markup)
|
|
except Exception as err:
|
|
print(err)
|
|
traceback.print_exc()
|
|
|
|
# checking, if message is reply for firefly server request
|
|
def _check_if_reply_to_server_request(msg):
|
|
if not hasattr(msg,'reply_to_message'):
|
|
return False
|
|
if not hasattr(msg.reply_to_message,'text'):
|
|
return False
|
|
return msg.reply_to_message.text == MESSAGES["request_for_server"]
|
|
|
|
@bot.message_handler(func=_check_if_reply_to_server_request)
|
|
def got_reply_on_server_request(message):
|
|
if (True): # TODO: validate message
|
|
users.setServer(message.from_user.username, message.text)
|
|
# Please, tell me firefly access token (for example `eyJ0eXAiOiJKV1QiLCJZboci9iJSUzI1NiIsImp0aSI6ImY1YWY0Yzc2ZTBkNDliNjA2ZTAwZjcyYTc0YjQ4YzM4MTc1Y2JjNWI4MjU1MWU3NDMwNTM5MWJkNGRiYmU0NDk2ODE1MGRmYThhYjg0NzM2In0`)
|
|
markup = telebot.types.ForceReply(selective=False)
|
|
bot.send_message(message.chat.id, MESSAGES["request_for_oauth_key"], reply_markup=markup)
|
|
else:
|
|
bot.send_message(message.chat.id, MESSAGES["request_for_server_failed_validation"])
|
|
markup = telebot.types.ForceReply(selective=False)
|
|
bot.send_message(message.chat.id, MESSAGES["request_for_server"], reply_markup=markup)
|
|
|
|
# checking, if message is reply to firefly oauth token request
|
|
def _check_if_reply_to_access_token_request(msg):
|
|
if not hasattr(msg,'reply_to_message'):
|
|
return False
|
|
if not hasattr(msg.reply_to_message,'text'):
|
|
return False
|
|
return msg.reply_to_message.text == MESSAGES["request_for_oauth_key"]
|
|
|
|
@bot.message_handler(func=_check_if_reply_to_access_token_request)
|
|
def got_reply_on_access_token(message):
|
|
try:
|
|
users.setAccessToken(message.from_user.username, message.text)
|
|
if firefly.testConnection(message.from_user.username, users):
|
|
if not users.hasMaster():
|
|
bot.send_message(message.chat.id, MESSAGES["you_are_my_master"])
|
|
# ask user, which account should be locked to this user. Using telegram's buttons: "choose pocket <pocketname>"
|
|
balances = firefly.getBalances(message.from_user.username, users)
|
|
markup = telebot.types.ReplyKeyboardMarkup()
|
|
for balace in balances:
|
|
markup.row(telebot.types.KeyboardButton(MESSAGES["choose_your_pocket_prefix"]+str(balace)))
|
|
bot.reply_to(message, MESSAGES["choose_your_pocket_account"], reply_markup=markup)
|
|
else:
|
|
bot.send_message(message.chat.id, MESSAGES["no_connection"])
|
|
markup = telebot.types.ForceReply(selective=False)
|
|
bot.send_message(message.chat.id, MESSAGES["request_for_server"], reply_markup=markup)
|
|
except Exception as err:
|
|
print(err)
|
|
traceback.print_exc()
|
|
|
|
@bot.message_handler(regexp=MESSAGES["choose_your_pocket_prefix"])
|
|
def choose_pocket(message):
|
|
try:
|
|
message_text = message.text
|
|
# get balances and incomes from firefly
|
|
pockets_data = firefly.getBalancesExtended(message.from_user.username, users)
|
|
|
|
# get pockets
|
|
message_pocket = ''
|
|
account_id = 0
|
|
account_currency = ""
|
|
for pocket in pockets_data:
|
|
if pocket["attributes"]["name"] in message_text:
|
|
message_pocket = pocket["attributes"]["name"]
|
|
account_id = pocket["id"]
|
|
account_currency = pocket["attributes"]["currency_code"]
|
|
message_text = message_text.replace(pocket["attributes"]["name"],'')
|
|
break
|
|
# message is left in message_text
|
|
|
|
if message_pocket:
|
|
users.setPocket(message.from_user.username, value=message_pocket, account_id=account_id, account_currency=account_currency)
|
|
users.setAuthorized(message.from_user.username)
|
|
# Send welcome message
|
|
markup = telebot.types.ReplyKeyboardRemove(selective=False)
|
|
bot.send_message(message.chat.id, MESSAGES["rules_introduction"], reply_markup=markup)
|
|
else:
|
|
for pocket in pockets:
|
|
markup.row(telebot.types.KeyboardButton('choose pocket '+str(pocket)))
|
|
markup = telebot.types.ReplyKeyboardMarkup()
|
|
bot.reply_to(message, MESSAGES["choose_your_pocket_account_retry"], reply_markup=markup)
|
|
except Exception as err:
|
|
print(err)
|
|
traceback.print_exc()
|
|
|
|
|
|
########################################
|
|
# Crons communication
|
|
|
|
# check for next function
|
|
def _check_if_message_made_by_cron(msg):
|
|
if not hasattr(msg,'reply_to_message'):
|
|
return False
|
|
if not hasattr(msg.reply_to_message,'text'):
|
|
return False
|
|
return msg.reply_to_message.text == MESSAGES["asking_to_verify_money_in_pocket"]
|
|
|
|
|
|
# if this is reply to message, made by cron - we expect money in pocket (see _check_if_message_made_by_cron())
|
|
@bot.message_handler(func=_check_if_message_made_by_cron)
|
|
def got_reply_on_cron(message):
|
|
message_text = message.text
|
|
# get money in pocket from firefly
|
|
current_balance = firefly.getCurrentBalance(message.from_user.username, users)
|
|
|
|
# get number
|
|
message_number = re.findall('\d+', message_text)[0]
|
|
message_text = message_text.replace(message_number,'')
|
|
|
|
if not message_number:
|
|
bot.reply_to(message, MESSAGES["excuses_for_bothering"])
|
|
bot.send_message(users.getMasterId(), "User @"+message.from_user.username+" ignores me!")
|
|
pass
|
|
else:
|
|
try:
|
|
message_integer = int(message_number)
|
|
balance_diff = message_integer-current_balance
|
|
if message_integer > current_balance:
|
|
# get balances and incomes from firefly
|
|
balances = firefly.getBalances(message.from_user.username, users)
|
|
balances.extend(firefly.getIncomes(message.from_user.username, users))
|
|
|
|
markup = telebot.types.ReplyKeyboardMarkup()
|
|
for balance in balances:
|
|
markup.row(telebot.types.KeyboardButton('took '+str(abs(balance_diff))+' from '+str(balance)))
|
|
bot.reply_to(message, MESSAGES["where_did_you_get_money"], reply_markup=markup)
|
|
elif message_integer < current_balance:
|
|
bot.send_message(message.chat.id, "You have spent "+str(abs(balance_diff))+".")
|
|
_talk_about_spent_money(message, message_number=str(abs(balance_diff)))
|
|
else:
|
|
bot.send_message(message.chat.id, "Nothing changed. Thanks for info!")
|
|
except Exception as err:
|
|
print(err)
|
|
|
|
|
|
# Adding money to user's balance from another balance
|
|
# Works with messages:
|
|
# - took 1231 from balance1
|
|
# - took 1231
|
|
@bot.message_handler(regexp="took")
|
|
def took_money(message):
|
|
message_text = message.text
|
|
# get balances and incomes from firefly
|
|
balances = firefly.getBalances(message.from_user.username, users)
|
|
|
|
# get number
|
|
message_number = re.findall('\d+', message_text)[0]
|
|
message_text = message_text.replace(message_number,'')
|
|
|
|
# get balance
|
|
message_balance = ''
|
|
for balance in balances:
|
|
if balance in message_text:
|
|
message_balance = balance
|
|
message_text = message_text.replace(balance,'')
|
|
break
|
|
# message is left in message_text
|
|
|
|
if not message_balance:
|
|
# TODO: ask user for balance. With buttons.
|
|
pass
|
|
elif not message_number:
|
|
markup = telebot.types.ReplyKeyboardRemove()
|
|
bot.reply_to(message, MESSAGES["no_amount_sent"], reply_markup=markup)
|
|
else:
|
|
firefly.take(message.from_user.username, users, int(message_number), message_balance, message_text)
|
|
markup = telebot.types.ReplyKeyboardRemove()
|
|
bot.reply_to(message, MESSAGES["thankyou"], reply_markup=markup)
|
|
pass
|
|
|
|
########################################
|
|
# Make transaction communication
|
|
|
|
# talking about spent money. Aknowledge needed info.
|
|
def _talk_about_spent_money(message_to_reply, message_number="",message_budget="",message_text=""):
|
|
# get budgets from firefly
|
|
budgets=firefly.getBudgets(message_to_reply.from_user.username, users)
|
|
|
|
if not message_text or message_text.isspace():
|
|
message_text=MESSAGES["money_in_pocket_update_transaction"]
|
|
|
|
if not message_budget:
|
|
markup = telebot.types.ReplyKeyboardMarkup()
|
|
for budget in budgets:
|
|
markup.row(telebot.types.KeyboardButton(message_number+' '+budget+' '+message_text))
|
|
bot.reply_to(message_to_reply, MESSAGES["choose_budget"], reply_markup=markup)
|
|
# if everything got - just add it to firefly
|
|
else:
|
|
try:
|
|
markup = telebot.types.ReplyKeyboardRemove()
|
|
firefly.spend(message_to_reply.from_user.username, users, int(message_number), message_budget, message_text)
|
|
bot.reply_to(message_to_reply, MESSAGES["thankyou"], reply_markup=markup)
|
|
except Exception as err:
|
|
print(err)
|
|
|
|
# If message have numbers and not caught by previous handlers - we suppose it's about spent money
|
|
@bot.message_handler(regexp="[0-9]+")
|
|
def recieved_number(message):
|
|
message_text = message.text
|
|
# get budgets from firefly
|
|
budgets=firefly.getBudgets(message.from_user.username, users)
|
|
|
|
# get number
|
|
message_number = re.findall('\d+', message_text)[0]
|
|
message_text = message_text.replace(message_number,'')
|
|
# get budget
|
|
message_budget = ''
|
|
for budget in budgets:
|
|
if budget in message_text:
|
|
message_budget = budget
|
|
message_text = message_text.replace(budget,'')
|
|
break
|
|
# message is left in message_text
|
|
|
|
# if expense not set - ask for it
|
|
_talk_about_spent_money(message, message_number=message_number, message_budget=message_budget, message_text=message_text)
|
|
|
|
#########################################################################################
|
|
# Main executing code
|
|
#
|
|
# TODO: scheduling should be in config or in personal user's settings
|
|
schedule.every().day.at("20:00").do(cronjob)
|
|
#schedule.every().minute.do(cronjob)
|
|
|
|
bot.polling(schedule) |