From 5d5000893613ac7fca3488b557698faa372fbe76 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Sun, 3 May 2020 01:01:44 +0200 Subject: [PATCH 1/5] Refactor localization support to close #51 --- config/template_config.ini | 11 +- core.py | 23 +-- database.py | 7 + localization.py | 29 ++++ strings/{en_US.py => en.py} | 0 strings/{it_IT.py => it.py} | 0 strings/{ru_RU.py => ru.py} | 0 strings/{uk_UA.py => uk.py} | 0 worker.py | 325 +++++++++++++++++++----------------- 9 files changed, 225 insertions(+), 170 deletions(-) create mode 100644 localization.py rename strings/{en_US.py => en.py} (100%) rename strings/{it_IT.py => it.py} (100%) rename strings/{ru_RU.py => ru.py} (100%) rename strings/{uk_UA.py => uk.py} (100%) diff --git a/config/template_config.ini b/config/template_config.ini index 21b21e9..5941364 100644 --- a/config/template_config.ini +++ b/config/template_config.ini @@ -5,15 +5,16 @@ # Config file parameters [Config] ; Config file version. DO NOT EDIT THIS! -version = 17 +version = 18 ; Set this to no when you are done editing the file is_template = yes ; Language code for string file +; Uses the https://en.wikipedia.org/wiki/IETF_language_tag name of the language ; Available languages: -; it_IT - Italian, by Steffo -; en_US - English, by https://github.com/DarrenWestwood (incomplete, please improve it!) -; ua_UK - Ukrainian, by https://github.com/pzhuk -; ru_RU - Russian, by https://github.com/pzhuk +; it - Italian, by Steffo +; en - English, by https://github.com/DarrenWestwood (incomplete, please improve it!) +; uk - Ukrainian, by https://github.com/pzhuk +; ru - Russian, by https://github.com/pzhuk language = it_IT # Telegram bot parameters diff --git a/core.py b/core.py index 5245eba..fb4a8e0 100644 --- a/core.py +++ b/core.py @@ -4,7 +4,7 @@ import configloader import utils import threading -import importlib +import localization import logging try: @@ -12,9 +12,6 @@ except ImportError: coloredlogs = None -language = configloader.config["Config"]["language"] -strings = importlib.import_module("strings." + language) - def main(): """The core code of the program. Should be run only in the main process!""" @@ -48,6 +45,10 @@ def main(): sys.exit(1) log.debug("Bot token is valid!") + # Finding default language + default_language = configloader.config["Config"]["language"] + default_loc = localization.Localization(default_language) + # Create a dictionary linking the chat ids to the Worker objects # {"1234": } chat_workers = {} @@ -72,7 +73,7 @@ def main(): if update.message.chat.type != "private": log.debug(f"Received a message from a non-private chat: {update.message.chat.id}") # Notify the chat - bot.send_message(update.message.chat.id, strings.error_nonprivate_chat) + bot.send_message(update.message.chat.id, default_loc.get("error_nonprivate_chat")) # Skip the update continue # If the message is a start command... @@ -85,7 +86,9 @@ def main(): log.debug(f"Received request to stop {old_worker.name}") old_worker.stop("request") # Initialize a new worker for the chat - new_worker = worker.Worker(bot, update.message.chat) + new_worker = worker.Worker(bot=bot, + chat=update.message.chat, + telegram_user=update.message.from_user) # Start the worker log.debug(f"Starting {new_worker.name}") new_worker.start() @@ -99,12 +102,12 @@ def main(): 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 - bot.send_message(update.message.chat.id, strings.error_no_worker_for_chat, + bot.send_message(update.message.chat.id, default_loc.get("error_no_worker_for_chat"), reply_markup=telegram.ReplyKeyboardRemove()) # Skip the update continue # If the message contains the "Cancel" string defined in the strings file... - if update.message.text == strings.menu_cancel: + if update.message.text == receiving_worker.loc.get("menu_cancel"): log.debug(f"Forwarding CancelSignal to {receiving_worker}") # Send a CancelSignal to the worker instead of the update receiving_worker.queue.put(worker.CancelSignal()) @@ -120,7 +123,7 @@ def main(): 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 - bot.send_message(update.callback_query.from_user.id, strings.error_no_worker_for_chat) + bot.send_message(update.callback_query.from_user.id, default_loc.get("error_no_worker_for_chat")) # Skip the update continue # Check if the pressed inline key is a cancel button @@ -146,7 +149,7 @@ def main(): try: bot.answer_pre_checkout_query(update.pre_checkout_query.id, ok=False, - error_message=strings.error_invoice_expired) + error_message=default_loc.get("error_invoice_expired")) except telegram.error.BadRequest: log.error("pre-checkout query expired before an answer could be sent!") # Go to the next update diff --git a/database.py b/database.py index 4bdbfe4..90e0128 100644 --- a/database.py +++ b/database.py @@ -74,6 +74,13 @@ def recalculate_credit(self): valid_transactions: typing.List[Transaction] = [t for t in self.transactions if not t.refunded] self.credit = sum(map(lambda t: t.value, valid_transactions)) + @property + def full_name(self): + if self.last_name: + return f"{self.first_name} {self.last_name}" + else: + return self.first_name + def __repr__(self): return f"" diff --git a/localization.py b/localization.py new file mode 100644 index 0000000..84f3516 --- /dev/null +++ b/localization.py @@ -0,0 +1,29 @@ +import importlib + + +class IgnoreDict(dict): + """A dictionary that if passed to format_map, ignores the missing replacement fields.""" + def __missing__(self, key): + return "{" + key + "}" + + +class Localization: + def __init__(self, language, replacements=None): + self.language = language + self.module = importlib.import_module("strings." + language) + self.replacements = replacements if replacements else {} + + @staticmethod + def is_supported(language) -> bool: + try: + importlib.import_module("strings." + language) + except ImportError: + return False + else: + return True + + def get(self, key, **kwargs) -> str: + string = self.module.__getattribute__(key) + assert isinstance(string, str) + formatter = IgnoreDict(**self.replacements, **kwargs) + return string.format_map(formatter) diff --git a/strings/en_US.py b/strings/en.py similarity index 100% rename from strings/en_US.py rename to strings/en.py diff --git a/strings/it_IT.py b/strings/it.py similarity index 100% rename from strings/it_IT.py rename to strings/it.py diff --git a/strings/ru_RU.py b/strings/ru.py similarity index 100% rename from strings/ru_RU.py rename to strings/ru.py diff --git a/strings/uk_UA.py b/strings/uk.py similarity index 100% rename from strings/uk_UA.py rename to strings/uk.py diff --git a/worker.py b/worker.py index d481fc0..afaf5b3 100644 --- a/worker.py +++ b/worker.py @@ -13,14 +13,11 @@ import traceback from html import escape import requests -import importlib import logging +import localization log = logging.getLogger(__name__) -language = configloader.config["Config"]["language"] -strings = importlib.import_module("strings." + language) - class StopSignal: """A data class that should be sent to the worker when the conversation has to be stopped abnormally.""" @@ -36,12 +33,13 @@ class CancelSignal: class Worker(threading.Thread): """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, telegram_user: telegram.User, *args, **kwargs): # Initialize the thread super().__init__(name=f"Worker {chat.id}", *args, **kwargs) # Store the bot and chat info inside the class self.bot: utils.DuckBot = bot self.chat: telegram.Chat = chat + self.telegram_user: telegram.User = telegram_user # Open a new database session log.debug(f"Opening new database session for {self.name}") self.session = db.Session() @@ -52,6 +50,8 @@ def __init__(self, bot: utils.DuckBot, chat: telegram.Chat, *args, **kwargs): self.queue = queuem.Queue() # The current active invoice payload; reject all invoices with a different payload self.invoice_payload = None + # The localization strings for this user + self.loc = None # The Sentry client for reporting errors encountered by the user if configloader.config["Error Reporting"]["sentry_token"] != \ "https://00000000000000000000000000000000:00000000000000000000000000000000@sentry.io/0000000": @@ -69,9 +69,7 @@ def __repr__(self): def run(self): """The conversation code.""" - # Welcome the user to the bot log.debug("Starting conversation") - self.bot.send_message(self.chat.id, strings.conversation_after_start) # 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.admin = self.session.query(db.Admin).filter(db.Admin.user_id == self.chat.id).one_or_none() @@ -102,9 +100,33 @@ def run(self): 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}") + # Detect language from Telegram metadata + default_language = configloader.config["Config"]["language"] + language = self.telegram_user.language_code + if language: + log.debug(f"Detected language: {language}") + if not localization.Localization.is_supported(language): + log.debug(f"Unsupported language, using default: {default_language}") + language = default_language + else: + log.debug(f"No language detected, using default: {default_language}") + language = default_language + # Create a Localization object + self.loc = localization.Localization( + language=language, + replacements={ + "user_string": str(self.user), + "user_mention": self.user.mention(), + "user_full_name": self.user.full_name, + "user_first_name": self.user.first_name, + "today": datetime.datetime.now().strftime("%a %d %b %Y"), + } + ) # Capture exceptions that occour during the conversation # noinspection PyBroadException try: + # Welcome the user to the bot + self.bot.send_message(self.chat.id, self.loc.get("conversation_after_start")) # If the user is not an admin, send him to the user menu if self.admin is None: self.__user_menu() @@ -120,7 +142,7 @@ def run(self): # Try to notify the user of the exception # noinspection PyBroadException try: - self.bot.send_message(self.chat.id, strings.fatal_conversation_exception) + self.bot.send_message(self.chat.id, self.loc.get("fatal_conversation_exception")) except Exception: pass # If the Sentry integration is enabled, log the exception @@ -309,7 +331,7 @@ def __user_select(self) -> Union[db.User, CancelSignal]: # Find all the users in the database users = self.session.query(db.User).order_by(db.User.user_id).all() # Create a list containing all the keyboard button strings - keyboard_buttons = [[strings.menu_cancel]] + keyboard_buttons = [[self.loc.get("menu_cancel")]] # Add to the list all the users for user in users: keyboard_buttons.append([user.identifiable_str()]) @@ -318,7 +340,7 @@ def __user_select(self) -> Union[db.User, CancelSignal]: # Keep asking until a result is returned while True: # Send the keyboard - self.bot.send_message(self.chat.id, strings.conversation_admin_select_user, reply_markup=keyboard) + self.bot.send_message(self.chat.id, self.loc.get("conversation_admin_select_user"), reply_markup=keyboard) # Wait for a reply reply = self.__wait_for_regex("user_([0-9]+)", cancellable=True) # Propagate CancelSignals @@ -328,7 +350,7 @@ def __user_select(self) -> Union[db.User, CancelSignal]: user = self.session.query(db.User).filter_by(user_id=int(reply)).one_or_none() # Ensure the user exists if not user: - self.bot.send_message(self.chat.id, strings.error_user_does_not_exist) + self.bot.send_message(self.chat.id, self.loc.get("error_user_does_not_exist")) continue return user @@ -339,38 +361,39 @@ def __user_menu(self): # Loop used to returning to the menu after executing a command while True: # Create a keyboard with the user main menu - keyboard = [[telegram.KeyboardButton(strings.menu_order)], - [telegram.KeyboardButton(strings.menu_order_status)], - [telegram.KeyboardButton(strings.menu_add_credit)], - [telegram.KeyboardButton(strings.menu_help), telegram.KeyboardButton(strings.menu_bot_info)]] + keyboard = [[telegram.KeyboardButton(self.loc.get("menu_order"))], + [telegram.KeyboardButton(self.loc.get("menu_order_status"))], + [telegram.KeyboardButton(self.loc.get("menu_add_credit"))], + [telegram.KeyboardButton(self.loc.get("menu_help")), + telegram.KeyboardButton(self.loc.get("menu_bot_info"))]] # Send the previously created keyboard to the user (ensuring it can be clicked only 1 time) self.bot.send_message(self.chat.id, - strings.conversation_open_user_menu.format(credit=utils.Price(self.user.credit)), + self.loc.get("conversation_open_user_menu", credit=utils.Price(self.user.credit)), reply_markup=telegram.ReplyKeyboardMarkup(keyboard, one_time_keyboard=True)) # Wait for a reply from the user - selection = self.__wait_for_specific_message([strings.menu_order, strings.menu_order_status, - strings.menu_add_credit, strings.menu_bot_info, - strings.menu_help]) + selection = self.__wait_for_specific_message([self.loc.get("menu_order"), self.loc.get("menu_order_status"), + self.loc.get("menu_add_credit"), self.loc.get("menu_bot_info"), + self.loc.get("menu_help")]) # After the user reply, update the user data self.update_user() # If the user has selected the Order option... - if selection == strings.menu_order: + if selection == self.loc.get("menu_order"): # Open the order menu self.__order_menu() # If the user has selected the Order Status option... - elif selection == strings.menu_order_status: + elif selection == self.loc.get("menu_order_status"): # Display the order(s) status self.__order_status() # If the user has selected the Add Credit option... - elif selection == strings.menu_add_credit: + elif selection == self.loc.get("menu_add_credit"): # Display the add credit menu self.__add_credit_menu() # If the user has selected the Bot Info option... - elif selection == strings.menu_bot_info: + elif selection == self.loc.get("menu_bot_info"): # Display information about the bot self.__bot_info() # If the user has selected the Help option... - elif selection == strings.menu_help: + elif selection == self.loc.get("menu_help"): # Go to the Help menu self.__help_menu() @@ -392,7 +415,7 @@ def __order_menu(self): # Add the product to the cart cart[message['result']['message_id']] = [product, 0] # Create the inline keyboard to add the product to the cart - inline_keyboard = telegram.InlineKeyboardMarkup([[telegram.InlineKeyboardButton(strings.menu_add_to_cart, + inline_keyboard = telegram.InlineKeyboardMarkup([[telegram.InlineKeyboardButton(self.loc.get("menu_add_to_cart"), callback_data="cart_add")]]) # Edit the sent message and add the inline keyboard if product.image is None: @@ -406,10 +429,10 @@ def __order_menu(self): caption=product.text(), reply_markup=inline_keyboard) # Create the keyboard with the cancel button - inline_keyboard = telegram.InlineKeyboardMarkup([[telegram.InlineKeyboardButton(strings.menu_cancel, + inline_keyboard = telegram.InlineKeyboardMarkup([[telegram.InlineKeyboardButton(self.loc.get("menu_cancel"), callback_data="cart_cancel")]]) # Send a message containing the button to cancel or pay - final_msg = self.bot.send_message(self.chat.id, strings.conversation_cart_actions, reply_markup=inline_keyboard) + final_msg = self.bot.send_message(self.chat.id, self.loc.get("conversation_cart_actions"), reply_markup=inline_keyboard) # Wait for user input while True: callback = self.__wait_for_inlinekeyboard_callback() @@ -430,14 +453,14 @@ def __order_menu(self): # Create the product inline keyboard product_inline_keyboard = telegram.InlineKeyboardMarkup( [ - [telegram.InlineKeyboardButton(strings.menu_add_to_cart, callback_data="cart_add"), - telegram.InlineKeyboardButton(strings.menu_remove_from_cart, callback_data="cart_remove")] + [telegram.InlineKeyboardButton(self.loc.get("menu_add_to_cart"), callback_data="cart_add"), + telegram.InlineKeyboardButton(self.loc.get("menu_remove_from_cart"), callback_data="cart_remove")] ]) # Create the final inline keyboard final_inline_keyboard = telegram.InlineKeyboardMarkup( [ - [telegram.InlineKeyboardButton(strings.menu_cancel, callback_data="cart_cancel")], - [telegram.InlineKeyboardButton(strings.menu_done, callback_data="cart_done")] + [telegram.InlineKeyboardButton(self.loc.get("menu_cancel"), callback_data="cart_cancel")], + [telegram.InlineKeyboardButton(self.loc.get("menu_done"), callback_data="cart_done")] ]) # Edit both the product and the final message if product.image is None: @@ -454,7 +477,7 @@ def __order_menu(self): self.bot.edit_message_text( chat_id=self.chat.id, message_id=final_msg.message_id, - text=strings.conversation_confirm_cart.format(product_list=self.__get_cart_summary(cart), + text=self.loc.get("conversation_confirm_cart", product_list=self.__get_cart_summary(cart), total_cost=str(self.__get_cart_value(cart))), reply_markup=final_inline_keyboard) # If the Remove from cart button has been pressed... @@ -470,17 +493,17 @@ def __order_menu(self): else: continue # Create the product inline keyboard - product_inline_list = [[telegram.InlineKeyboardButton(strings.menu_add_to_cart, + product_inline_list = [[telegram.InlineKeyboardButton(self.loc.get("menu_add_to_cart"), callback_data="cart_add")]] if cart[callback.message.message_id][1] > 0: - product_inline_list[0].append(telegram.InlineKeyboardButton(strings.menu_remove_from_cart, + product_inline_list[0].append(telegram.InlineKeyboardButton(self.loc.get("menu_remove_from_cart"), callback_data="cart_remove")) product_inline_keyboard = telegram.InlineKeyboardMarkup(product_inline_list) # Create the final inline keyboard - final_inline_list = [[telegram.InlineKeyboardButton(strings.menu_cancel, callback_data="cart_cancel")]] + final_inline_list = [[telegram.InlineKeyboardButton(self.loc.get("menu_cancel"), callback_data="cart_cancel")]] for product_id in cart: if cart[product_id][1] > 0: - final_inline_list.append([telegram.InlineKeyboardButton(strings.menu_done, + final_inline_list.append([telegram.InlineKeyboardButton(self.loc.get("menu_done"), callback_data="cart_done")]) break final_inline_keyboard = telegram.InlineKeyboardMarkup(final_inline_list) @@ -498,18 +521,17 @@ def __order_menu(self): self.bot.edit_message_text( chat_id=self.chat.id, message_id=final_msg.message_id, - text=strings.conversation_confirm_cart.format(product_list=self.__get_cart_summary(cart), - total_cost=str(self.__get_cart_value(cart))), + text=self.loc.get("conversation_confirm_cart", product_list=self.__get_cart_summary(cart), total_cost=str(self.__get_cart_value(cart))), reply_markup=final_inline_keyboard) # If the done button has been pressed... elif callback.data == "cart_done": # End the loop break # Create an inline keyboard with a single skip button - cancel = telegram.InlineKeyboardMarkup([[telegram.InlineKeyboardButton(strings.menu_skip, + cancel = telegram.InlineKeyboardMarkup([[telegram.InlineKeyboardButton(self.loc.get("menu_skip"), callback_data="cmd_cancel")]]) # Ask if the user wants to add notes to the order - self.bot.send_message(self.chat.id, strings.ask_order_notes, reply_markup=cancel) + self.bot.send_message(self.chat.id, self.loc.get("ask_order_notes"), reply_markup=cancel) # Wait for user input notes = self.__wait_for_regex(r"(.*)", cancellable=True) # Create a new Order @@ -530,7 +552,7 @@ def __order_menu(self): credit_required = self.__get_cart_value(cart) - self.user.credit # Notify user in case of insufficient credit if credit_required > 0: - self.bot.send_message(self.chat.id, strings.error_not_enough_credit) + self.bot.send_message(self.chat.id, self.loc.get("error_not_enough_credit")) # Suggest payment for missing credit value if configuration allows refill if configloader.config["Credit Card"]["credit_card_token"] != "" \ and configloader.config["Appearance"]["refill_on_checkout"] == 'yes' \ @@ -580,20 +602,19 @@ def __order_transaction(self, order, value): def __order_notify_admins(self, order): # Notify the user of the order result - self.bot.send_message(self.chat.id, strings.success_order_created.format(order=order.get_text(self.session, - user=True))) + self.bot.send_message(self.chat.id, self.loc.get("success_order_created", order=order.get_text(self.session, user=True))) # Notify the admins (in Live Orders mode) of the new order admins = self.session.query(db.Admin).filter_by(live_mode=True).all() # Create the order keyboard order_keyboard = telegram.InlineKeyboardMarkup( [ - [telegram.InlineKeyboardButton(strings.menu_complete, callback_data="order_complete")], - [telegram.InlineKeyboardButton(strings.menu_refund, callback_data="order_refund")] + [telegram.InlineKeyboardButton(self.loc.get("menu_complete"), callback_data="order_complete")], + [telegram.InlineKeyboardButton(self.loc.get("menu_refund"), callback_data="order_refund")] ]) # Notify them of the new placed order for admin in admins: self.bot.send_message(admin.user_id, - f"{strings.notification_order_placed.format(order=order.get_text(self.session))}", + f"{self.loc.get('notification_order_placed', order=order.get_text(self.session))}", reply_markup=order_keyboard) def __order_status(self): @@ -607,7 +628,7 @@ def __order_status(self): .all() # Ensure there is at least one order to display if len(orders) == 0: - self.bot.send_message(self.chat.id, strings.error_no_orders) + self.bot.send_message(self.chat.id, self.loc.get("error_no_orders")) # Display the order status to the user for order in orders: self.bot.send_message(self.chat.id, order.get_text(self.session, user=True)) @@ -620,25 +641,25 @@ def __add_credit_menu(self): keyboard = list() # Add the supported payment methods to the keyboard # Cash - keyboard.append([telegram.KeyboardButton(strings.menu_cash)]) + keyboard.append([telegram.KeyboardButton(self.loc.get("menu_cash"))]) # Telegram Payments if configloader.config["Credit Card"]["credit_card_token"] != "": - keyboard.append([telegram.KeyboardButton(strings.menu_credit_card)]) + keyboard.append([telegram.KeyboardButton(self.loc.get("menu_credit_card"))]) # Keyboard: go back to the previous menu - keyboard.append([telegram.KeyboardButton(strings.menu_cancel)]) + keyboard.append([telegram.KeyboardButton(self.loc.get("menu_cancel"))]) # Send the keyboard to the user - self.bot.send_message(self.chat.id, strings.conversation_payment_method, + self.bot.send_message(self.chat.id, self.loc.get("conversation_payment_method"), reply_markup=telegram.ReplyKeyboardMarkup(keyboard, one_time_keyboard=True)) # Wait for a reply from the user - selection = self.__wait_for_specific_message([strings.menu_cash, strings.menu_credit_card, strings.menu_cancel], + selection = self.__wait_for_specific_message([self.loc.get("menu_cash"), self.loc.get("menu_credit_card"), self.loc.get("menu_cancel")], cancellable=True) # If the user has selected the Cash option... - if selection == strings.menu_cash: + if selection == self.loc.get("menu_cash"): # Go to the pay with cash function self.bot.send_message(self.chat.id, - strings.payment_cash.format(user_cash_id=self.user.identifiable_str())) + self.loc.get("payment_cash", user_cash_id=self.user.identifiable_str())) # If the user has selected the Credit Card option... - elif selection == strings.menu_credit_card: + elif selection == self.loc.get("menu_credit_card"): # Go to the pay with credit card function self.__add_credit_cc() # If the user has selected the Cancel option... @@ -652,16 +673,16 @@ def __add_credit_cc(self): # Create a keyboard to be sent later 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.append([telegram.KeyboardButton(strings.menu_cancel)]) + keyboard.append([telegram.KeyboardButton(self.loc.get("menu_cancel"))]) # Boolean variable to check if the user has cancelled the action cancelled = False # Loop used to continue asking if there's an error during the input while not cancelled: # Send the message and the keyboard - self.bot.send_message(self.chat.id, strings.payment_cc_amount, + self.bot.send_message(self.chat.id, self.loc.get("payment_cc_amount"), reply_markup=telegram.ReplyKeyboardMarkup(keyboard, one_time_keyboard=True)) # Wait until a valid amount is sent - selection = self.__wait_for_regex(r"([0-9]+(?:[.,][0-9]+)?|" + strings.menu_cancel + r")", cancellable=True) + selection = self.__wait_for_regex(r"([0-9]+(?:[.,][0-9]+)?|" + self.loc.get("menu_cancel") + r")", cancellable=True) # If the user cancelled the action if isinstance(selection, CancelSignal): # Exit the loop @@ -672,15 +693,11 @@ def __add_credit_cc(self): # Ensure the amount is within the range if value > utils.Price(int(configloader.config["Credit Card"]["max_amount"])): self.bot.send_message(self.chat.id, - strings.error_payment_amount_over_max.format( - max_amount=utils.Price(configloader.config["Credit Card"]["max_amount"])) - ) + self.loc.get("error_payment_amount_over_max", max_amount=utils.Price(configloader.config["Credit Card"]["max_amount"]))) continue elif value < utils.Price(int(configloader.config["Credit Card"]["min_amount"])): self.bot.send_message(self.chat.id, - strings.error_payment_amount_under_min.format( - min_amount=utils.Price(configloader.config["Credit Card"]["min_amount"])) - ) + self.loc.get("error_payment_amount_under_min", min_amount=utils.Price(configloader.config["Credit Card"]["min_amount"]))) continue break # If the user cancelled the action... @@ -694,20 +711,20 @@ def __make_payment(self, amount): # Set the invoice active invoice payload self.invoice_payload = str(uuid.uuid4()) # Create the price array - prices = [telegram.LabeledPrice(label=strings.payment_invoice_label, amount=int(amount))] + prices = [telegram.LabeledPrice(label=self.loc.get("payment_invoice_label"), amount=int(amount))] # If the user has to pay a fee when using the credit card, add it to the prices list fee = int(self.__get_total_fee(amount)) if fee > 0: - prices.append(telegram.LabeledPrice(label=strings.payment_invoice_fee_label, + prices.append(telegram.LabeledPrice(label=self.loc.get("payment_invoice_fee_label"), amount=fee)) # Create the invoice keyboard - inline_keyboard = telegram.InlineKeyboardMarkup([[telegram.InlineKeyboardButton(strings.menu_pay, pay=True)], - [telegram.InlineKeyboardButton(strings.menu_cancel, + inline_keyboard = telegram.InlineKeyboardMarkup([[telegram.InlineKeyboardButton(self.loc.get("menu_pay"), pay=True)], + [telegram.InlineKeyboardButton(self.loc.get("menu_cancel"), callback_data="cmd_cancel")]]) # The amount is valid, send the invoice self.bot.send_invoice(self.chat.id, - title=strings.payment_invoice_title, - description=strings.payment_invoice_description.format(amount=str(amount)), + title=self.loc.get("payment_invoice_title"), + description=self.loc.get("payment_invoice_description", amount=str(amount)), payload=self.invoice_payload, provider_token=configloader.config["Credit Card"]["credit_card_token"], start_parameter="tempdeeplink", @@ -757,7 +774,7 @@ def __get_total_fee(amount): def __bot_info(self): """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, self.loc.get("bot_info")) def __admin_menu(self): """Function called from the run method when the user is an administrator. @@ -768,52 +785,52 @@ def __admin_menu(self): # Create a keyboard with the admin main menu based on the admin permissions specified in the db keyboard = [] if self.admin.edit_products: - keyboard.append([strings.menu_products]) + keyboard.append([self.loc.get("menu_products")]) if self.admin.receive_orders: - keyboard.append([strings.menu_orders]) + keyboard.append([self.loc.get("menu_orders")]) if self.admin.create_transactions: - keyboard.append([strings.menu_edit_credit]) - keyboard.append([strings.menu_transactions, strings.menu_csv]) + keyboard.append([self.loc.get("menu_edit_credit")]) + keyboard.append([self.loc.get("menu_transactions"), self.loc.get("menu_csv")]) if self.admin.is_owner: - keyboard.append([strings.menu_edit_admins]) - keyboard.append([strings.menu_user_mode]) + keyboard.append([self.loc.get("menu_edit_admins")]) + keyboard.append([self.loc.get("menu_user_mode")]) # Send the previously created keyboard to the user (ensuring it can be clicked only 1 time) - self.bot.send_message(self.chat.id, strings.conversation_open_admin_menu, + self.bot.send_message(self.chat.id, self.loc.get("conversation_open_admin_menu"), reply_markup=telegram.ReplyKeyboardMarkup(keyboard, one_time_keyboard=True), ) # Wait for a reply from the user - selection = self.__wait_for_specific_message([strings.menu_products, strings.menu_orders, - strings.menu_user_mode, strings.menu_edit_credit, - strings.menu_transactions, strings.menu_csv, - strings.menu_edit_admins]) + selection = self.__wait_for_specific_message([self.loc.get("menu_products"), self.loc.get("menu_orders"), + self.loc.get("menu_user_mode"), self.loc.get("menu_edit_credit"), + self.loc.get("menu_transactions"), self.loc.get("menu_csv"), + self.loc.get("menu_edit_admins")]) # If the user has selected the Products option... - if selection == strings.menu_products: + if selection == self.loc.get("menu_products"): # Open the products menu self.__products_menu() # If the user has selected the Orders option... - elif selection == strings.menu_orders: + elif selection == self.loc.get("menu_orders"): # Open the orders menu self.__orders_menu() # If the user has selected the Transactions option... - elif selection == strings.menu_edit_credit: + elif selection == self.loc.get("menu_edit_credit"): # Open the edit credit menu self.__create_transaction() # If the user has selected the User mode option... - elif selection == strings.menu_user_mode: + elif selection == self.loc.get("menu_user_mode"): # Tell the user how to go back to admin menu - self.bot.send_message(self.chat.id, strings.conversation_switch_to_user_mode) + self.bot.send_message(self.chat.id, self.loc.get("conversation_switch_to_user_mode")) # Start the bot in user mode self.__user_menu() # If the user has selected the Add Admin option... - elif selection == strings.menu_edit_admins: + elif selection == self.loc.get("menu_edit_admins"): # Open the edit admin menu self.__add_admin() # If the user has selected the Transactions option... - elif selection == strings.menu_transactions: + elif selection == self.loc.get("menu_transactions"): # Open the transaction pages self.__transaction_pages() # If the user has selected the .csv option... - elif selection == strings.menu_csv: + elif selection == self.loc.get("menu_csv"): # Generate the .csv file self.__transactions_file() @@ -825,13 +842,13 @@ def __products_menu(self): # Create a list of product names product_names = [product.name for product in products] # Insert at the start of the list the add product option, the remove product option and the Cancel option - product_names.insert(0, strings.menu_cancel) - product_names.insert(1, strings.menu_add_product) - product_names.insert(2, strings.menu_delete_product) + product_names.insert(0, self.loc.get("menu_cancel")) + product_names.insert(1, self.loc.get("menu_add_product")) + product_names.insert(2, self.loc.get("menu_delete_product")) # Create a keyboard using the product names keyboard = [[telegram.KeyboardButton(product_name)] for product_name in product_names] # Send the previously created keyboard to the user (ensuring it can be clicked only 1 time) - self.bot.send_message(self.chat.id, strings.conversation_admin_select_product, + self.bot.send_message(self.chat.id, self.loc.get("conversation_admin_select_product"), reply_markup=telegram.ReplyKeyboardMarkup(keyboard, one_time_keyboard=True)) # Wait for a reply from the user selection = self.__wait_for_specific_message(product_names, cancellable=True) @@ -840,11 +857,11 @@ def __products_menu(self): # Exit the menu return # If the user has selected the Add Product option... - elif selection == strings.menu_add_product: + elif selection == self.loc.get("menu_add_product"): # Open the add product menu self.__edit_product_menu() # If the user has selected the Remove Product option... - elif selection == strings.menu_delete_product: + elif selection == self.loc.get("menu_delete_product"): # Open the delete product menu self.__delete_product_menu() # If the user has selected a product @@ -858,15 +875,15 @@ def __edit_product_menu(self, product: Optional[db.Product] = None): """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 - cancel = telegram.InlineKeyboardMarkup([[telegram.InlineKeyboardButton(strings.menu_skip, + cancel = telegram.InlineKeyboardMarkup([[telegram.InlineKeyboardButton(self.loc.get("menu_skip"), callback_data="cmd_cancel")]]) # Ask for the product name until a valid product name is specified while True: # Ask the question to the user - self.bot.send_message(self.chat.id, strings.ask_product_name) + self.bot.send_message(self.chat.id, self.loc.get("ask_product_name")) # Display the current name if you're editing an existing product if product: - self.bot.send_message(self.chat.id, strings.edit_current_value.format(value=escape(product.name)), + self.bot.send_message(self.chat.id, self.loc.get("edit_current_value", value=escape(product.name)), reply_markup=cancel) # Wait for an answer name = self.__wait_for_regex(r"(.*)", cancellable=bool(product)) @@ -875,23 +892,23 @@ def __edit_product_menu(self, product: Optional[db.Product] = None): self.session.query(db.Product).filter_by(name=name, deleted=False).one_or_none() in [None, product]: # Exit the loop break - self.bot.send_message(self.chat.id, strings.error_duplicate_name) + self.bot.send_message(self.chat.id, self.loc.get("error_duplicate_name")) # Ask for the product description - self.bot.send_message(self.chat.id, strings.ask_product_description) + self.bot.send_message(self.chat.id, self.loc.get("ask_product_description")) # Display the current description if you're editing an existing product if product: self.bot.send_message(self.chat.id, - strings.edit_current_value.format(value=escape(product.description)), + self.loc.get("edit_current_value", value=escape(product.description)), reply_markup=cancel) # Wait for an answer description = self.__wait_for_regex(r"(.*)", cancellable=bool(product)) # Ask for the product price self.bot.send_message(self.chat.id, - strings.ask_product_price) + self.loc.get("ask_product_price")) # Display the current name if you're editing an existing product if product: self.bot.send_message(self.chat.id, - strings.edit_current_value.format( + self.loc.get("edit_current_value", value=(str(utils.Price(product.price)) if product.price is not None else 'Non in vendita')), reply_markup=cancel) @@ -906,7 +923,7 @@ def __edit_product_menu(self, product: Optional[db.Product] = None): else: price = utils.Price(price) # Ask for the product image - self.bot.send_message(self.chat.id, strings.ask_product_image, reply_markup=cancel) + self.bot.send_message(self.chat.id, self.loc.get("ask_product_image"), reply_markup=cancel) # Wait for an answer photo_list = self.__wait_for_photo(cancellable=True) # If a new product is being added... @@ -935,14 +952,14 @@ def __edit_product_menu(self, product: Optional[db.Product] = None): # Get the file object associated with the photo photo_file = self.bot.get_file(largest_photo.file_id) # Notify the user that the bot is downloading the image and might be inactive for a while - self.bot.send_message(self.chat.id, strings.downloading_image) + self.bot.send_message(self.chat.id, self.loc.get("downloading_image")) self.bot.send_chat_action(self.chat.id, action="upload_photo") # Set the image for that product product.set_image(photo_file) # Commit the session changes self.session.commit() # Notify the user - self.bot.send_message(self.chat.id, strings.success_product_edited) + self.bot.send_message(self.chat.id, self.loc.get("success_product_edited")) def __delete_product_menu(self): log.debug("Displaying __delete_product_menu") @@ -951,11 +968,11 @@ def __delete_product_menu(self): # Create a list of product names product_names = [product.name for product in products] # Insert at the start of the list the Cancel button - product_names.insert(0, strings.menu_cancel) + product_names.insert(0, self.loc.get("menu_cancel")) # Create a keyboard using the product names keyboard = [[telegram.KeyboardButton(product_name)] for product_name in product_names] # Send the previously created keyboard to the user (ensuring it can be clicked only 1 time) - self.bot.send_message(self.chat.id, strings.conversation_admin_select_product_to_delete, + self.bot.send_message(self.chat.id, self.loc.get("conversation_admin_select_product_to_delete"), reply_markup=telegram.ReplyKeyboardMarkup(keyboard, one_time_keyboard=True)) # Wait for a reply from the user selection = self.__wait_for_specific_message(product_names, cancellable=True) @@ -969,22 +986,22 @@ def __delete_product_menu(self): product.deleted = True self.session.commit() # Notify the user - self.bot.send_message(self.chat.id, strings.success_product_deleted) + self.bot.send_message(self.chat.id, self.loc.get("success_product_deleted")) def __orders_menu(self): """Display a live flow of orders.""" log.debug("Displaying __orders_menu") # Create a cancel and a stop keyboard - stop_keyboard = telegram.InlineKeyboardMarkup([[telegram.InlineKeyboardButton(strings.menu_stop, + stop_keyboard = telegram.InlineKeyboardMarkup([[telegram.InlineKeyboardButton(self.loc.get("menu_stop"), callback_data="cmd_cancel")]]) - cancel_keyboard = telegram.InlineKeyboardMarkup([[telegram.InlineKeyboardButton(strings.menu_cancel, + cancel_keyboard = telegram.InlineKeyboardMarkup([[telegram.InlineKeyboardButton(self.loc.get("menu_cancel"), callback_data="cmd_cancel")]]) # Send a small intro message on the Live Orders mode - self.bot.send_message(self.chat.id, strings.conversation_live_orders_start, reply_markup=stop_keyboard) + self.bot.send_message(self.chat.id, self.loc.get("conversation_live_orders_start"), reply_markup=stop_keyboard) # Create the order keyboard - order_keyboard = telegram.InlineKeyboardMarkup([[telegram.InlineKeyboardButton(strings.menu_complete, + order_keyboard = telegram.InlineKeyboardMarkup([[telegram.InlineKeyboardButton(self.loc.get("menu_complete"), callback_data="order_complete")], - [telegram.InlineKeyboardButton(strings.menu_refund, + [telegram.InlineKeyboardButton(self.loc.get("menu_refund"), callback_data="order_refund")]]) # Display the past pending orders orders = self.session.query(db.Order) \ @@ -1010,12 +1027,12 @@ def __orders_menu(self): self.admin.live_mode = False break # Find the order - order_id = re.search(strings.order_number.replace("{id}", "([0-9]+)"), update.message.text).group(1) + order_id = re.search(self.loc.get("order_number").replace("{id}", "([0-9]+)"), update.message.text).group(1) order = self.session.query(db.Order).filter(db.Order.order_id == order_id).one() # Check if the order hasn't been already cleared if order.delivery_date is not None or order.refund_date is not None: # Notify the admin and skip that order - self.bot.edit_message_text(self.chat.id, strings.error_order_already_cleared) + self.bot.edit_message_text(self.chat.id, self.loc.get("error_order_already_cleared")) break # If the user pressed the complete order button, complete the order if update.data == "order_complete": @@ -1028,12 +1045,11 @@ def __orders_menu(self): message_id=update.message.message_id) # Notify the user of the completition self.bot.send_message(order.user_id, - strings.notification_order_completed.format(order=order.get_text(self.session, - user=True))) + self.loc.get("notification_order_completed", order=order.get_text(self.session, user=True))) # If the user pressed the refund order button, refund the order... elif update.data == "order_refund": # Ask for a refund reason - reason_msg = self.bot.send_message(self.chat.id, strings.ask_refund_reason, + reason_msg = self.bot.send_message(self.chat.id, self.loc.get("ask_refund_reason"), reply_markup=cancel_keyboard) # Wait for a reply reply = self.__wait_for_regex("(.*)", cancellable=True) @@ -1058,10 +1074,10 @@ def __orders_menu(self): message_id=update.message.message_id) # Notify the user of the refund self.bot.send_message(order.user_id, - strings.notification_order_refunded.format(order=order.get_text(self.session, + self.loc.get("notification_order_refunded", order=order.get_text(self.session, user=True))) # Notify the admin of the refund - self.bot.send_message(self.chat.id, strings.success_order_refunded.format(order_id=order.order_id)) + self.bot.send_message(self.chat.id, self.loc.get("success_order_refunded", order_id=order.order_id)) def __create_transaction(self): """Edit manually the credit of an user.""" @@ -1072,10 +1088,10 @@ def __create_transaction(self): if isinstance(user, CancelSignal): return # Create an inline keyboard with a single cancel button - cancel = telegram.InlineKeyboardMarkup([[telegram.InlineKeyboardButton(strings.menu_cancel, + cancel = telegram.InlineKeyboardMarkup([[telegram.InlineKeyboardButton(self.loc.get("menu_cancel"), callback_data="cmd_cancel")]]) # Request from the user the amount of money to be credited manually - self.bot.send_message(self.chat.id, strings.ask_credit, reply_markup=cancel) + self.bot.send_message(self.chat.id, self.loc.get("ask_credit"), reply_markup=cancel) # Wait for an answer reply = self.__wait_for_regex(r"(-? ?[0-9]{1,3}(?:[.,][0-9]{1,2})?)", cancellable=True) # Allow the cancellation of the operation @@ -1084,7 +1100,7 @@ def __create_transaction(self): # Convert the reply to a price object price = utils.Price(reply) # Ask the user for notes - self.bot.send_message(self.chat.id, strings.ask_transaction_notes, reply_markup=cancel) + self.bot.send_message(self.chat.id, self.loc.get("ask_transaction_notes"), reply_markup=cancel) # Wait for an answer reply = self.__wait_for_regex(r"(.*)", cancellable=True) # Allow the cancellation of the operation @@ -1102,36 +1118,36 @@ def __create_transaction(self): self.session.commit() # Notify the user of the credit/debit self.bot.send_message(user.user_id, - strings.notification_transaction_created.format(transaction=str(transaction))) + self.loc.get("notification_transaction_created", transaction=str(transaction))) # Notify the admin of the success - self.bot.send_message(self.chat.id, strings.success_transaction_created.format(transaction=str(transaction))) + self.bot.send_message(self.chat.id, self.loc.get("success_transaction_created", transaction=str(transaction))) def __help_menu(self): """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 - keyboard = [[telegram.KeyboardButton(strings.menu_guide)], - [telegram.KeyboardButton(strings.menu_contact_shopkeeper)], - [telegram.KeyboardButton(strings.menu_cancel)]] + keyboard = [[telegram.KeyboardButton(self.loc.get("menu_guide"))], + [telegram.KeyboardButton(self.loc.get("menu_contact_shopkeeper"))], + [telegram.KeyboardButton(self.loc.get("menu_cancel"))]] # Send the previously created keyboard to the user (ensuring it can be clicked only 1 time) self.bot.send_message(self.chat.id, - strings.conversation_open_help_menu, + self.loc.get("conversation_open_help_menu"), reply_markup=telegram.ReplyKeyboardMarkup(keyboard, one_time_keyboard=True)) # Wait for a reply from the user - selection = self.__wait_for_specific_message([strings.menu_guide, strings.menu_contact_shopkeeper, - strings.menu_cancel]) + selection = self.__wait_for_specific_message([self.loc.get("menu_guide"), self.loc.get("menu_contact_shopkeeper"), + self.loc.get("menu_cancel")]) # If the user has selected the Guide option... - if selection == strings.menu_guide: + if selection == self.loc.get("menu_guide"): # Send them the bot guide - self.bot.send_message(self.chat.id, strings.help_msg) + self.bot.send_message(self.chat.id, self.loc.get("help_msg")) # If the user has selected the Order Status option... - elif selection == strings.menu_contact_shopkeeper: + elif selection == self.loc.get("menu_contact_shopkeeper"): # Find the list of available shopkeepers shopkeepers = self.session.query(db.Admin).filter_by(display_on_help=True).join(db.User).all() # Create the string shopkeepers_string = "\n".join([admin.user.mention() for admin in shopkeepers]) # Send the message to the user - self.bot.send_message(self.chat.id, strings.contact_shopkeeper.format(shopkeepers=shopkeepers_string)) + self.bot.send_message(self.chat.id, self.loc.get("contact_shopkeeper", shopkeepers=shopkeepers_string)) # If the user has selected the Cancel option the function will return immediately def __transaction_pages(self): @@ -1140,7 +1156,7 @@ def __transaction_pages(self): # Page number page = 0 # Create and send a placeholder message to be populated - message = self.bot.send_message(self.chat.id, strings.loading_transactions) + message = self.bot.send_message(self.chat.id, self.loc.get("loading_transactions")) # Loop used to move between pages while True: # Retrieve the 10 transactions in that page @@ -1155,22 +1171,21 @@ def __transaction_pages(self): if page != 0: # Add a previous page button inline_keyboard_list[0].append( - telegram.InlineKeyboardButton(strings.menu_previous, callback_data="cmd_previous") + telegram.InlineKeyboardButton(self.loc.get("menu_previous"), callback_data="cmd_previous") ) # Don't add a next page button if this is the last page if len(transactions) == 10: # Add a next page button inline_keyboard_list[0].append( - telegram.InlineKeyboardButton(strings.menu_next, callback_data="cmd_next") + telegram.InlineKeyboardButton(self.loc.get("menu_next"), callback_data="cmd_next") ) # Add a Done button - inline_keyboard_list.append([telegram.InlineKeyboardButton(strings.menu_done, callback_data="cmd_done")]) + inline_keyboard_list.append([telegram.InlineKeyboardButton(self.loc.get("menu_done"), callback_data="cmd_done")]) # Create the inline keyboard markup inline_keyboard = telegram.InlineKeyboardMarkup(inline_keyboard_list) # Create the message text transactions_string = "\n".join([str(transaction) for transaction in transactions]) - text = strings.transactions_page.format(page=page + 1, - transactions=transactions_string) + text = self.loc.get("transactions_page", page=page + 1, transactions=transactions_string) # Update the previously sent message self.bot.edit_message_text(chat_id=self.chat.id, message_id=message.message_id, text=text, reply_markup=inline_keyboard) @@ -1224,7 +1239,7 @@ def __transactions_file(self): f"{transaction.payment_email if transaction.payment_email is not None else ''};" f"{transaction.refunded if transaction.refunded is not None else ''}\n") # Describe the file to the user - self.bot.send_message(self.chat.id, strings.csv_caption) + self.bot.send_message(self.chat.id, self.loc.get("csv_caption")) # Reopen the file for reading with open(f"transactions_{self.chat.id}.csv") as file: # Send the file via a manual request to Telegram @@ -1247,13 +1262,13 @@ def __add_admin(self): admin = self.session.query(db.Admin).filter_by(user_id=user.user_id).one_or_none() if admin is None: # Create the keyboard to be sent - keyboard = telegram.ReplyKeyboardMarkup([[strings.emoji_yes, strings.emoji_no]], one_time_keyboard=True) + keyboard = telegram.ReplyKeyboardMarkup([[self.loc.get("emoji_yes"), self.loc.get("emoji_no")]], one_time_keyboard=True) # Ask for confirmation - self.bot.send_message(self.chat.id, strings.conversation_confirm_admin_promotion, reply_markup=keyboard) + self.bot.send_message(self.chat.id, self.loc.get("conversation_confirm_admin_promotion"), reply_markup=keyboard) # Wait for an answer - selection = self.__wait_for_specific_message([strings.emoji_yes, strings.emoji_no]) + selection = self.__wait_for_specific_message([self.loc.get("emoji_yes"), self.loc.get("emoji_no")]) # Proceed only if the answer is yes - if selection == strings.emoji_no: + if selection == self.loc.get("emoji_no"): return # Create a new admin admin = db.Admin(user=user, @@ -1264,22 +1279,22 @@ def __add_admin(self): display_on_help=False) self.session.add(admin) # Send the empty admin message and record the id - message = self.bot.send_message(self.chat.id, strings.admin_properties.format(name=str(admin.user))) + message = self.bot.send_message(self.chat.id, self.loc.get("admin_properties", name=str(admin.user))) # Start accepting edits while True: # Create the inline keyboard with the admin status inline_keyboard = telegram.InlineKeyboardMarkup([ - [telegram.InlineKeyboardButton(f"{utils.boolmoji(admin.edit_products)} {strings.prop_edit_products}", + [telegram.InlineKeyboardButton(f"{utils.boolmoji(admin.edit_products)} {self.loc.get('prop_edit_products')}", callback_data="toggle_edit_products")], - [telegram.InlineKeyboardButton(f"{utils.boolmoji(admin.receive_orders)} {strings.prop_receive_orders}", + [telegram.InlineKeyboardButton(f"{utils.boolmoji(admin.receive_orders)} {self.loc.get('prop_receive_orders')}", callback_data="toggle_receive_orders")], [telegram.InlineKeyboardButton( - f"{utils.boolmoji(admin.create_transactions)} {strings.prop_create_transactions}", + f"{utils.boolmoji(admin.create_transactions)} {self.loc.get('prop_create_transactions')}", callback_data="toggle_create_transactions")], [telegram.InlineKeyboardButton( - f"{utils.boolmoji(admin.display_on_help)} {strings.prop_display_on_help}", + f"{utils.boolmoji(admin.display_on_help)} {self.loc.get('prop_display_on_help')}", callback_data="toggle_display_on_help")], - [telegram.InlineKeyboardButton(strings.menu_done, callback_data="cmd_done")] + [telegram.InlineKeyboardButton(self.loc.get('menu_done'), callback_data="cmd_done")] ]) # Update the inline keyboard self.bot.edit_message_reply_markup(message_id=message.message_id, @@ -1306,7 +1321,7 @@ def __graceful_stop(self, stop_trigger: StopSignal): # If the session has expired... if stop_trigger.reason == "timeout": # Notify the user that the session has expired and remove the keyboard - self.bot.send_message(self.chat.id, strings.conversation_expired, + self.bot.send_message(self.chat.id, self.loc.get('conversation_expired'), reply_markup=telegram.ReplyKeyboardRemove()) # If a restart has been requested... # Do nothing. From 8356565bd29f4e8ef336d8e14d34572e841f59d3 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Sun, 3 May 2020 01:17:22 +0200 Subject: [PATCH 2/5] Fix template_config.ini --- config/template_config.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/template_config.ini b/config/template_config.ini index 5941364..6f63b8e 100644 --- a/config/template_config.ini +++ b/config/template_config.ini @@ -15,7 +15,7 @@ is_template = yes ; en - English, by https://github.com/DarrenWestwood (incomplete, please improve it!) ; uk - Ukrainian, by https://github.com/pzhuk ; ru - Russian, by https://github.com/pzhuk -language = it_IT +language = it # Telegram bot parameters [Telegram] From 5c79ec75814e1925bccb6c43e10066b843d9ec76 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Sun, 3 May 2020 11:41:12 +0200 Subject: [PATCH 3/5] Improve language configuration options --- config/template_config.ini | 16 +++++++++----- core.py | 3 ++- database.py | 15 ++++++++----- localization.py | 43 +++++++++++++++++++++++++------------- worker.py | 21 +++++++------------ 5 files changed, 60 insertions(+), 38 deletions(-) diff --git a/config/template_config.ini b/config/template_config.ini index 6f63b8e..0545ac4 100644 --- a/config/template_config.ini +++ b/config/template_config.ini @@ -8,14 +8,20 @@ version = 18 ; Set this to no when you are done editing the file is_template = yes -; Language code for string file -; Uses the https://en.wikipedia.org/wiki/IETF_language_tag name of the language + +# Language parameters +[Language] ; Available languages: -; it - Italian, by Steffo -; en - English, by https://github.com/DarrenWestwood (incomplete, please improve it!) +; it - Italian, by https://github.com/Steffo99 +; en - English, by https://github.com/DarrenWestwood ; uk - Ukrainian, by https://github.com/pzhuk ; ru - Russian, by https://github.com/pzhuk -language = it +; The lanugages that messages can be displayed in +enabled_languages = it | en | uk | ru +; The default language to be set for users whose language cannot be autodetected or whose language is not enabled +default_language = it +; The language to fallback to if a string is missing in a specific language +fallback_language = en # Telegram bot parameters [Telegram] diff --git a/core.py b/core.py index fb4a8e0..a318191 100644 --- a/core.py +++ b/core.py @@ -47,7 +47,8 @@ def main(): # Finding default language default_language = configloader.config["Config"]["language"] - default_loc = localization.Localization(default_language) + # Creating localization object + default_loc = localization.Localization(language=default_language, fallback=default_language) # Create a dictionary linking the chat ids to the Worker objects # {"1234": } diff --git a/database.py b/database.py index 90e0128..0745056 100644 --- a/database.py +++ b/database.py @@ -8,6 +8,9 @@ import requests import utils import importlib +import logging + +log = logging.getLogger(__name__) language = configloader.config["Config"]["language"] strings = importlib.import_module("strings." + language) @@ -31,6 +34,7 @@ class User(TableDeclarativeBase): first_name = Column(String, nullable=False) last_name = Column(String) username = Column(String) + language = Column(String, nullable=False) # Current wallet credit credit = Column(Integer, nullable=False) @@ -38,14 +42,15 @@ class User(TableDeclarativeBase): # Extra table parameters __tablename__ = "users" - def __init__(self, telegram_chat: telegram.Chat, **kwargs): + def __init__(self, telegram_user: telegram.User, **kwargs): # Initialize the super super().__init__(**kwargs) # Get the data from telegram - self.user_id = telegram_chat.id - self.first_name = telegram_chat.first_name - self.last_name = telegram_chat.last_name - self.username = telegram_chat.username + self.user_id = telegram_user.id + self.first_name = telegram_user.first_name + self.last_name = telegram_user.last_name + self.username = telegram_user.username + self.language = telegram_user.language_code if telegram_user.language_code else configloader.config["Language"]["default_language"] # The starting wallet value is 0 self.credit = 0 diff --git a/localization.py b/localization.py index 84f3516..ee02f87 100644 --- a/localization.py +++ b/localization.py @@ -1,4 +1,10 @@ +from typing import * import importlib +import types +import logging + + +log = logging.getLogger(__name__) class IgnoreDict(dict): @@ -8,22 +14,31 @@ def __missing__(self, key): class Localization: - def __init__(self, language, replacements=None): - self.language = language - self.module = importlib.import_module("strings." + language) - self.replacements = replacements if replacements else {} - - @staticmethod - def is_supported(language) -> bool: - try: - importlib.import_module("strings." + language) - except ImportError: - return False + def __init__(self, language: str, *, fallback: str, replacements: Dict[str, str] = None): + log.debug(f"Creating localization for {language}") + self.language: str = language + log.debug(f"Importing strings.{language}") + self.module: types.ModuleType = importlib.import_module(f"strings.{language}") + if language != fallback: + log.debug(f"Importing strings.{fallback} as fallback") + self.fallback_language: str = fallback + self.fallback_module = importlib.import_module(f"strings.{fallback}") if fallback else None else: - return True + log.debug("Language is the same as the default, not importing any fallback") + self.fallback_language = None + self.fallback_module = None + self.replacements: Dict[str, str] = replacements if replacements else {} - def get(self, key, **kwargs) -> str: - string = self.module.__getattribute__(key) + def get(self, key: str, **kwargs) -> str: + try: + log.debug(f"Getting localized string with key {key}") + string = self.module.__getattribute__(key) + except AttributeError: + if self.fallback_module: + log.warning(f"Missing localized string with key {key}, using default") + string = self.fallback_module.__getattribute__(key) + else: + raise assert isinstance(string, str) formatter = IgnoreDict(**self.replacements, **kwargs) return string.format_map(formatter) diff --git a/worker.py b/worker.py index afaf5b3..cc44590 100644 --- a/worker.py +++ b/worker.py @@ -78,7 +78,7 @@ def run(self): # Check if there are other registered users: if there aren't any, the first user will be owner of the bot will_be_owner = (self.session.query(db.Admin).first() is None) # Create the new record - self.user = db.User(self.chat) + self.user = db.User(self.telegram_user) # Add the new record to the db self.session.add(self.user) # Flush the session to get an userid @@ -100,20 +100,15 @@ def run(self): 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}") - # Detect language from Telegram metadata - default_language = configloader.config["Config"]["language"] - language = self.telegram_user.language_code - if language: - log.debug(f"Detected language: {language}") - if not localization.Localization.is_supported(language): - log.debug(f"Unsupported language, using default: {default_language}") - language = default_language - else: - log.debug(f"No language detected, using default: {default_language}") - language = default_language + # Check if the user's language is enabled; if it isn't, change it to the default + if self.user.language not in configloader.config["Language"]["enabled_languages"]: + log.debug(f"User's language '{self.user.language}' is not enabled, changing it to the default") + self.user.language = configloader.config["Language"]["default_language"] + self.session.commit() # Create a Localization object self.loc = localization.Localization( - language=language, + language=self.user.language, + fallback=configloader.config["Language"]["fallback_language"], replacements={ "user_string": str(self.user), "user_mention": self.user.mention(), From 6b3fb880cbe9bc856ce09f93a1bf694970cdfe48 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Sun, 3 May 2020 12:40:11 +0200 Subject: [PATCH 4/5] Fully remove global strings files --- core.py | 2 +- database.py | 83 ++++++++++---------- localization.py | 3 + strings/en.py | 6 ++ strings/it.py | 6 ++ utils.py | 46 +++++------ worker.py | 201 ++++++++++++++++++++++++++++++++---------------- 7 files changed, 209 insertions(+), 138 deletions(-) diff --git a/core.py b/core.py index a318191..781dc1d 100644 --- a/core.py +++ b/core.py @@ -46,7 +46,7 @@ def main(): log.debug("Bot token is valid!") # Finding default language - default_language = configloader.config["Config"]["language"] + default_language = configloader.config["Language"]["default_language"] # Creating localization object default_loc = localization.Localization(language=default_language, fallback=default_language) diff --git a/database.py b/database.py index 0745056..8d8b99b 100644 --- a/database.py +++ b/database.py @@ -7,14 +7,11 @@ import telegram import requests import utils -import importlib +import localization import logging log = logging.getLogger(__name__) -language = configloader.config["Config"]["language"] -strings = importlib.import_module("strings." + language) - # Create a (lazy) database engine engine = create_engine(configloader.config["Database"]["engine"]) @@ -50,7 +47,8 @@ def __init__(self, telegram_user: telegram.User, **kwargs): self.first_name = telegram_user.first_name self.last_name = telegram_user.last_name self.username = telegram_user.username - self.language = telegram_user.language_code if telegram_user.language_code else configloader.config["Language"]["default_language"] + self.language = telegram_user.language_code if telegram_user.language_code else configloader.config["Language"][ + "default_language"] # The starting wallet value is 0 self.credit = 0 @@ -111,40 +109,37 @@ class Product(TableDeclarativeBase): # No __init__ is needed, the default one is sufficient - def __str__(self): - return self.text() - - def text(self, style: str="full", cart_qty: int=None): + def text(self, *, loc: localization.Localization, style: str = "full", cart_qty: int = None): """Return the product details formatted with Telegram HTML. The image is omitted.""" if style == "short": - return f"{cart_qty}x {utils.telegram_html_escape(self.name)} - {str(utils.Price(self.price) * cart_qty)}" + return f"{cart_qty}x {utils.telegram_html_escape(self.name)} - {str(utils.Price(self.price, loc) * cart_qty)}" elif style == "full": if cart_qty is not None: - cart = strings.in_cart_format_string.format(quantity=cart_qty) + cart = loc.get("in_cart_format_string", quantity=cart_qty) else: cart = '' - return strings.product_format_string.format(name=utils.telegram_html_escape(self.name), - description=utils.telegram_html_escape(self.description), - price=str(utils.Price(self.price)), - cart=cart) + return loc.get("product_format_string", name=utils.telegram_html_escape(self.name), + description=utils.telegram_html_escape(self.description), + price=str(utils.Price(self.price, loc)), + cart=cart) else: raise ValueError("style is not an accepted value") def __repr__(self): return f"" - def send_as_message(self, chat_id: int) -> dict: + def send_as_message(self, loc: localization.Localization, chat_id: int) -> dict: """Send a message containing the product data.""" if self.image is None: r = requests.get(f"https://api.telegram.org/bot{configloader.config['Telegram']['token']}/sendMessage", params={"chat_id": chat_id, - "text": self.text(), + "text": self.text(loc=loc), "parse_mode": "HTML"}) else: r = requests.post(f"https://api.telegram.org/bot{configloader.config['Telegram']['token']}/sendPhoto", files={"photo": self.image}, params={"chat_id": chat_id, - "caption": self.text(), + "caption": self.text(loc=loc), "parse_mode": "HTML"}) return r.json() @@ -193,10 +188,10 @@ class Transaction(TableDeclarativeBase): __tablename__ = "transactions" __table_args__ = (UniqueConstraint("provider", "provider_charge_id"),) - def __str__(self): - string = f"T{self.transaction_id} | {str(self.user)} | {utils.Price(self.value)}" + def text(self, *, loc: localization.Localization): + string = f"T{self.transaction_id} | {str(self.user)} | {utils.Price(self.value, loc)}" if self.refunded: - string += f" | {strings.emoji_refunded}" + string += f" | {loc.get('emoji_refunded')}" if self.provider: string += f" | {self.provider}" if self.notes: @@ -259,36 +254,38 @@ class Order(TableDeclarativeBase): def __repr__(self): return f"" - def get_text(self, session, user=False): + def text(self, *, loc: localization.Localization, session, user=False): joined_self = session.query(Order).filter_by(order_id=self.order_id).join(Transaction).one() items = "" for item in self.items: items += str(item) + "\n" if self.delivery_date is not None: - status_emoji = strings.emoji_completed - status_text = strings.text_completed + status_emoji = loc.get("emoji_completed") + status_text = loc.get("text_completed") elif self.refund_date is not None: - status_emoji = strings.emoji_refunded - status_text = strings.text_refunded + status_emoji = loc.get("emoji_refunded") + status_text = loc.get("text_refunded") else: - status_emoji = strings.emoji_not_processed - status_text = strings.text_not_processed + status_emoji = loc.get("emoji_not_processed") + status_text = loc.get("text_not_processed") if user and configloader.config["Appearance"]["full_order_info"] == "no": - return strings.user_order_format_string.format(status_emoji=status_emoji, - status_text=status_text, - items=items, - notes=self.notes, - value=str(utils.Price(-joined_self.transaction.value))) + \ - (strings.refund_reason.format(reason=self.refund_reason) if self.refund_date is not None else "") + return loc.get("user_order_format_string", + status_emoji=status_emoji, + status_text=status_text, + items=items, + notes=self.notes, + value=str(utils.Price(-joined_self.transaction.value, loc))) + \ + (loc.get("refund_reason", reason=self.refund_reason) if self.refund_date is not None else "") else: return status_emoji + " " + \ - strings.order_number.format(id=self.order_id) + "\n" + \ - strings.order_format_string.format(user=self.user.mention(), - date=self.creation_date.isoformat(), - items=items, - notes=self.notes if self.notes is not None else "", - value=str(utils.Price(-joined_self.transaction.value))) + \ - (strings.refund_reason.format(reason=self.refund_reason) if self.refund_date is not None else "") + loc.get("order_number", id=self.order_id) + "\n" + \ + loc.get("order_format_string", + user=self.user.mention(), + date=self.creation_date.isoformat(), + items=items, + notes=self.notes if self.notes is not None else "", + value=str(utils.Price(-joined_self.transaction.value, loc))) + \ + (loc.get("refund_reason", reason=self.refund_reason) if self.refund_date is not None else "") class OrderItem(TableDeclarativeBase): @@ -305,8 +302,8 @@ class OrderItem(TableDeclarativeBase): # Extra table parameters __tablename__ = "orderitems" - def __str__(self): - return f"{self.product.name} - {str(utils.Price(self.product.price))}" + def text(self, *, loc: localization.Localization): + return f"{self.product.name} - {str(utils.Price(self.product.price, loc))}" def __repr__(self): return f"" diff --git a/localization.py b/localization.py index ee02f87..e6e6249 100644 --- a/localization.py +++ b/localization.py @@ -42,3 +42,6 @@ def get(self, key: str, **kwargs) -> str: assert isinstance(string, str) formatter = IgnoreDict(**self.replacements, **kwargs) return string.format_map(formatter) + + def boolmoji(self, boolean: bool) -> str: + return self.get("emoji_yes") if boolean else self.get("emoji_no") diff --git a/strings/en.py b/strings/en.py index f153e77..55d1387 100644 --- a/strings/en.py +++ b/strings/en.py @@ -118,6 +118,9 @@ conversation_confirm_admin_promotion = "Are you sure you want to promote this user to 💼 Manager?\n" \ "It is an irreversible action!" +# Conversation: language select menu header +conversation_language_select = "Select a language:" + # Conversation: switching to user mode conversation_switch_to_user_mode = " You are switching to 👤 Customer mode.\n" \ "If you want to go back to the 💼 Manager menu, restart the conversation with /start." @@ -214,6 +217,9 @@ # Menu: edit admins list menu_edit_admins = "🏵 Edit Managers" +# Menu: language +menu_language = "🇬🇧 Language" + # Emoji: unprocessed order emoji_not_processed = "*️⃣" diff --git a/strings/it.py b/strings/it.py index 5499ede..0392576 100644 --- a/strings/it.py +++ b/strings/it.py @@ -118,6 +118,9 @@ conversation_confirm_admin_promotion = "Sei sicuro di voler promuovere questo utente a 💼 Gestore?\n" \ "E' un'azione irreversibile!" +# Conversation: language select menu header +conversation_language_select = "Scegli una lingua:" + # Conversation: switching to user mode conversation_switch_to_user_mode = "Stai passando alla modalità 👤 Cliente.\n" \ "Se vuoi riassumere il ruolo di 💼 Gestore, riavvia la conversazione con /start." @@ -214,6 +217,9 @@ # Menu: edit admins list menu_edit_admins = "🏵 Modifica gestori" +# Menu: language +menu_language = "🇮🇹 Lingua" + # Emoji: unprocessed order emoji_not_processed = "*️⃣" diff --git a/utils.py b/utils.py index d36bc6d..0b2e47d 100644 --- a/utils.py +++ b/utils.py @@ -8,18 +8,12 @@ import importlib import logging import traceback +import localization log = logging.getLogger(__name__) -language = config["Config"]["language"] -try: - strings = importlib.import_module("strings." + language) -except ModuleNotFoundError: - print("The strings file you specified in the config file does not exist.") - sys.exit(1) - if config["Error Reporting"]["sentry_token"] != \ "https://00000000000000000000000000000000:00000000000000000000000000000000@sentry.io/0000000": import raven @@ -39,7 +33,9 @@ class Price: """The base class for the prices in greed. Its int value is in minimum units, while its float and str values are in decimal format.int(""" - def __init__(self, value: typing.Union[int, float, str, "Price"] = 0): + def __init__(self, value: typing.Union[int, float, str, "Price"], loc: localization.Localization): + # Keep a reference to the localization file + self.loc = loc if isinstance(value, int): # Keep the value as it is self.value = int(value) @@ -57,9 +53,9 @@ def __repr__(self): return f"" def __str__(self): - return strings.currency_format_string.format(symbol=(config["Payments"]["currency_symbol"] or strings.currency_symbol), - value="{0:.2f}".format( - self.value / (10 ** int(config["Payments"]["currency_exp"])))) + return self.loc.get("currency_format_string", + symbol=config["Payments"]["currency_symbol"], + value="{0:.2f}".format(self.value / (10 ** int(config["Payments"]["currency_exp"])))) def __int__(self): return self.value @@ -68,48 +64,48 @@ def __float__(self): return self.value / (10 ** int(config["Payments"]["currency_exp"])) def __ge__(self, other): - return self.value >= Price(other).value + return self.value >= Price(other, self.loc).value def __le__(self, other): - return self.value <= Price(other).value + return self.value <= Price(other, self.loc).value def __eq__(self, other): - return self.value == Price(other).value + return self.value == Price(other, self.loc).value def __gt__(self, other): - return self.value > Price(other).value + return self.value > Price(other, self.loc).value def __lt__(self, other): - return self.value < Price(other).value + return self.value < Price(other, self.loc).value def __add__(self, other): - return Price(self.value + Price(other).value) + return Price(self.value + Price(other, self.loc).value, self.loc) def __sub__(self, other): - return Price(self.value - Price(other).value) + return Price(self.value - Price(other, self.loc).value, self.loc) def __mul__(self, other): - return Price(int(self.value * other)) + return Price(int(self.value * other), self.loc) def __floordiv__(self, other): - return Price(int(self.value // other)) + return Price(int(self.value // other), self.loc) def __radd__(self, other): return self.__add__(other) def __rsub__(self, other): - return Price(Price(other).value - self.value) + return Price(Price(other, self.loc).value - self.value, self.loc) def __rmul__(self, other): return self.__mul__(other) def __iadd__(self, other): - self.value += Price(other).value + self.value += Price(other, self.loc).value return self def __isub__(self, other): - self.value -= Price(other).value + self.value -= Price(other, self.loc).value return self def __imul__(self, other): @@ -235,7 +231,3 @@ def send_document(self, *args, **kwargs): return self.bot.send_document(*args, **kwargs) # More methods can be added here - - -def boolmoji(boolean: bool): - return strings.emoji_yes if boolean else strings.emoji_no diff --git a/worker.py b/worker.py index cc44590..da9de39 100644 --- a/worker.py +++ b/worker.py @@ -100,23 +100,8 @@ def run(self): 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}") - # Check if the user's language is enabled; if it isn't, change it to the default - if self.user.language not in configloader.config["Language"]["enabled_languages"]: - log.debug(f"User's language '{self.user.language}' is not enabled, changing it to the default") - self.user.language = configloader.config["Language"]["default_language"] - self.session.commit() - # Create a Localization object - self.loc = localization.Localization( - language=self.user.language, - fallback=configloader.config["Language"]["fallback_language"], - replacements={ - "user_string": str(self.user), - "user_mention": self.user.mention(), - "user_full_name": self.user.full_name, - "user_first_name": self.user.first_name, - "today": datetime.datetime.now().strftime("%a %d %b %Y"), - } - ) + # Create the localization object + self.__create_localization() # Capture exceptions that occour during the conversation # noinspection PyBroadException try: @@ -359,16 +344,23 @@ def __user_menu(self): keyboard = [[telegram.KeyboardButton(self.loc.get("menu_order"))], [telegram.KeyboardButton(self.loc.get("menu_order_status"))], [telegram.KeyboardButton(self.loc.get("menu_add_credit"))], + [telegram.KeyboardButton(self.loc.get("menu_language"))], [telegram.KeyboardButton(self.loc.get("menu_help")), telegram.KeyboardButton(self.loc.get("menu_bot_info"))]] # Send the previously created keyboard to the user (ensuring it can be clicked only 1 time) self.bot.send_message(self.chat.id, - self.loc.get("conversation_open_user_menu", credit=utils.Price(self.user.credit)), + self.loc.get("conversation_open_user_menu", + credit=utils.Price(self.user.credit, self.loc)), reply_markup=telegram.ReplyKeyboardMarkup(keyboard, one_time_keyboard=True)) # Wait for a reply from the user - selection = self.__wait_for_specific_message([self.loc.get("menu_order"), self.loc.get("menu_order_status"), - self.loc.get("menu_add_credit"), self.loc.get("menu_bot_info"), - self.loc.get("menu_help")]) + selection = self.__wait_for_specific_message([ + self.loc.get("menu_order"), + self.loc.get("menu_order_status"), + self.loc.get("menu_add_credit"), + self.loc.get("menu_language"), + self.loc.get("menu_help"), + self.loc.get("menu_bot_info"), + ]) # After the user reply, update the user data self.update_user() # If the user has selected the Order option... @@ -383,6 +375,10 @@ def __user_menu(self): elif selection == self.loc.get("menu_add_credit"): # Display the add credit menu self.__add_credit_menu() + # If the user has selected the Language option... + elif selection == self.loc.get("menu_language"): + # Display the language menu + self.__language_menu() # If the user has selected the Bot Info option... elif selection == self.loc.get("menu_bot_info"): # Display information about the bot @@ -410,24 +406,27 @@ def __order_menu(self): # Add the product to the cart cart[message['result']['message_id']] = [product, 0] # Create the inline keyboard to add the product to the cart - inline_keyboard = telegram.InlineKeyboardMarkup([[telegram.InlineKeyboardButton(self.loc.get("menu_add_to_cart"), - callback_data="cart_add")]]) + inline_keyboard = telegram.InlineKeyboardMarkup( + [[telegram.InlineKeyboardButton(self.loc.get("menu_add_to_cart"), callback_data="cart_add")]] + ) # Edit the sent message and add the inline keyboard if product.image is None: self.bot.edit_message_text(chat_id=self.chat.id, message_id=message['result']['message_id'], - text=product.text(), + text=product.text(loc=self.loc), reply_markup=inline_keyboard) else: self.bot.edit_message_caption(chat_id=self.chat.id, message_id=message['result']['message_id'], - caption=product.text(), + caption=product.text(loc=self.loc), reply_markup=inline_keyboard) # Create the keyboard with the cancel button inline_keyboard = telegram.InlineKeyboardMarkup([[telegram.InlineKeyboardButton(self.loc.get("menu_cancel"), callback_data="cart_cancel")]]) # Send a message containing the button to cancel or pay - final_msg = self.bot.send_message(self.chat.id, self.loc.get("conversation_cart_actions"), reply_markup=inline_keyboard) + final_msg = self.bot.send_message(self.chat.id, + self.loc.get("conversation_cart_actions"), + reply_markup=inline_keyboard) # Wait for user input while True: callback = self.__wait_for_inlinekeyboard_callback() @@ -448,8 +447,10 @@ def __order_menu(self): # Create the product inline keyboard product_inline_keyboard = telegram.InlineKeyboardMarkup( [ - [telegram.InlineKeyboardButton(self.loc.get("menu_add_to_cart"), callback_data="cart_add"), - telegram.InlineKeyboardButton(self.loc.get("menu_remove_from_cart"), callback_data="cart_remove")] + [telegram.InlineKeyboardButton(self.loc.get("menu_add_to_cart"), + callback_data="cart_add"), + telegram.InlineKeyboardButton(self.loc.get("menu_remove_from_cart"), + callback_data="cart_remove")] ]) # Create the final inline keyboard final_inline_keyboard = telegram.InlineKeyboardMarkup( @@ -461,19 +462,22 @@ def __order_menu(self): if product.image is None: self.bot.edit_message_text(chat_id=self.chat.id, message_id=callback.message.message_id, - text=product.text(cart_qty=cart[callback.message.message_id][1]), + text=product.text(loc=self.loc, + cart_qty=cart[callback.message.message_id][1]), reply_markup=product_inline_keyboard) else: self.bot.edit_message_caption(chat_id=self.chat.id, message_id=callback.message.message_id, - caption=product.text(cart_qty=cart[callback.message.message_id][1]), + caption=product.text(loc=self.loc, + cart_qty=cart[callback.message.message_id][1]), reply_markup=product_inline_keyboard) self.bot.edit_message_text( chat_id=self.chat.id, message_id=final_msg.message_id, - text=self.loc.get("conversation_confirm_cart", product_list=self.__get_cart_summary(cart), - total_cost=str(self.__get_cart_value(cart))), + text=self.loc.get("conversation_confirm_cart", + product_list=self.__get_cart_summary(cart), + total_cost=str(self.__get_cart_value(cart))), reply_markup=final_inline_keyboard) # If the Remove from cart button has been pressed... elif callback.data == "cart_remove": @@ -495,7 +499,8 @@ def __order_menu(self): callback_data="cart_remove")) product_inline_keyboard = telegram.InlineKeyboardMarkup(product_inline_list) # Create the final inline keyboard - final_inline_list = [[telegram.InlineKeyboardButton(self.loc.get("menu_cancel"), callback_data="cart_cancel")]] + final_inline_list = [[telegram.InlineKeyboardButton(self.loc.get("menu_cancel"), + callback_data="cart_cancel")]] for product_id in cart: if cart[product_id][1] > 0: final_inline_list.append([telegram.InlineKeyboardButton(self.loc.get("menu_done"), @@ -505,18 +510,22 @@ def __order_menu(self): # Edit the product message if product.image is None: self.bot.edit_message_text(chat_id=self.chat.id, message_id=callback.message.message_id, - text=product.text(cart_qty=cart[callback.message.message_id][1]), + text=product.text(loc=self.loc, + cart_qty=cart[callback.message.message_id][1]), reply_markup=product_inline_keyboard) else: self.bot.edit_message_caption(chat_id=self.chat.id, message_id=callback.message.message_id, - caption=product.text(cart_qty=cart[callback.message.message_id][1]), + caption=product.text(loc=self.loc, + cart_qty=cart[callback.message.message_id][1]), reply_markup=product_inline_keyboard) self.bot.edit_message_text( chat_id=self.chat.id, message_id=final_msg.message_id, - text=self.loc.get("conversation_confirm_cart", product_list=self.__get_cart_summary(cart), total_cost=str(self.__get_cart_value(cart))), + text=self.loc.get("conversation_confirm_cart", + product_list=self.__get_cart_summary(cart), + total_cost=str(self.__get_cart_value(cart))), reply_markup=final_inline_keyboard) # If the done button has been pressed... elif callback.data == "cart_done": @@ -551,10 +560,10 @@ def __order_menu(self): # Suggest payment for missing credit value if configuration allows refill if configloader.config["Credit Card"]["credit_card_token"] != "" \ and configloader.config["Appearance"]["refill_on_checkout"] == 'yes' \ - and utils.Price(int(configloader.config["Credit Card"]["min_amount"])) <= \ + and utils.Price(int(configloader.config["Credit Card"]["min_amount"]), self.loc) <= \ credit_required <= \ - utils.Price(int(configloader.config["Credit Card"]["max_amount"])): - self.__make_payment(utils.Price(credit_required)) + utils.Price(int(configloader.config["Credit Card"]["max_amount"]), self.loc): + self.__make_payment(utils.Price(credit_required, self.loc)) # If afer requested payment credit is still insufficient (either payment failure or cancel) if self.user.credit < self.__get_cart_value(cart): # Rollback all the changes @@ -563,21 +572,21 @@ def __order_menu(self): # User has credit and valid order, perform transaction now self.__order_transaction(order=order, value=-int(self.__get_cart_value(cart))) - @staticmethod - def __get_cart_value(cart): + def __get_cart_value(self, cart): # Calculate total items value in cart - value = utils.Price(0) + value = utils.Price(0, self.loc) for product in cart: value += cart[product][0].price * cart[product][1] return value - @staticmethod - def __get_cart_summary(cart): + def __get_cart_summary(self, cart): # Create the cart summary product_list = "" for product_id in cart: if cart[product_id][1] > 0: - product_list += cart[product_id][0].text(style="short", cart_qty=cart[product_id][1]) + "\n" + product_list += cart[product_id][0].text(loc=self.loc, + style="short", + cart_qty=cart[product_id][1]) + "\n" return product_list def __order_transaction(self, order, value): @@ -597,7 +606,9 @@ def __order_transaction(self, order, value): def __order_notify_admins(self, order): # Notify the user of the order result - self.bot.send_message(self.chat.id, self.loc.get("success_order_created", order=order.get_text(self.session, user=True))) + self.bot.send_message(self.chat.id, self.loc.get("success_order_created", order=order.text(loc=self.loc, + session=self.session, + user=True))) # Notify the admins (in Live Orders mode) of the new order admins = self.session.query(db.Admin).filter_by(live_mode=True).all() # Create the order keyboard @@ -609,7 +620,8 @@ def __order_notify_admins(self, order): # Notify them of the new placed order for admin in admins: self.bot.send_message(admin.user_id, - f"{self.loc.get('notification_order_placed', order=order.get_text(self.session))}", + self.loc.get('notification_order_placed', + order=order.text(loc=self.loc, session=self.session)), reply_markup=order_keyboard) def __order_status(self): @@ -626,7 +638,7 @@ def __order_status(self): self.bot.send_message(self.chat.id, self.loc.get("error_no_orders")) # Display the order status to the user for order in orders: - self.bot.send_message(self.chat.id, order.get_text(self.session, user=True)) + self.bot.send_message(self.chat.id, order.text(loc=self.loc, session=self.session, user=True)) # TODO: maybe add a page displayer instead of showing the latest 5 orders def __add_credit_menu(self): @@ -667,7 +679,7 @@ def __add_credit_cc(self): log.debug("Displaying __add_credit_cc") # Create a keyboard to be sent later 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, self.loc)))] for preset in presets] keyboard.append([telegram.KeyboardButton(self.loc.get("menu_cancel"))]) # Boolean variable to check if the user has cancelled the action cancelled = False @@ -684,15 +696,15 @@ def __add_credit_cc(self): cancelled = True continue # Convert the amount to an integer - value = utils.Price(selection) + value = utils.Price(selection, self.loc) # Ensure the amount is within the range - if value > utils.Price(int(configloader.config["Credit Card"]["max_amount"])): + if value > utils.Price(int(configloader.config["Credit Card"]["max_amount"]), self.loc): self.bot.send_message(self.chat.id, - self.loc.get("error_payment_amount_over_max", max_amount=utils.Price(configloader.config["Credit Card"]["max_amount"]))) + self.loc.get("error_payment_amount_over_max", max_amount=utils.Price(configloader.config["Credit Card"]["max_amount"], self.loc))) continue - elif value < utils.Price(int(configloader.config["Credit Card"]["min_amount"])): + elif value < utils.Price(int(configloader.config["Credit Card"]["min_amount"]), self.loc): self.bot.send_message(self.chat.id, - self.loc.get("error_payment_amount_under_min", min_amount=utils.Price(configloader.config["Credit Card"]["min_amount"]))) + self.loc.get("error_payment_amount_under_min", min_amount=utils.Price(configloader.config["Credit Card"]["min_amount"], self.loc))) continue break # If the user cancelled the action... @@ -904,7 +916,7 @@ def __edit_product_menu(self, product: Optional[db.Product] = None): if product: self.bot.send_message(self.chat.id, self.loc.get("edit_current_value", - value=(str(utils.Price(product.price)) + value=(str(utils.Price(product.price, self.loc)) if product.price is not None else 'Non in vendita')), reply_markup=cancel) # Wait for an answer @@ -916,7 +928,7 @@ def __edit_product_menu(self, product: Optional[db.Product] = None): elif price.lower() == "x": price = None else: - price = utils.Price(price) + price = utils.Price(price, self.loc) # Ask for the product image self.bot.send_message(self.chat.id, self.loc.get("ask_product_image"), reply_markup=cancel) # Wait for an answer @@ -1007,7 +1019,7 @@ def __orders_menu(self): # Create a message for every one of them for order in orders: # Send the created message - self.bot.send_message(self.chat.id, order.get_text(session=self.session), + self.bot.send_message(self.chat.id, order.text(loc=self.loc, session=self.session), reply_markup=order_keyboard) # Set the Live mode flag to True self.admin.live_mode = True @@ -1036,11 +1048,11 @@ def __orders_menu(self): # Commit the transaction self.session.commit() # Update order message - self.bot.edit_message_text(order.get_text(session=self.session), chat_id=self.chat.id, + self.bot.edit_message_text(order.text(loc=self.loc, session=self.session), chat_id=self.chat.id, message_id=update.message.message_id) # Notify the user of the completition self.bot.send_message(order.user_id, - self.loc.get("notification_order_completed", order=order.get_text(self.session, user=True))) + self.loc.get("notification_order_completed", order=order.text(loc=self.loc, session=self.session, user=True))) # If the user pressed the refund order button, refund the order... elif update.data == "order_refund": # Ask for a refund reason @@ -1064,13 +1076,14 @@ def __orders_menu(self): # Commit the changes self.session.commit() # Update the order message - self.bot.edit_message_text(order.get_text(session=self.session), + self.bot.edit_message_text(order.text(loc=self.loc, session=self.session), chat_id=self.chat.id, message_id=update.message.message_id) # Notify the user of the refund self.bot.send_message(order.user_id, - self.loc.get("notification_order_refunded", order=order.get_text(self.session, - user=True))) + self.loc.get("notification_order_refunded", order=order.text(loc=self.loc, + session=self.session, + user=True))) # Notify the admin of the refund self.bot.send_message(self.chat.id, self.loc.get("success_order_refunded", order_id=order.order_id)) @@ -1093,7 +1106,7 @@ def __create_transaction(self): if isinstance(reply, CancelSignal): return # Convert the reply to a price object - price = utils.Price(reply) + price = utils.Price(reply, self.loc) # Ask the user for notes self.bot.send_message(self.chat.id, self.loc.get("ask_transaction_notes"), reply_markup=cancel) # Wait for an answer @@ -1279,15 +1292,15 @@ def __add_admin(self): while True: # Create the inline keyboard with the admin status inline_keyboard = telegram.InlineKeyboardMarkup([ - [telegram.InlineKeyboardButton(f"{utils.boolmoji(admin.edit_products)} {self.loc.get('prop_edit_products')}", + [telegram.InlineKeyboardButton(f"{self.loc.boolmoji(admin.edit_products)} {self.loc.get('prop_edit_products')}", callback_data="toggle_edit_products")], - [telegram.InlineKeyboardButton(f"{utils.boolmoji(admin.receive_orders)} {self.loc.get('prop_receive_orders')}", + [telegram.InlineKeyboardButton(f"{self.loc.boolmoji(admin.receive_orders)} {self.loc.get('prop_receive_orders')}", callback_data="toggle_receive_orders")], [telegram.InlineKeyboardButton( - f"{utils.boolmoji(admin.create_transactions)} {self.loc.get('prop_create_transactions')}", + f"{self.loc.boolmoji(admin.create_transactions)} {self.loc.get('prop_create_transactions')}", callback_data="toggle_create_transactions")], [telegram.InlineKeyboardButton( - f"{utils.boolmoji(admin.display_on_help)} {self.loc.get('prop_display_on_help')}", + f"{self.loc.boolmoji(admin.display_on_help)} {self.loc.get('prop_display_on_help')}", callback_data="toggle_display_on_help")], [telegram.InlineKeyboardButton(self.loc.get('menu_done'), callback_data="cmd_done")] ]) @@ -1310,6 +1323,60 @@ def __add_admin(self): break self.session.commit() + def __language_menu(self): + """Select a language.""" + log.debug("Displaying __language_menu") + keyboard = [] + options: Dict[str, str] = {} + # https://en.wikipedia.org/wiki/List_of_language_names + if "it" in configloader.config["Language"]["enabled_languages"]: + lang = "🇮🇹 Italiano" + keyboard.append([telegram.KeyboardButton(lang)]) + options[lang] = "it" + if "en" in configloader.config["Language"]["enabled_languages"]: + lang = "🇬🇧 English" + keyboard.append([telegram.KeyboardButton(lang)]) + options[lang] = "en" + if "ru" in configloader.config["Language"]["enabled_languages"]: + lang = "🇷🇺 Русский" + keyboard.append([telegram.KeyboardButton(lang)]) + options[lang] = "ru" + if "uk" in configloader.config["Language"]["enabled_languages"]: + lang = "🇺🇦 Українська" + keyboard.append([telegram.KeyboardButton(lang)]) + options[lang] = "uk" + # Send the previously created keyboard to the user (ensuring it can be clicked only 1 time) + self.bot.send_message(self.chat.id, + self.loc.get("conversation_language_select"), + reply_markup=telegram.ReplyKeyboardMarkup(keyboard, one_time_keyboard=True)) + # Wait for an answer + response = self.__wait_for_specific_message(list(options.keys())) + # Set the language to the corresponding value + self.user.language = options[response] + # Commit the edit to the database + self.session.commit() + # Recreate the localization object + self.__create_localization() + + def __create_localization(self): + # Check if the user's language is enabled; if it isn't, change it to the default + if self.user.language not in configloader.config["Language"]["enabled_languages"]: + log.debug(f"User's language '{self.user.language}' is not enabled, changing it to the default") + self.user.language = configloader.config["Language"]["default_language"] + self.session.commit() + # Create a new Localization object + self.loc = localization.Localization( + language=self.user.language, + fallback=configloader.config["Language"]["fallback_language"], + replacements={ + "user_string": str(self.user), + "user_mention": self.user.mention(), + "user_full_name": self.user.full_name, + "user_first_name": self.user.first_name, + "today": datetime.datetime.now().strftime("%a %d %b %Y"), + } + ) + def __graceful_stop(self, stop_trigger: StopSignal): """Handle the graceful stop of the thread.""" log.debug("Gracefully stopping the conversation") From 12c70ac38890d2bd3aad31758911a201dd767f64 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Sun, 3 May 2020 13:36:01 +0200 Subject: [PATCH 5/5] Add a json conversion --- localization.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/localization.py b/localization.py index e6e6249..329aea1 100644 --- a/localization.py +++ b/localization.py @@ -2,6 +2,7 @@ import importlib import types import logging +import json log = logging.getLogger(__name__) @@ -45,3 +46,14 @@ def get(self, key: str, **kwargs) -> str: def boolmoji(self, boolean: bool) -> str: return self.get("emoji_yes") if boolean else self.get("emoji_no") + + +def create_json_localization_file_from_strings(language: str): + module: types.ModuleType = importlib.import_module(f"strings.{language}") + raw = module.__dict__ + clean = {} + for key in raw: + if not (key.startswith("__") and key.endswith("__")): + clean[key] = raw[key] + with open(f"locale/{language}.json", "w") as file: + json.dump(clean, file)