1
Fork 0
mirror of https://github.com/Steffo99/greed.git synced 2024-11-24 14:54:18 +00:00

Merge #52: Add logging support

This commit is contained in:
Steffo 2020-04-23 17:24:15 +02:00 committed by GitHub
commit dbd137b23e
Signed by: github
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 135 additions and 35 deletions

View file

@ -17,6 +17,7 @@ A [customizable](/config/template_config.ini), [multilanguage](/strings) Telegra
1. Download the project files through `git clone https://github.com/Steffo99/greed.git` (recommended) or [this link](https://github.com/Steffo99/greed/archive/master.zip). 1. Download the project files through `git clone https://github.com/Steffo99/greed.git` (recommended) or [this link](https://github.com/Steffo99/greed/archive/master.zip).
2. Install the project requirements with `pip install -r requirements.txt` 2. Install the project requirements with `pip install -r requirements.txt`
3. _Optional: run `pip install coloredlogs` to have colored logging output._
3. Run `python -OO core.py` to generate the configuration file. 3. Run `python -OO core.py` to generate the configuration file.
4. Open the config folder and edit the `config.ini` file following the contained instructions. 4. Open the config folder and edit the `config.ini` file following the contained instructions.
Ensure the `is_template` field is set to `no`. Ensure the `is_template` field is set to `no`.

View file

@ -5,7 +5,7 @@
# Config file parameters # Config file parameters
[Config] [Config]
; Config file version. DO NOT EDIT THIS! ; Config file version. DO NOT EDIT THIS!
version = 16 version = 17
; Set this to no when you are done editing the file ; Set this to no when you are done editing the file
is_template = yes is_template = yes
; Language code for string file ; Language code for string file
@ -87,3 +87,12 @@ refill_on_checkout = yes
; Optional sentry token: get the token at https://sentry.io/ or ask @Steffo for one ; Optional sentry token: get the token at https://sentry.io/ or ask @Steffo for one
; Needed to automatically report bugs found by the users in the code. ; Needed to automatically report bugs found by the users in the code.
sentry_token = https://00000000000000000000000000000000:00000000000000000000000000000000@sentry.io/0000000 sentry_token = https://00000000000000000000000000000000:00000000000000000000000000000000@sentry.io/0000000
# Logging settings
[Logging]
; The output format for the messages printed to the console
; See https://docs.python.org/3/library/logging.html#logrecord-attributes for information about the {}-attributes
format = {asctime} | {threadName} | {name} | {message}
; Logging level: ignore all log entries with a level lower than the specified one
; Valid options are FATAL, ERROR, WARNING, INFO, and DEBUG
level = INFO

View file

@ -1,9 +1,16 @@
import sys import sys
import os import os
import configparser import configparser
import logging
# Logs won't show up for this file as it is imported before logging is configured
log = logging.getLogger(__name__)
# Check if the config file exists, and create one if it doesn't # Check if the config file exists, and create one if it doesn't
if not os.path.isfile("config/config.ini"): if not os.path.isfile("config/config.ini"):
log.debug("Creating config.ini from template_config.ini")
# Open the template file and create the config file # Open the template file and create the config file
with open("config/template_config.ini", encoding="utf8") as template_file, \ with open("config/template_config.ini", encoding="utf8") as template_file, \
open("config/config.ini", "w", encoding="utf8") as config_file: open("config/config.ini", "w", encoding="utf8") as config_file:
@ -15,27 +22,34 @@ with open("config/template_config.ini", encoding="utf8") as template_file:
config = configparser.ConfigParser() config = configparser.ConfigParser()
config.read_file(template_file) config.read_file(template_file)
template_version = int(config["Config"]["version"]) template_version = int(config["Config"]["version"])
log.debug(f"Template is version {template_version}")
# Overwrite the template config with the values in the config # Overwrite the template config with the values in the config
with open("config/config.ini", encoding="utf8") as config_file: with open("config/config.ini", encoding="utf8") as config_file:
config.read_file(config_file) config.read_file(config_file)
config_version = int(config["Config"]["version"])
log.debug(f"Config is version {template_version}")
# Check if the file has been edited # Check if the file has been edited
if config["Config"]["is_template"] == "yes": if config["Config"]["is_template"] == "yes":
print("A config file has been created in config/config.ini.\n" log.debug("Config is a template, aborting...")
"Edit it with your configuration, set the is_template flag to false and restart this script.") log.fatal("A config file has been created in config/config.ini.\n"
"Edit it with your configuration, set the is_template flag to 'no' and restart this script.")
sys.exit(1) sys.exit(1)
# Check if the version has changed from the template # Check if the version has changed from the template
if template_version > int(config["Config"]["version"]): if template_version > config_version:
log.debug("Config is older than Template, trying to merge...")
# Reset the is_template flag # Reset the is_template flag
config["Config"]["is_template"] = "yes" config["Config"]["is_template"] = "yes"
# Update the config version # Update the config version
config["Config"]["version"] = str(template_version) config["Config"]["version"] = str(template_version)
# Save the file # Save the file
with open("config/config.ini", "w", encoding="utf8") as config_file: with open("config/config.ini", "w", encoding="utf8") as config_file:
log.debug("Writing merged config file...")
config.write(config_file) config.write(config_file)
# Notify the user and quit # Notify the user and quit
print("The config file in config/config.ini has been updated.\n" log.debug("Config is now a template, aborting...")
"Edit it with the new required data, set the is_template flag to true and restart this script.") log.fatal("The config file in config/config.ini has been updated.\n"
"Edit it with the new required data, set the is_template flag to true and restart this script.")
sys.exit(1) sys.exit(1)

52
core.py
View file

@ -5,6 +5,12 @@ import configloader
import utils import utils
import threading import threading
import importlib import importlib
import logging
try:
import coloredlogs
except ImportError:
coloredlogs = None
language = configloader.config["Config"]["language"] language = configloader.config["Config"]["language"]
strings = importlib.import_module("strings." + language) strings = importlib.import_module("strings." + language)
@ -12,34 +18,50 @@ strings = importlib.import_module("strings." + language)
def main(): def main():
"""The core code of the program. Should be run only in the main process!""" """The core code of the program. Should be run only in the main process!"""
# Rename the main thread for presentation purposes # Rename the main thread for presentation purposes
threading.current_thread().name = "Core" threading.current_thread().name = "Core"
# Setup logging
log = logging.getLogger("core")
logging.root.setLevel(configloader.config["Logging"]["level"])
stream_handler = logging.StreamHandler()
if coloredlogs is not None:
stream_handler.formatter = coloredlogs.ColoredFormatter(configloader.config["Logging"]["format"], style="{")
else:
stream_handler.formatter = logging.Formatter(configloader.config["Logging"]["format"], style="{")
logging.root.handlers.clear()
logging.root.addHandler(stream_handler)
log.debug("Logging setup successfully!")
# Ignore most python-telegram-bot logs, as they are useless most of the time
logging.getLogger("telegram").setLevel("ERROR")
# Create a bot instance # Create a bot instance
bot = utils.DuckBot(configloader.config["Telegram"]["token"]) bot = utils.DuckBot(configloader.config["Telegram"]["token"])
# Test the specified token # Test the specified token
log.debug("Testing bot token...")
try: try:
bot.get_me() bot.get_me()
except telegram.error.Unauthorized: except telegram.error.Unauthorized:
print("The token you have entered in the config file is invalid.\n" logging.fatal("The token you have entered in the config file is invalid. Fix it, then restart greed.")
"Fix it, then restart this script.")
sys.exit(1) sys.exit(1)
log.debug("Bot token is valid!")
# Create a dictionary linking the chat ids to the ChatWorker objects # Create a dictionary linking the chat ids to the Worker objects
# {"1234": <ChatWorker>} # {"1234": <Worker>}
chat_workers = {} chat_workers = {}
# Current update offset; if None it will get the last 100 unparsed messages # Current update offset; if None it will get the last 100 unparsed messages
next_update = None next_update = None
# Notify on the console that the bot is starting # Notify on the console that the bot is starting
print("greed-bot is now starting!") log.info("greed is starting!")
# Main loop of the program # Main loop of the program
while True: while True:
# Get a new batch of 100 updates and mark the last 100 parsed as read # Get a new batch of 100 updates and mark the last 100 parsed as read
log.debug("Getting updates from Telegram")
updates = bot.get_updates(offset=next_update, updates = bot.get_updates(offset=next_update,
timeout=int(configloader.config["Telegram"]["long_polling_timeout"])) timeout=int(configloader.config["Telegram"]["long_polling_timeout"]))
# Parse all the updates # Parse all the updates
@ -48,20 +70,24 @@ def main():
if update.message is not None: if update.message is not None:
# Ensure the message has been sent in a private chat # Ensure the message has been sent in a private chat
if update.message.chat.type != "private": if update.message.chat.type != "private":
log.debug(f"Received a message from a non-private chat: {update.message.chat.id}")
# Notify the chat # Notify the chat
bot.send_message(update.message.chat.id, strings.error_nonprivate_chat) bot.send_message(update.message.chat.id, strings.error_nonprivate_chat)
# Skip the update # Skip the update
continue continue
# If the message is a start command... # If the message is a start command...
if isinstance(update.message.text, str) and update.message.text.startswith("/start"): if isinstance(update.message.text, str) and update.message.text.startswith("/start"):
log.info(f"Received /start from: {update.message.chat.id}")
# Check if a worker already exists for that chat # Check if a worker already exists for that chat
old_worker = chat_workers.get(update.message.chat.id) old_worker = chat_workers.get(update.message.chat.id)
# If it exists, gracefully stop the worker # If it exists, gracefully stop the worker
if old_worker: if old_worker:
log.debug(f"Received request to stop {old_worker.name}")
old_worker.stop("request") old_worker.stop("request")
# Initialize a new worker for the chat # Initialize a new worker for the chat
new_worker = worker.ChatWorker(bot, update.message.chat) new_worker = worker.Worker(bot, update.message.chat)
# Start the worker # Start the worker
log.debug(f"Starting {new_worker.name}")
new_worker.start() new_worker.start()
# Store the worker in the dictionary # Store the worker in the dictionary
chat_workers[update.message.chat.id] = new_worker chat_workers[update.message.chat.id] = new_worker
@ -71,6 +97,7 @@ def main():
receiving_worker = chat_workers.get(update.message.chat.id) receiving_worker = chat_workers.get(update.message.chat.id)
# Ensure a worker exists for the chat and is alive # Ensure a worker exists for the chat and is alive
if receiving_worker is None or not receiving_worker.is_alive(): if receiving_worker is None or not receiving_worker.is_alive():
log.debug(f"Received a message in a chat without worker: {update.message.chat.id}")
# Suggest that the user restarts the chat with /start # Suggest that the user restarts the chat with /start
bot.send_message(update.message.chat.id, strings.error_no_worker_for_chat, bot.send_message(update.message.chat.id, strings.error_no_worker_for_chat,
reply_markup=telegram.ReplyKeyboardRemove()) reply_markup=telegram.ReplyKeyboardRemove())
@ -78,9 +105,11 @@ def main():
continue continue
# If the message contains the "Cancel" string defined in the strings file... # If the message contains the "Cancel" string defined in the strings file...
if update.message.text == strings.menu_cancel: if update.message.text == strings.menu_cancel:
log.debug(f"Forwarding CancelSignal to {receiving_worker}")
# Send a CancelSignal to the worker instead of the update # Send a CancelSignal to the worker instead of the update
receiving_worker.queue.put(worker.CancelSignal()) receiving_worker.queue.put(worker.CancelSignal())
else: else:
log.debug(f"Forwarding message to {receiving_worker}")
# Forward the update to the worker # Forward the update to the worker
receiving_worker.queue.put(update) receiving_worker.queue.put(update)
# If the update is a inline keyboard press... # If the update is a inline keyboard press...
@ -89,17 +118,20 @@ def main():
receiving_worker = chat_workers.get(update.callback_query.from_user.id) receiving_worker = chat_workers.get(update.callback_query.from_user.id)
# Ensure a worker exists for the chat # Ensure a worker exists for the chat
if receiving_worker is None: if receiving_worker is None:
log.debug(f"Received a callback query in a chat without worker: {update.callback_query.from_user.id}")
# Suggest that the user restarts the chat with /start # Suggest that the user restarts the chat with /start
bot.send_message(update.callback_query.from_user.id, strings.error_no_worker_for_chat) bot.send_message(update.callback_query.from_user.id, strings.error_no_worker_for_chat)
# Skip the update # Skip the update
continue continue
# Check if the pressed inline key is a cancel button # Check if the pressed inline key is a cancel button
if update.callback_query.data == "cmd_cancel": if update.callback_query.data == "cmd_cancel":
log.debug(f"Forwarding CancelSignal to {receiving_worker}")
# Forward a CancelSignal to the worker # Forward a CancelSignal to the worker
receiving_worker.queue.put(worker.CancelSignal()) receiving_worker.queue.put(worker.CancelSignal())
# Notify the Telegram client that the inline keyboard press has been received # Notify the Telegram client that the inline keyboard press has been received
bot.answer_callback_query(update.callback_query.id) bot.answer_callback_query(update.callback_query.id)
else: else:
log.debug(f"Forwarding callback query to {receiving_worker}")
# Forward the update to the worker # Forward the update to the worker
receiving_worker.queue.put(update) receiving_worker.queue.put(update)
# If the update is a precheckoutquery, ensure it hasn't expired before forwarding it # If the update is a precheckoutquery, ensure it hasn't expired before forwarding it
@ -107,17 +139,19 @@ def main():
# Forward the update to the corresponding worker # Forward the update to the corresponding worker
receiving_worker = chat_workers.get(update.pre_checkout_query.from_user.id) receiving_worker = chat_workers.get(update.pre_checkout_query.from_user.id)
# Check if it's the active invoice for this chat # Check if it's the active invoice for this chat
if receiving_worker is None or\ if receiving_worker is None or \
update.pre_checkout_query.invoice_payload != receiving_worker.invoice_payload: update.pre_checkout_query.invoice_payload != receiving_worker.invoice_payload:
# Notify the user that the invoice has expired # Notify the user that the invoice has expired
log.debug(f"Received a pre-checkout query for an expired invoice in: {update.message.chat.id}")
try: try:
bot.answer_pre_checkout_query(update.pre_checkout_query.id, bot.answer_pre_checkout_query(update.pre_checkout_query.id,
ok=False, ok=False,
error_message=strings.error_invoice_expired) error_message=strings.error_invoice_expired)
except telegram.error.BadRequest: except telegram.error.BadRequest:
print(f"ERROR: pre_checkout_query expired before an answer could be sent") log.error("pre-checkout query expired before an answer could be sent!")
# Go to the next update # Go to the next update
continue continue
log.debug(f"Forwarding pre-checkout query to {receiving_worker}")
# Forward the update to the worker # Forward the update to the worker
receiving_worker.queue.put(update) receiving_worker.queue.put(update)
# If there were any updates... # If there were any updates...

View file

@ -6,6 +6,12 @@ import typing
import os import os
import sys import sys
import importlib import importlib
import logging
import traceback
log = logging.getLogger(__name__)
language = config["Config"]["language"] language = config["Config"]["language"]
try: try:
@ -132,38 +138,38 @@ def catch_telegram_errors(func):
return func(*args, **kwargs) return func(*args, **kwargs)
# Bot was blocked by the user # Bot was blocked by the user
except telegram.error.Unauthorized: except telegram.error.Unauthorized:
print(f"Unauthorized to call {func.__name__}(), skipping.") log.debug(f"Unauthorized to call {func.__name__}(), skipping.")
break break
# Telegram API didn't answer in time # Telegram API didn't answer in time
except telegram.error.TimedOut: except telegram.error.TimedOut:
print(f"Timed out while calling {func.__name__}()," log.warning(f"Timed out while calling {func.__name__}(),"
f" retrying in {config['Telegram']['timed_out_pause']} secs...") f" retrying in {config['Telegram']['timed_out_pause']} secs...")
time.sleep(int(config["Telegram"]["timed_out_pause"])) time.sleep(int(config["Telegram"]["timed_out_pause"]))
# Telegram is not reachable # Telegram is not reachable
except telegram.error.NetworkError as error: except telegram.error.NetworkError as error:
print(f"Network error while calling {func.__name__}()," log.error(f"Network error while calling {func.__name__}(),"
f" retrying in {config['Telegram']['error_pause']} secs...") f" retrying in {config['Telegram']['error_pause']} secs...\n"
# Display the full NetworkError f"Full error: {error.message}")
print(f"Full error: {error.message}")
time.sleep(int(config["Telegram"]["error_pause"])) time.sleep(int(config["Telegram"]["error_pause"]))
# Unknown error # Unknown error
except telegram.error.TelegramError as error: except telegram.error.TelegramError as error:
if error.message.lower() in ["bad gateway", "invalid server response"]: if error.message.lower() in ["bad gateway", "invalid server response"]:
print(f"Bad Gateway while calling {func.__name__}()," log.warning(f"Bad Gateway while calling {func.__name__}(),"
f" retrying in {config['Telegram']['error_pause']} secs...") f" retrying in {config['Telegram']['error_pause']} secs...")
time.sleep(int(config["Telegram"]["error_pause"])) time.sleep(int(config["Telegram"]["error_pause"]))
elif error.message.lower() == "timed out": elif error.message.lower() == "timed out":
print(f"Timed out while calling {func.__name__}()," log.warning(f"Timed out while calling {func.__name__}(),"
f" retrying in {config['Telegram']['timed_out_pause']} secs...") f" retrying in {config['Telegram']['timed_out_pause']} secs...")
time.sleep(int(config["Telegram"]["timed_out_pause"])) time.sleep(int(config["Telegram"]["timed_out_pause"]))
else: else:
print(f"Telegram error while calling {func.__name__}()," log.error(f"Telegram error while calling {func.__name__}(),"
f" retrying in {config['Telegram']['error_pause']} secs...") f" retrying in {config['Telegram']['error_pause']} secs...\n"
# Display the full TelegramError f"Full error: {error.message}")
print(f"Full error: {error.message}")
# Send the error to the Sentry server # Send the error to the Sentry server
if sentry_client is not None: if sentry_client is not None:
sentry_client.captureException(exc_info=sys.exc_info()) sentry_client.captureException(exc_info=sys.exc_info())
else:
traceback.print_exception(*sys.exc_info())
time.sleep(int(config["Telegram"]["error_pause"])) time.sleep(int(config["Telegram"]["error_pause"]))
return result_func return result_func

View file

@ -14,6 +14,9 @@ import traceback
from html import escape from html import escape
import requests import requests
import importlib import importlib
import logging
log = logging.getLogger(__name__)
language = configloader.config["Config"]["language"] language = configloader.config["Config"]["language"]
strings = importlib.import_module("strings." + language) strings = importlib.import_module("strings." + language)
@ -21,7 +24,6 @@ strings = importlib.import_module("strings." + language)
class StopSignal: class StopSignal:
"""A data class that should be sent to the worker when the conversation has to be stopped abnormally.""" """A data class that should be sent to the worker when the conversation has to be stopped abnormally."""
def __init__(self, reason: str = ""): def __init__(self, reason: str = ""):
self.reason = reason self.reason = reason
@ -31,7 +33,7 @@ class CancelSignal:
pass pass
class ChatWorker(threading.Thread): class Worker(threading.Thread):
"""A worker for a single conversation. A new one is created every time the /start command is sent.""" """A worker for a single conversation. A new one is created every time the /start command is sent."""
def __init__(self, bot: utils.DuckBot, chat: telegram.Chat, *args, **kwargs): def __init__(self, bot: utils.DuckBot, chat: telegram.Chat, *args, **kwargs):
@ -41,11 +43,12 @@ class ChatWorker(threading.Thread):
self.bot: utils.DuckBot = bot self.bot: utils.DuckBot = bot
self.chat: telegram.Chat = chat self.chat: telegram.Chat = chat
# Open a new database session # Open a new database session
log.debug(f"Opening new database session for {self.name}")
self.session = db.Session() self.session = db.Session()
# Get the user db data from the users and admin tables # Get the user db data from the users and admin tables
self.user: Optional[db.User] = None self.user: Optional[db.User] = None
self.admin: Optional[db.Admin] = None self.admin: Optional[db.Admin] = None
# The sending pipe is stored in the ChatWorker class, allowing the forwarding of messages to the chat process # The sending pipe is stored in the Worker class, allowing the forwarding of messages to the chat process
self.queue = queuem.Queue() self.queue = queuem.Queue()
# The current active invoice payload; reject all invoices with a different payload # The current active invoice payload; reject all invoices with a different payload
self.invoice_payload = None self.invoice_payload = None
@ -56,13 +59,19 @@ class ChatWorker(threading.Thread):
self.sentry_client = raven.Client(configloader.config["Error Reporting"]["sentry_token"], self.sentry_client = raven.Client(configloader.config["Error Reporting"]["sentry_token"],
release=raven.fetch_git_sha(os.path.dirname(__file__)), release=raven.fetch_git_sha(os.path.dirname(__file__)),
environment="Dev" if __debug__ else "Prod") environment="Dev" if __debug__ else "Prod")
log.debug("Sentry: enabled")
else: else:
self.sentry_client = None self.sentry_client = None
log.debug("Sentry: disabled")
def __repr__(self):
return f"<{self.__class__.__qualname__} {self.chat.id}>"
# noinspection PyBroadException # noinspection PyBroadException
def run(self): def run(self):
"""The conversation code.""" """The conversation code."""
# Welcome the user to the bot # Welcome the user to the bot
log.debug("Starting conversation")
self.bot.send_message(self.chat.id, strings.conversation_after_start) self.bot.send_message(self.chat.id, strings.conversation_after_start)
# Get the user db data from the users and admin tables # Get the user db data from the users and admin tables
self.user = self.session.query(db.User).filter(db.User.user_id == self.chat.id).one_or_none() self.user = self.session.query(db.User).filter(db.User.user_id == self.chat.id).one_or_none()
@ -91,6 +100,9 @@ class ChatWorker(threading.Thread):
self.session.add(self.admin) self.session.add(self.admin)
# Commit the transaction # Commit the transaction
self.session.commit() self.session.commit()
log.info(f"Created new user: {self.user}")
if will_be_owner:
log.warning(f"User was auto-promoted to Admin as no other admins existed: {self.user}")
# Capture exceptions that occour during the conversation # Capture exceptions that occour during the conversation
try: try:
# If the user is not an admin, send him to the user menu # If the user is not an admin, send him to the user menu
@ -123,8 +135,8 @@ class ChatWorker(threading.Thread):
self.join() self.join()
def update_user(self) -> db.User: def update_user(self) -> db.User:
"""Update the user data. """Update the user data."""
Note that this method will cause crashes if used in different threads with sqlite databases.""" log.debug("Fetching updated user data from the database")
self.user = self.session.query(db.User).filter(db.User.user_id == self.chat.id).one_or_none() self.user = self.session.query(db.User).filter(db.User.user_id == self.chat.id).one_or_none()
return self.user return self.user
@ -150,6 +162,7 @@ class ChatWorker(threading.Thread):
items: List[str], items: List[str],
cancellable: bool = False) -> Union[str, CancelSignal]: cancellable: bool = False) -> Union[str, CancelSignal]:
"""Continue getting updates until until one of the strings contained in the list is received as a message.""" """Continue getting updates until until one of the strings contained in the list is received as a message."""
log.debug("Waiting for a specific message...")
while True: while True:
# Get the next update # Get the next update
update = self.__receive_next_update() update = self.__receive_next_update()
@ -176,6 +189,7 @@ class ChatWorker(threading.Thread):
def __wait_for_regex(self, regex: str, cancellable: bool = False) -> Union[str, CancelSignal]: def __wait_for_regex(self, regex: str, cancellable: bool = False) -> Union[str, CancelSignal]:
"""Continue getting updates until the regex finds a match in a message, then return the first capture group.""" """Continue getting updates until the regex finds a match in a message, then return the first capture group."""
log.debug("Waiting for a regex...")
while True: while True:
# Get the next update # Get the next update
update = self.__receive_next_update() update = self.__receive_next_update()
@ -206,6 +220,7 @@ class ChatWorker(threading.Thread):
cancellable: bool = False) -> Union[telegram.PreCheckoutQuery, CancelSignal]: cancellable: bool = False) -> Union[telegram.PreCheckoutQuery, CancelSignal]:
"""Continue getting updates until a precheckoutquery is received. """Continue getting updates until a precheckoutquery is received.
The payload is checked by the core before forwarding the message.""" The payload is checked by the core before forwarding the message."""
log.debug("Waiting for a PreCheckoutQuery...")
while True: while True:
# Get the next update # Get the next update
update = self.__receive_next_update() update = self.__receive_next_update()
@ -226,6 +241,7 @@ class ChatWorker(threading.Thread):
def __wait_for_successfulpayment(self) -> telegram.SuccessfulPayment: def __wait_for_successfulpayment(self) -> telegram.SuccessfulPayment:
"""Continue getting updates until a successfulpayment is received.""" """Continue getting updates until a successfulpayment is received."""
log.debug("Waiting for a SuccessfulPayment...")
while True: while True:
# Get the next update # Get the next update
update = self.__receive_next_update() update = self.__receive_next_update()
@ -240,6 +256,7 @@ class ChatWorker(threading.Thread):
def __wait_for_photo(self, cancellable: bool = False) -> Union[List[telegram.PhotoSize], CancelSignal]: def __wait_for_photo(self, cancellable: bool = False) -> Union[List[telegram.PhotoSize], CancelSignal]:
"""Continue getting updates until a photo is received, then return it.""" """Continue getting updates until a photo is received, then return it."""
log.debug("Waiting for a photo...")
while True: while True:
# Get the next update # Get the next update
update = self.__receive_next_update() update = self.__receive_next_update()
@ -264,6 +281,7 @@ class ChatWorker(threading.Thread):
def __wait_for_inlinekeyboard_callback(self, cancellable: bool = False) \ def __wait_for_inlinekeyboard_callback(self, cancellable: bool = False) \
-> Union[telegram.CallbackQuery, CancelSignal]: -> Union[telegram.CallbackQuery, CancelSignal]:
"""Continue getting updates until an inline keyboard callback is received, then return it.""" """Continue getting updates until an inline keyboard callback is received, then return it."""
log.debug("Waiting for a CallbackQuery...")
while True: while True:
# Get the next update # Get the next update
update = self.__receive_next_update() update = self.__receive_next_update()
@ -286,6 +304,7 @@ class ChatWorker(threading.Thread):
def __user_select(self) -> Union[db.User, CancelSignal]: def __user_select(self) -> Union[db.User, CancelSignal]:
"""Select an user from the ones in the database.""" """Select an user from the ones in the database."""
log.debug("Waiting for a user selection...")
# Find all the users in the database # Find all the users in the database
users = self.session.query(db.User).order_by(db.User.user_id).all() users = self.session.query(db.User).order_by(db.User.user_id).all()
# Create a list containing all the keyboard button strings # Create a list containing all the keyboard button strings
@ -315,6 +334,7 @@ class ChatWorker(threading.Thread):
def __user_menu(self): def __user_menu(self):
"""Function called from the run method when the user is not an administrator. """Function called from the run method when the user is not an administrator.
Normal bot actions should be placed here.""" Normal bot actions should be placed here."""
log.debug("Displaying __user_menu")
# Loop used to returning to the menu after executing a command # Loop used to returning to the menu after executing a command
while True: while True:
# Create a keyboard with the user main menu # Create a keyboard with the user main menu
@ -355,6 +375,7 @@ class ChatWorker(threading.Thread):
def __order_menu(self): def __order_menu(self):
"""User menu to order products from the shop.""" """User menu to order products from the shop."""
log.debug("Displaying __order_menu")
# Get the products list from the db # Get the products list from the db
products = self.session.query(db.Product).filter_by(deleted=False).all() products = self.session.query(db.Product).filter_by(deleted=False).all()
# Create a dict to be used as 'cart' # Create a dict to be used as 'cart'
@ -575,6 +596,7 @@ class ChatWorker(threading.Thread):
def __order_status(self): def __order_status(self):
"""Display the status of the sent orders.""" """Display the status of the sent orders."""
log.debug("Displaying __order_status")
# Find the latest orders # Find the latest orders
orders = self.session.query(db.Order) \ orders = self.session.query(db.Order) \
.filter(db.Order.user == self.user) \ .filter(db.Order.user == self.user) \
@ -591,6 +613,7 @@ class ChatWorker(threading.Thread):
def __add_credit_menu(self): def __add_credit_menu(self):
"""Add more credit to the account.""" """Add more credit to the account."""
log.debug("Displaying __add_credit_menu")
# Create a payment methods keyboard # Create a payment methods keyboard
keyboard = list() keyboard = list()
# Add the supported payment methods to the keyboard # Add the supported payment methods to the keyboard
@ -623,6 +646,7 @@ class ChatWorker(threading.Thread):
def __add_credit_cc(self): def __add_credit_cc(self):
"""Add money to the wallet through a credit card payment.""" """Add money to the wallet through a credit card payment."""
log.debug("Displaying __add_credit_cc")
# Create a keyboard to be sent later # Create a keyboard to be sent later
presets = list(map(lambda s: s.strip(" "), configloader.config["Credit Card"]["payment_presets"].split('|'))) presets = list(map(lambda s: s.strip(" "), configloader.config["Credit Card"]["payment_presets"].split('|')))
keyboard = [[telegram.KeyboardButton(str(utils.Price(preset)))] for preset in presets] keyboard = [[telegram.KeyboardButton(str(utils.Price(preset)))] for preset in presets]
@ -730,11 +754,13 @@ class ChatWorker(threading.Thread):
def __bot_info(self): def __bot_info(self):
"""Send information about the bot.""" """Send information about the bot."""
log.debug("Displaying __bot_info")
self.bot.send_message(self.chat.id, strings.bot_info) self.bot.send_message(self.chat.id, strings.bot_info)
def __admin_menu(self): def __admin_menu(self):
"""Function called from the run method when the user is an administrator. """Function called from the run method when the user is an administrator.
Administrative bot actions should be placed here.""" Administrative bot actions should be placed here."""
log.debug("Displaying __admin_menu")
# Loop used to return to the menu after executing a command # Loop used to return to the menu after executing a command
while True: while True:
# Create a keyboard with the admin main menu based on the admin permissions specified in the db # Create a keyboard with the admin main menu based on the admin permissions specified in the db
@ -791,6 +817,7 @@ class ChatWorker(threading.Thread):
def __products_menu(self): def __products_menu(self):
"""Display the admin menu to select a product to edit.""" """Display the admin menu to select a product to edit."""
log.debug("Displaying __products_menu")
# Get the products list from the db # Get the products list from the db
products = self.session.query(db.Product).filter_by(deleted=False).all() products = self.session.query(db.Product).filter_by(deleted=False).all()
# Create a list of product names # Create a list of product names
@ -827,6 +854,7 @@ class ChatWorker(threading.Thread):
def __edit_product_menu(self, product: Optional[db.Product] = None): def __edit_product_menu(self, product: Optional[db.Product] = None):
"""Add a product to the database or edit an existing one.""" """Add a product to the database or edit an existing one."""
log.debug("Displaying __edit_product_menu")
# Create an inline keyboard with a single skip button # Create an inline keyboard with a single skip button
cancel = telegram.InlineKeyboardMarkup([[telegram.InlineKeyboardButton(strings.menu_skip, cancel = telegram.InlineKeyboardMarkup([[telegram.InlineKeyboardButton(strings.menu_skip,
callback_data="cmd_cancel")]]) callback_data="cmd_cancel")]])
@ -915,6 +943,7 @@ class ChatWorker(threading.Thread):
self.bot.send_message(self.chat.id, strings.success_product_edited) self.bot.send_message(self.chat.id, strings.success_product_edited)
def __delete_product_menu(self): def __delete_product_menu(self):
log.debug("Displaying __delete_product_menu")
# Get the products list from the db # Get the products list from the db
products = self.session.query(db.Product).filter_by(deleted=False).all() products = self.session.query(db.Product).filter_by(deleted=False).all()
# Create a list of product names # Create a list of product names
@ -942,6 +971,7 @@ class ChatWorker(threading.Thread):
def __orders_menu(self): def __orders_menu(self):
"""Display a live flow of orders.""" """Display a live flow of orders."""
log.debug("Displaying __orders_menu")
# Create a cancel and a stop keyboard # Create a cancel and a stop keyboard
stop_keyboard = telegram.InlineKeyboardMarkup([[telegram.InlineKeyboardButton(strings.menu_stop, stop_keyboard = telegram.InlineKeyboardMarkup([[telegram.InlineKeyboardButton(strings.menu_stop,
callback_data="cmd_cancel")]]) callback_data="cmd_cancel")]])
@ -1033,6 +1063,7 @@ class ChatWorker(threading.Thread):
def __create_transaction(self): def __create_transaction(self):
"""Edit manually the credit of an user.""" """Edit manually the credit of an user."""
log.debug("Displaying __create_transaction")
# Make the admin select an user # Make the admin select an user
user = self.__user_select() user = self.__user_select()
# Allow the cancellation of the operation # Allow the cancellation of the operation
@ -1075,6 +1106,7 @@ class ChatWorker(threading.Thread):
def __help_menu(self): def __help_menu(self):
"""Help menu. Allows the user to ask for assistance, get a guide or see some info about the bot.""" """Help menu. Allows the user to ask for assistance, get a guide or see some info about the bot."""
log.debug("Displaying __help_menu")
# Create a keyboard with the user help menu # Create a keyboard with the user help menu
keyboard = [[telegram.KeyboardButton(strings.menu_guide)], keyboard = [[telegram.KeyboardButton(strings.menu_guide)],
[telegram.KeyboardButton(strings.menu_contact_shopkeeper)], [telegram.KeyboardButton(strings.menu_contact_shopkeeper)],
@ -1102,6 +1134,7 @@ class ChatWorker(threading.Thread):
def __transaction_pages(self): def __transaction_pages(self):
"""Display the latest transactions, in pages.""" """Display the latest transactions, in pages."""
log.debug("Displaying __transaction_pages")
# Page number # Page number
page = 0 page = 0
# Create and send a placeholder message to be populated # Create and send a placeholder message to be populated
@ -1156,6 +1189,7 @@ class ChatWorker(threading.Thread):
def __transactions_file(self): def __transactions_file(self):
"""Generate a .csv file containing the list of all transactions.""" """Generate a .csv file containing the list of all transactions."""
log.debug("Generating __transaction_file")
# Retrieve all the transactions # Retrieve all the transactions
transactions = self.session.query(db.Transaction).order_by(db.Transaction.transaction_id.asc()).all() transactions = self.session.query(db.Transaction).order_by(db.Transaction.transaction_id.asc()).all()
# Create the file if it doesn't exists # Create the file if it doesn't exists
@ -1201,6 +1235,7 @@ class ChatWorker(threading.Thread):
def __add_admin(self): def __add_admin(self):
"""Add an administrator to the bot.""" """Add an administrator to the bot."""
log.debug("Displaying __add_admin")
# Let the admin select an administrator to promote # Let the admin select an administrator to promote
user = self.__user_select() user = self.__user_select()
# Allow the cancellation of the operation # Allow the cancellation of the operation
@ -1265,6 +1300,7 @@ class ChatWorker(threading.Thread):
def __graceful_stop(self, stop_trigger: StopSignal): def __graceful_stop(self, stop_trigger: StopSignal):
"""Handle the graceful stop of the thread.""" """Handle the graceful stop of the thread."""
log.debug("Gracefully stopping the conversation")
# If the session has expired... # If the session has expired...
if stop_trigger.reason == "timeout": if stop_trigger.reason == "timeout":
# Notify the user that the session has expired and remove the keyboard # Notify the user that the session has expired and remove the keyboard