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

Merge pull request #54 from Steffo99/i18n

Closes #51: Better strings and localization support
This commit is contained in:
Steffo 2020-06-09 02:40:28 +02:00 committed by GitHub
commit 038b0e574d
Signed by: github
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 435 additions and 275 deletions

View file

@ -5,16 +5,23 @@
# Config file parameters # Config file parameters
[Config] [Config]
; Config file version. DO NOT EDIT THIS! ; Config file version. DO NOT EDIT THIS!
version = 17 version = 18
; Set this to no when you are done editing the file ; Set this to no when you are done editing the file
is_template = yes is_template = yes
; Language code for string file
# Language parameters
[Language]
; Available languages: ; Available languages:
; it_IT - Italian, by Steffo ; it - Italian, by https://github.com/Steffo99
; en_US - English, by https://github.com/DarrenWestwood (incomplete, please improve it!) ; en - English, by https://github.com/DarrenWestwood
; ua_UK - Ukrainian, by https://github.com/pzhuk ; uk - Ukrainian, by https://github.com/pzhuk
; ru_RU - Russian, by https://github.com/pzhuk ; ru - Russian, by https://github.com/pzhuk
language = it_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 bot parameters
[Telegram] [Telegram]

24
core.py
View file

@ -4,7 +4,7 @@ import worker
import configloader import configloader
import utils import utils
import threading import threading
import importlib import localization
import logging import logging
try: try:
@ -12,9 +12,6 @@ try:
except ImportError: except ImportError:
coloredlogs = None coloredlogs = None
language = configloader.config["Config"]["language"]
strings = importlib.import_module("strings." + language)
def main(): def main():
"""The core code of the program. Should be run only in the main process!""" """The core code of the program. Should be run only in the main process!"""
@ -48,6 +45,11 @@ def main():
sys.exit(1) sys.exit(1)
log.debug("Bot token is valid!") log.debug("Bot token is valid!")
# Finding default language
default_language = configloader.config["Language"]["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 # Create a dictionary linking the chat ids to the Worker objects
# {"1234": <Worker>} # {"1234": <Worker>}
chat_workers = {} chat_workers = {}
@ -72,7 +74,7 @@ def main():
if update.message.chat.type != "private": if update.message.chat.type != "private":
log.debug(f"Received a message from a non-private chat: {update.message.chat.id}") log.debug(f"Received a message from a non-private chat: {update.message.chat.id}")
# Notify the chat # Notify the chat
bot.send_message(update.message.chat.id, strings.error_nonprivate_chat) bot.send_message(update.message.chat.id, default_loc.get("error_nonprivate_chat"))
# Skip the update # Skip the update
continue continue
# If the message is a start command... # If the message is a start command...
@ -85,7 +87,9 @@ def main():
log.debug(f"Received request to stop {old_worker.name}") log.debug(f"Received request to stop {old_worker.name}")
old_worker.stop("request") old_worker.stop("request")
# Initialize a new worker for the chat # Initialize a new worker for the chat
new_worker = worker.Worker(bot, update.message.chat) new_worker = worker.Worker(bot=bot,
chat=update.message.chat,
telegram_user=update.message.from_user)
# Start the worker # Start the worker
log.debug(f"Starting {new_worker.name}") log.debug(f"Starting {new_worker.name}")
new_worker.start() new_worker.start()
@ -99,12 +103,12 @@ def main():
if receiving_worker is None or not receiving_worker.is_alive(): if receiving_worker is None or not receiving_worker.is_alive():
log.debug(f"Received a message in a chat without worker: {update.message.chat.id}") log.debug(f"Received a message in a chat without worker: {update.message.chat.id}")
# Suggest that the user restarts the chat with /start # Suggest that the user restarts the chat with /start
bot.send_message(update.message.chat.id, strings.error_no_worker_for_chat, bot.send_message(update.message.chat.id, default_loc.get("error_no_worker_for_chat"),
reply_markup=telegram.ReplyKeyboardRemove()) reply_markup=telegram.ReplyKeyboardRemove())
# Skip the update # Skip the update
continue continue
# If the message contains the "Cancel" string defined in the strings file... # If the message contains the "Cancel" string defined in the strings file...
if update.message.text == strings.menu_cancel: if update.message.text == receiving_worker.loc.get("menu_cancel"):
log.debug(f"Forwarding CancelSignal to {receiving_worker}") log.debug(f"Forwarding CancelSignal to {receiving_worker}")
# Send a CancelSignal to the worker instead of the update # Send a CancelSignal to the worker instead of the update
receiving_worker.queue.put(worker.CancelSignal()) receiving_worker.queue.put(worker.CancelSignal())
@ -120,7 +124,7 @@ def main():
if receiving_worker is None: if receiving_worker is None:
log.debug(f"Received a callback query in a chat without worker: {update.callback_query.from_user.id}") log.debug(f"Received a callback query in a chat without worker: {update.callback_query.from_user.id}")
# Suggest that the user restarts the chat with /start # Suggest that the user restarts the chat with /start
bot.send_message(update.callback_query.from_user.id, strings.error_no_worker_for_chat) bot.send_message(update.callback_query.from_user.id, default_loc.get("error_no_worker_for_chat"))
# Skip the update # Skip the update
continue continue
# Check if the pressed inline key is a cancel button # Check if the pressed inline key is a cancel button
@ -146,7 +150,7 @@ def main():
try: try:
bot.answer_pre_checkout_query(update.pre_checkout_query.id, bot.answer_pre_checkout_query(update.pre_checkout_query.id,
ok=False, ok=False,
error_message=strings.error_invoice_expired) error_message=default_loc.get("error_invoice_expired"))
except telegram.error.BadRequest: except telegram.error.BadRequest:
log.error("pre-checkout query expired before an answer could be sent!") log.error("pre-checkout query expired before an answer could be sent!")
# Go to the next update # Go to the next update

View file

@ -7,10 +7,10 @@ import configloader
import telegram import telegram
import requests import requests
import utils import utils
import importlib import localization
import logging
language = configloader.config["Config"]["language"] log = logging.getLogger(__name__)
strings = importlib.import_module("strings." + language)
# Create a (lazy) database engine # Create a (lazy) database engine
engine = create_engine(configloader.config["Database"]["engine"]) engine = create_engine(configloader.config["Database"]["engine"])
@ -31,6 +31,7 @@ class User(TableDeclarativeBase):
first_name = Column(String, nullable=False) first_name = Column(String, nullable=False)
last_name = Column(String) last_name = Column(String)
username = Column(String) username = Column(String)
language = Column(String, nullable=False)
# Current wallet credit # Current wallet credit
credit = Column(Integer, nullable=False) credit = Column(Integer, nullable=False)
@ -38,14 +39,16 @@ class User(TableDeclarativeBase):
# Extra table parameters # Extra table parameters
__tablename__ = "users" __tablename__ = "users"
def __init__(self, telegram_chat: telegram.Chat, **kwargs): def __init__(self, telegram_user: telegram.User, **kwargs):
# Initialize the super # Initialize the super
super().__init__(**kwargs) super().__init__(**kwargs)
# Get the data from telegram # Get the data from telegram
self.user_id = telegram_chat.id self.user_id = telegram_user.id
self.first_name = telegram_chat.first_name self.first_name = telegram_user.first_name
self.last_name = telegram_chat.last_name self.last_name = telegram_user.last_name
self.username = telegram_chat.username 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 # The starting wallet value is 0
self.credit = 0 self.credit = 0
@ -74,6 +77,13 @@ class User(TableDeclarativeBase):
valid_transactions: typing.List[Transaction] = [t for t in self.transactions if not t.refunded] 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)) 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): def __repr__(self):
return f"<User {self} having {self.credit} credit>" return f"<User {self} having {self.credit} credit>"
@ -99,21 +109,18 @@ class Product(TableDeclarativeBase):
# No __init__ is needed, the default one is sufficient # No __init__ is needed, the default one is sufficient
def __str__(self): def text(self, *, loc: localization.Localization, style: str = "full", cart_qty: int = None):
return self.text()
def text(self, style: str="full", cart_qty: int=None):
"""Return the product details formatted with Telegram HTML. The image is omitted.""" """Return the product details formatted with Telegram HTML. The image is omitted."""
if style == "short": 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": elif style == "full":
if cart_qty is not None: 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: else:
cart = '' cart = ''
return strings.product_format_string.format(name=utils.telegram_html_escape(self.name), return loc.get("product_format_string", name=utils.telegram_html_escape(self.name),
description=utils.telegram_html_escape(self.description), description=utils.telegram_html_escape(self.description),
price=str(utils.Price(self.price)), price=str(utils.Price(self.price, loc)),
cart=cart) cart=cart)
else: else:
raise ValueError("style is not an accepted value") raise ValueError("style is not an accepted value")
@ -121,18 +128,18 @@ class Product(TableDeclarativeBase):
def __repr__(self): def __repr__(self):
return f"<Product {self.name}>" return f"<Product {self.name}>"
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.""" """Send a message containing the product data."""
if self.image is None: if self.image is None:
r = requests.get(f"https://api.telegram.org/bot{configloader.config['Telegram']['token']}/sendMessage", r = requests.get(f"https://api.telegram.org/bot{configloader.config['Telegram']['token']}/sendMessage",
params={"chat_id": chat_id, params={"chat_id": chat_id,
"text": self.text(), "text": self.text(loc=loc),
"parse_mode": "HTML"}) "parse_mode": "HTML"})
else: else:
r = requests.post(f"https://api.telegram.org/bot{configloader.config['Telegram']['token']}/sendPhoto", r = requests.post(f"https://api.telegram.org/bot{configloader.config['Telegram']['token']}/sendPhoto",
files={"photo": self.image}, files={"photo": self.image},
params={"chat_id": chat_id, params={"chat_id": chat_id,
"caption": self.text(), "caption": self.text(loc=loc),
"parse_mode": "HTML"}) "parse_mode": "HTML"})
return r.json() return r.json()
@ -181,10 +188,10 @@ class Transaction(TableDeclarativeBase):
__tablename__ = "transactions" __tablename__ = "transactions"
__table_args__ = (UniqueConstraint("provider", "provider_charge_id"),) __table_args__ = (UniqueConstraint("provider", "provider_charge_id"),)
def __str__(self): def text(self, *, loc: localization.Localization):
string = f"<b>T{self.transaction_id}</b> | {str(self.user)} | {utils.Price(self.value)}" string = f"<b>T{self.transaction_id}</b> | {str(self.user)} | {utils.Price(self.value, loc)}"
if self.refunded: if self.refunded:
string += f" | {strings.emoji_refunded}" string += f" | {loc.get('emoji_refunded')}"
if self.provider: if self.provider:
string += f" | {self.provider}" string += f" | {self.provider}"
if self.notes: if self.notes:
@ -247,36 +254,38 @@ class Order(TableDeclarativeBase):
def __repr__(self): def __repr__(self):
return f"<Order {self.order_id} placed by User {self.user_id}>" return f"<Order {self.order_id} placed by User {self.user_id}>"
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() joined_self = session.query(Order).filter_by(order_id=self.order_id).join(Transaction).one()
items = "" items = ""
for item in self.items: for item in self.items:
items += str(item) + "\n" items += str(item) + "\n"
if self.delivery_date is not None: if self.delivery_date is not None:
status_emoji = strings.emoji_completed status_emoji = loc.get("emoji_completed")
status_text = strings.text_completed status_text = loc.get("text_completed")
elif self.refund_date is not None: elif self.refund_date is not None:
status_emoji = strings.emoji_refunded status_emoji = loc.get("emoji_refunded")
status_text = strings.text_refunded status_text = loc.get("text_refunded")
else: else:
status_emoji = strings.emoji_not_processed status_emoji = loc.get("emoji_not_processed")
status_text = strings.text_not_processed status_text = loc.get("text_not_processed")
if user and configloader.config["Appearance"]["full_order_info"] == "no": if user and configloader.config["Appearance"]["full_order_info"] == "no":
return strings.user_order_format_string.format(status_emoji=status_emoji, return loc.get("user_order_format_string",
status_emoji=status_emoji,
status_text=status_text, status_text=status_text,
items=items, items=items,
notes=self.notes, notes=self.notes,
value=str(utils.Price(-joined_self.transaction.value))) + \ value=str(utils.Price(-joined_self.transaction.value, loc))) + \
(strings.refund_reason.format(reason=self.refund_reason) if self.refund_date is not None else "") (loc.get("refund_reason", reason=self.refund_reason) if self.refund_date is not None else "")
else: else:
return status_emoji + " " + \ return status_emoji + " " + \
strings.order_number.format(id=self.order_id) + "\n" + \ loc.get("order_number", id=self.order_id) + "\n" + \
strings.order_format_string.format(user=self.user.mention(), loc.get("order_format_string",
user=self.user.mention(),
date=self.creation_date.isoformat(), date=self.creation_date.isoformat(),
items=items, items=items,
notes=self.notes if self.notes is not None else "", notes=self.notes if self.notes is not None else "",
value=str(utils.Price(-joined_self.transaction.value))) + \ value=str(utils.Price(-joined_self.transaction.value, loc))) + \
(strings.refund_reason.format(reason=self.refund_reason) if self.refund_date is not None else "") (loc.get("refund_reason", reason=self.refund_reason) if self.refund_date is not None else "")
class OrderItem(TableDeclarativeBase): class OrderItem(TableDeclarativeBase):
@ -293,8 +302,8 @@ class OrderItem(TableDeclarativeBase):
# Extra table parameters # Extra table parameters
__tablename__ = "orderitems" __tablename__ = "orderitems"
def __str__(self): def text(self, *, loc: localization.Localization):
return f"{self.product.name} - {str(utils.Price(self.product.price))}" return f"{self.product.name} - {str(utils.Price(self.product.price, loc))}"
def __repr__(self): def __repr__(self):
return f"<OrderItem {self.item_id}>" return f"<OrderItem {self.item_id}>"

59
localization.py Normal file
View file

@ -0,0 +1,59 @@
from typing import *
import importlib
import types
import logging
import json
log = logging.getLogger(__name__)
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: 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:
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: 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)
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)

View file

@ -118,6 +118,9 @@ conversation_open_help_menu = "What kind of help do you need?"
conversation_confirm_admin_promotion = "Are you sure you want to promote this user to 💼 Manager?\n" \ conversation_confirm_admin_promotion = "Are you sure you want to promote this user to 💼 Manager?\n" \
"It is an irreversible action!" "It is an irreversible action!"
# Conversation: language select menu header
conversation_language_select = "Select a language:"
# Conversation: switching to user mode # Conversation: switching to user mode
conversation_switch_to_user_mode = " You are switching to 👤 Customer mode.\n" \ 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." "If you want to go back to the 💼 Manager menu, restart the conversation with /start."
@ -214,6 +217,9 @@ menu_csv = "📄 .csv"
# Menu: edit admins list # Menu: edit admins list
menu_edit_admins = "🏵 Edit Managers" menu_edit_admins = "🏵 Edit Managers"
# Menu: language
menu_language = "🇬🇧 Language"
# Emoji: unprocessed order # Emoji: unprocessed order
emoji_not_processed = "*️⃣" emoji_not_processed = "*️⃣"

View file

@ -118,6 +118,9 @@ conversation_open_help_menu = "Che tipo di assistenza desideri ricevere?"
conversation_confirm_admin_promotion = "Sei sicuro di voler promuovere questo utente a 💼 Gestore?\n" \ conversation_confirm_admin_promotion = "Sei sicuro di voler promuovere questo utente a 💼 Gestore?\n" \
"E' un'azione irreversibile!" "E' un'azione irreversibile!"
# Conversation: language select menu header
conversation_language_select = "Scegli una lingua:"
# Conversation: switching to user mode # Conversation: switching to user mode
conversation_switch_to_user_mode = "Stai passando alla modalità 👤 Cliente.\n" \ conversation_switch_to_user_mode = "Stai passando alla modalità 👤 Cliente.\n" \
"Se vuoi riassumere il ruolo di 💼 Gestore, riavvia la conversazione con /start." "Se vuoi riassumere il ruolo di 💼 Gestore, riavvia la conversazione con /start."
@ -214,6 +217,9 @@ menu_csv = "📄 .csv"
# Menu: edit admins list # Menu: edit admins list
menu_edit_admins = "🏵 Modifica gestori" menu_edit_admins = "🏵 Modifica gestori"
# Menu: language
menu_language = "🇮🇹 Lingua"
# Emoji: unprocessed order # Emoji: unprocessed order
emoji_not_processed = "*️⃣" emoji_not_processed = "*️⃣"

View file

@ -8,18 +8,12 @@ import sys
import importlib import importlib
import logging import logging
import traceback import traceback
import localization
log = logging.getLogger(__name__) 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"] != \ if config["Error Reporting"]["sentry_token"] != \
"https://00000000000000000000000000000000:00000000000000000000000000000000@sentry.io/0000000": "https://00000000000000000000000000000000:00000000000000000000000000000000@sentry.io/0000000":
import raven import raven
@ -39,7 +33,9 @@ class Price:
"""The base class for the prices in greed. """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(""" 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): if isinstance(value, int):
# Keep the value as it is # Keep the value as it is
self.value = int(value) self.value = int(value)
@ -57,9 +53,9 @@ class Price:
return f"<Price of value {self.value}>" return f"<Price of value {self.value}>"
def __str__(self): def __str__(self):
return strings.currency_format_string.format(symbol=(config["Payments"]["currency_symbol"] or strings.currency_symbol), return self.loc.get("currency_format_string",
value="{0:.2f}".format( symbol=config["Payments"]["currency_symbol"],
self.value / (10 ** int(config["Payments"]["currency_exp"])))) value="{0:.2f}".format(self.value / (10 ** int(config["Payments"]["currency_exp"]))))
def __int__(self): def __int__(self):
return self.value return self.value
@ -68,48 +64,48 @@ class Price:
return self.value / (10 ** int(config["Payments"]["currency_exp"])) return self.value / (10 ** int(config["Payments"]["currency_exp"]))
def __ge__(self, other): def __ge__(self, other):
return self.value >= Price(other).value return self.value >= Price(other, self.loc).value
def __le__(self, other): def __le__(self, other):
return self.value <= Price(other).value return self.value <= Price(other, self.loc).value
def __eq__(self, other): def __eq__(self, other):
return self.value == Price(other).value return self.value == Price(other, self.loc).value
def __gt__(self, other): def __gt__(self, other):
return self.value > Price(other).value return self.value > Price(other, self.loc).value
def __lt__(self, other): def __lt__(self, other):
return self.value < Price(other).value return self.value < Price(other, self.loc).value
def __add__(self, other): 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): 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): def __mul__(self, other):
return Price(int(self.value * other)) return Price(int(self.value * other), self.loc)
def __floordiv__(self, other): def __floordiv__(self, other):
return Price(int(self.value // other)) return Price(int(self.value // other), self.loc)
def __radd__(self, other): def __radd__(self, other):
return self.__add__(other) return self.__add__(other)
def __rsub__(self, 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): def __rmul__(self, other):
return self.__mul__(other) return self.__mul__(other)
def __iadd__(self, other): def __iadd__(self, other):
self.value += Price(other).value self.value += Price(other, self.loc).value
return self return self
def __isub__(self, other): def __isub__(self, other):
self.value -= Price(other).value self.value -= Price(other, self.loc).value
return self return self
def __imul__(self, other): def __imul__(self, other):
@ -235,7 +231,3 @@ class DuckBot:
return self.bot.send_document(*args, **kwargs) return self.bot.send_document(*args, **kwargs)
# More methods can be added here # More methods can be added here
def boolmoji(boolean: bool):
return strings.emoji_yes if boolean else strings.emoji_no

439
worker.py
View file

@ -13,14 +13,11 @@ import os
import traceback import traceback
from html import escape from html import escape
import requests import requests
import importlib
import logging import logging
import localization
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
language = configloader.config["Config"]["language"]
strings = importlib.import_module("strings." + language)
class StopSignal: class StopSignal:
"""A data class that should be sent to the worker when the conversation has to be stopped abnormally.""" """A data class that should be sent to the worker when the conversation has to be stopped abnormally."""
@ -36,12 +33,13 @@ class CancelSignal:
class Worker(threading.Thread): class Worker(threading.Thread):
"""A worker for a single conversation. A new one is created every time the /start command is sent.""" """A worker for a single conversation. A new one is created every time the /start command is sent."""
def __init__(self, bot: utils.DuckBot, chat: telegram.Chat, *args, **kwargs): def __init__(self, bot: utils.DuckBot, chat: telegram.Chat, telegram_user: telegram.User, *args, **kwargs):
# Initialize the thread # Initialize the thread
super().__init__(name=f"Worker {chat.id}", *args, **kwargs) super().__init__(name=f"Worker {chat.id}", *args, **kwargs)
# Store the bot and chat info inside the class # Store the bot and chat info inside the class
self.bot: utils.DuckBot = bot self.bot: utils.DuckBot = bot
self.chat: telegram.Chat = chat self.chat: telegram.Chat = chat
self.telegram_user: telegram.User = telegram_user
# Open a new database session # Open a new database session
log.debug(f"Opening new database session for {self.name}") log.debug(f"Opening new database session for {self.name}")
self.session = db.Session() self.session = db.Session()
@ -52,6 +50,8 @@ class Worker(threading.Thread):
self.queue = queuem.Queue() self.queue = queuem.Queue()
# The current active invoice payload; reject all invoices with a different payload # The current active invoice payload; reject all invoices with a different payload
self.invoice_payload = None self.invoice_payload = None
# The localization strings for this user
self.loc = None
# The Sentry client for reporting errors encountered by the user # The Sentry client for reporting errors encountered by the user
if configloader.config["Error Reporting"]["sentry_token"] != \ if configloader.config["Error Reporting"]["sentry_token"] != \
"https://00000000000000000000000000000000:00000000000000000000000000000000@sentry.io/0000000": "https://00000000000000000000000000000000:00000000000000000000000000000000@sentry.io/0000000":
@ -69,9 +69,7 @@ class Worker(threading.Thread):
def run(self): def run(self):
"""The conversation code.""" """The conversation code."""
# Welcome the user to the bot
log.debug("Starting conversation") 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 # Get the user db data from the users and admin tables
self.user = self.session.query(db.User).filter(db.User.user_id == self.chat.id).one_or_none() self.user = self.session.query(db.User).filter(db.User.user_id == self.chat.id).one_or_none()
self.admin = self.session.query(db.Admin).filter(db.Admin.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()
@ -80,7 +78,7 @@ class Worker(threading.Thread):
# Check if there are other registered users: if there aren't any, the first user will be owner of the bot # 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) will_be_owner = (self.session.query(db.Admin).first() is None)
# Create the new record # Create the new record
self.user = db.User(self.chat) self.user = db.User(self.telegram_user)
# Add the new record to the db # Add the new record to the db
self.session.add(self.user) self.session.add(self.user)
# Flush the session to get an userid # Flush the session to get an userid
@ -102,9 +100,13 @@ class Worker(threading.Thread):
log.info(f"Created new user: {self.user}") log.info(f"Created new user: {self.user}")
if will_be_owner: if will_be_owner:
log.warning(f"User was auto-promoted to Admin as no other admins existed: {self.user}") log.warning(f"User was auto-promoted to Admin as no other admins existed: {self.user}")
# Create the localization object
self.__create_localization()
# Capture exceptions that occour during the conversation # Capture exceptions that occour during the conversation
# noinspection PyBroadException # noinspection PyBroadException
try: 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 the user is not an admin, send him to the user menu
if self.admin is None: if self.admin is None:
self.__user_menu() self.__user_menu()
@ -120,7 +122,7 @@ class Worker(threading.Thread):
# Try to notify the user of the exception # Try to notify the user of the exception
# noinspection PyBroadException # noinspection PyBroadException
try: 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: except Exception:
pass pass
# If the Sentry integration is enabled, log the exception # If the Sentry integration is enabled, log the exception
@ -309,7 +311,7 @@ class Worker(threading.Thread):
# Find all the users in the database # Find all the users in the database
users = self.session.query(db.User).order_by(db.User.user_id).all() users = self.session.query(db.User).order_by(db.User.user_id).all()
# Create a list containing all the keyboard button strings # Create a list containing all the keyboard button strings
keyboard_buttons = [[strings.menu_cancel]] keyboard_buttons = [[self.loc.get("menu_cancel")]]
# Add to the list all the users # Add to the list all the users
for user in users: for user in users:
keyboard_buttons.append([user.identifiable_str()]) keyboard_buttons.append([user.identifiable_str()])
@ -318,7 +320,7 @@ class Worker(threading.Thread):
# Keep asking until a result is returned # Keep asking until a result is returned
while True: while True:
# Send the keyboard # 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 # Wait for a reply
reply = self.__wait_for_regex("user_([0-9]+)", cancellable=True) reply = self.__wait_for_regex("user_([0-9]+)", cancellable=True)
# Propagate CancelSignals # Propagate CancelSignals
@ -328,7 +330,7 @@ class Worker(threading.Thread):
user = self.session.query(db.User).filter_by(user_id=int(reply)).one_or_none() user = self.session.query(db.User).filter_by(user_id=int(reply)).one_or_none()
# Ensure the user exists # Ensure the user exists
if not user: 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 continue
return user return user
@ -339,38 +341,50 @@ class Worker(threading.Thread):
# Loop used to returning to the menu after executing a command # Loop used to returning to the menu after executing a command
while True: while True:
# Create a keyboard with the user main menu # Create a keyboard with the user main menu
keyboard = [[telegram.KeyboardButton(strings.menu_order)], keyboard = [[telegram.KeyboardButton(self.loc.get("menu_order"))],
[telegram.KeyboardButton(strings.menu_order_status)], [telegram.KeyboardButton(self.loc.get("menu_order_status"))],
[telegram.KeyboardButton(strings.menu_add_credit)], [telegram.KeyboardButton(self.loc.get("menu_add_credit"))],
[telegram.KeyboardButton(strings.menu_help), telegram.KeyboardButton(strings.menu_bot_info)]] [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) # Send the previously created keyboard to the user (ensuring it can be clicked only 1 time)
self.bot.send_message(self.chat.id, 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, self.loc)),
reply_markup=telegram.ReplyKeyboardMarkup(keyboard, one_time_keyboard=True)) reply_markup=telegram.ReplyKeyboardMarkup(keyboard, one_time_keyboard=True))
# Wait for a reply from the user # Wait for a reply from the user
selection = self.__wait_for_specific_message([strings.menu_order, strings.menu_order_status, selection = self.__wait_for_specific_message([
strings.menu_add_credit, strings.menu_bot_info, self.loc.get("menu_order"),
strings.menu_help]) 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 # After the user reply, update the user data
self.update_user() self.update_user()
# If the user has selected the Order option... # If the user has selected the Order option...
if selection == strings.menu_order: if selection == self.loc.get("menu_order"):
# Open the order menu # Open the order menu
self.__order_menu() self.__order_menu()
# If the user has selected the Order Status option... # 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 # Display the order(s) status
self.__order_status() self.__order_status()
# If the user has selected the Add Credit option... # 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 # Display the add credit menu
self.__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... # 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 # Display information about the bot
self.__bot_info() self.__bot_info()
# If the user has selected the Help option... # 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 # Go to the Help menu
self.__help_menu() self.__help_menu()
@ -392,24 +406,27 @@ class Worker(threading.Thread):
# Add the product to the cart # Add the product to the cart
cart[message['result']['message_id']] = [product, 0] cart[message['result']['message_id']] = [product, 0]
# Create the inline keyboard to add the product to the cart # 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(
callback_data="cart_add")]]) [[telegram.InlineKeyboardButton(self.loc.get("menu_add_to_cart"), callback_data="cart_add")]]
)
# Edit the sent message and add the inline keyboard # Edit the sent message and add the inline keyboard
if product.image is None: if product.image is None:
self.bot.edit_message_text(chat_id=self.chat.id, self.bot.edit_message_text(chat_id=self.chat.id,
message_id=message['result']['message_id'], message_id=message['result']['message_id'],
text=product.text(), text=product.text(loc=self.loc),
reply_markup=inline_keyboard) reply_markup=inline_keyboard)
else: else:
self.bot.edit_message_caption(chat_id=self.chat.id, self.bot.edit_message_caption(chat_id=self.chat.id,
message_id=message['result']['message_id'], message_id=message['result']['message_id'],
caption=product.text(), caption=product.text(loc=self.loc),
reply_markup=inline_keyboard) reply_markup=inline_keyboard)
# Create the keyboard with the cancel button # 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")]]) callback_data="cart_cancel")]])
# Send a message containing the button to cancel or pay # 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 # Wait for user input
while True: while True:
callback = self.__wait_for_inlinekeyboard_callback() callback = self.__wait_for_inlinekeyboard_callback()
@ -430,31 +447,36 @@ class Worker(threading.Thread):
# Create the product inline keyboard # Create the product inline keyboard
product_inline_keyboard = telegram.InlineKeyboardMarkup( product_inline_keyboard = telegram.InlineKeyboardMarkup(
[ [
[telegram.InlineKeyboardButton(strings.menu_add_to_cart, callback_data="cart_add"), [telegram.InlineKeyboardButton(self.loc.get("menu_add_to_cart"),
telegram.InlineKeyboardButton(strings.menu_remove_from_cart, callback_data="cart_remove")] callback_data="cart_add"),
telegram.InlineKeyboardButton(self.loc.get("menu_remove_from_cart"),
callback_data="cart_remove")]
]) ])
# Create the final inline keyboard # Create the final inline keyboard
final_inline_keyboard = telegram.InlineKeyboardMarkup( final_inline_keyboard = telegram.InlineKeyboardMarkup(
[ [
[telegram.InlineKeyboardButton(strings.menu_cancel, callback_data="cart_cancel")], [telegram.InlineKeyboardButton(self.loc.get("menu_cancel"), callback_data="cart_cancel")],
[telegram.InlineKeyboardButton(strings.menu_done, callback_data="cart_done")] [telegram.InlineKeyboardButton(self.loc.get("menu_done"), callback_data="cart_done")]
]) ])
# Edit both the product and the final message # Edit both the product and the final message
if product.image is None: if product.image is None:
self.bot.edit_message_text(chat_id=self.chat.id, self.bot.edit_message_text(chat_id=self.chat.id,
message_id=callback.message.message_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) reply_markup=product_inline_keyboard)
else: else:
self.bot.edit_message_caption(chat_id=self.chat.id, self.bot.edit_message_caption(chat_id=self.chat.id,
message_id=callback.message.message_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) reply_markup=product_inline_keyboard)
self.bot.edit_message_text( self.bot.edit_message_text(
chat_id=self.chat.id, chat_id=self.chat.id,
message_id=final_msg.message_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))), total_cost=str(self.__get_cart_value(cart))),
reply_markup=final_inline_keyboard) reply_markup=final_inline_keyboard)
# If the Remove from cart button has been pressed... # If the Remove from cart button has been pressed...
@ -470,35 +492,39 @@ class Worker(threading.Thread):
else: else:
continue continue
# Create the product inline keyboard # 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")]] callback_data="cart_add")]]
if cart[callback.message.message_id][1] > 0: 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")) callback_data="cart_remove"))
product_inline_keyboard = telegram.InlineKeyboardMarkup(product_inline_list) product_inline_keyboard = telegram.InlineKeyboardMarkup(product_inline_list)
# Create the final inline keyboard # 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: for product_id in cart:
if cart[product_id][1] > 0: 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")]) callback_data="cart_done")])
break break
final_inline_keyboard = telegram.InlineKeyboardMarkup(final_inline_list) final_inline_keyboard = telegram.InlineKeyboardMarkup(final_inline_list)
# Edit the product message # Edit the product message
if product.image is None: if product.image is None:
self.bot.edit_message_text(chat_id=self.chat.id, message_id=callback.message.message_id, 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) reply_markup=product_inline_keyboard)
else: else:
self.bot.edit_message_caption(chat_id=self.chat.id, self.bot.edit_message_caption(chat_id=self.chat.id,
message_id=callback.message.message_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) reply_markup=product_inline_keyboard)
self.bot.edit_message_text( self.bot.edit_message_text(
chat_id=self.chat.id, chat_id=self.chat.id,
message_id=final_msg.message_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))), total_cost=str(self.__get_cart_value(cart))),
reply_markup=final_inline_keyboard) reply_markup=final_inline_keyboard)
# If the done button has been pressed... # If the done button has been pressed...
@ -506,10 +532,10 @@ class Worker(threading.Thread):
# End the loop # End the loop
break break
# Create an inline keyboard with a single skip button # Create an inline keyboard with a single skip button
cancel = telegram.InlineKeyboardMarkup([[telegram.InlineKeyboardButton(strings.menu_skip, cancel = telegram.InlineKeyboardMarkup([[telegram.InlineKeyboardButton(self.loc.get("menu_skip"),
callback_data="cmd_cancel")]]) callback_data="cmd_cancel")]])
# Ask if the user wants to add notes to the order # 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 # Wait for user input
notes = self.__wait_for_regex(r"(.*)", cancellable=True) notes = self.__wait_for_regex(r"(.*)", cancellable=True)
# Create a new Order # Create a new Order
@ -530,14 +556,14 @@ class Worker(threading.Thread):
credit_required = self.__get_cart_value(cart) - self.user.credit credit_required = self.__get_cart_value(cart) - self.user.credit
# Notify user in case of insufficient credit # Notify user in case of insufficient credit
if credit_required > 0: 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 # Suggest payment for missing credit value if configuration allows refill
if configloader.config["Credit Card"]["credit_card_token"] != "" \ if configloader.config["Credit Card"]["credit_card_token"] != "" \
and configloader.config["Appearance"]["refill_on_checkout"] == 'yes' \ 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 <= \ credit_required <= \
utils.Price(int(configloader.config["Credit Card"]["max_amount"])): utils.Price(int(configloader.config["Credit Card"]["max_amount"]), self.loc):
self.__make_payment(utils.Price(credit_required)) self.__make_payment(utils.Price(credit_required, self.loc))
# If afer requested payment credit is still insufficient (either payment failure or cancel) # If afer requested payment credit is still insufficient (either payment failure or cancel)
if self.user.credit < self.__get_cart_value(cart): if self.user.credit < self.__get_cart_value(cart):
# Rollback all the changes # Rollback all the changes
@ -546,21 +572,21 @@ class Worker(threading.Thread):
# User has credit and valid order, perform transaction now # User has credit and valid order, perform transaction now
self.__order_transaction(order=order, value=-int(self.__get_cart_value(cart))) self.__order_transaction(order=order, value=-int(self.__get_cart_value(cart)))
@staticmethod def __get_cart_value(self, cart):
def __get_cart_value(cart):
# Calculate total items value in cart # Calculate total items value in cart
value = utils.Price(0) value = utils.Price(0, self.loc)
for product in cart: for product in cart:
value += cart[product][0].price * cart[product][1] value += cart[product][0].price * cart[product][1]
return value return value
@staticmethod def __get_cart_summary(self, cart):
def __get_cart_summary(cart):
# Create the cart summary # Create the cart summary
product_list = "" product_list = ""
for product_id in cart: for product_id in cart:
if cart[product_id][1] > 0: 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 return product_list
def __order_transaction(self, order, value): def __order_transaction(self, order, value):
@ -580,20 +606,22 @@ class Worker(threading.Thread):
def __order_notify_admins(self, order): def __order_notify_admins(self, order):
# Notify the user of the order result # 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, self.bot.send_message(self.chat.id, self.loc.get("success_order_created", order=order.text(loc=self.loc,
session=self.session,
user=True))) user=True)))
# Notify the admins (in Live Orders mode) of the new order # Notify the admins (in Live Orders mode) of the new order
admins = self.session.query(db.Admin).filter_by(live_mode=True).all() admins = self.session.query(db.Admin).filter_by(live_mode=True).all()
# Create the order keyboard # Create the order keyboard
order_keyboard = telegram.InlineKeyboardMarkup( order_keyboard = telegram.InlineKeyboardMarkup(
[ [
[telegram.InlineKeyboardButton(strings.menu_complete, callback_data="order_complete")], [telegram.InlineKeyboardButton(self.loc.get("menu_complete"), callback_data="order_complete")],
[telegram.InlineKeyboardButton(strings.menu_refund, callback_data="order_refund")] [telegram.InlineKeyboardButton(self.loc.get("menu_refund"), callback_data="order_refund")]
]) ])
# Notify them of the new placed order # Notify them of the new placed order
for admin in admins: for admin in admins:
self.bot.send_message(admin.user_id, self.bot.send_message(admin.user_id,
f"{strings.notification_order_placed.format(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) reply_markup=order_keyboard)
def __order_status(self): def __order_status(self):
@ -607,10 +635,10 @@ class Worker(threading.Thread):
.all() .all()
# Ensure there is at least one order to display # Ensure there is at least one order to display
if len(orders) == 0: 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 # Display the order status to the user
for order in orders: 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 # TODO: maybe add a page displayer instead of showing the latest 5 orders
def __add_credit_menu(self): def __add_credit_menu(self):
@ -620,25 +648,25 @@ class Worker(threading.Thread):
keyboard = list() keyboard = list()
# Add the supported payment methods to the keyboard # Add the supported payment methods to the keyboard
# Cash # Cash
keyboard.append([telegram.KeyboardButton(strings.menu_cash)]) keyboard.append([telegram.KeyboardButton(self.loc.get("menu_cash"))])
# Telegram Payments # Telegram Payments
if configloader.config["Credit Card"]["credit_card_token"] != "": 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: 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 # 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)) reply_markup=telegram.ReplyKeyboardMarkup(keyboard, one_time_keyboard=True))
# Wait for a reply from the user # 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) cancellable=True)
# If the user has selected the Cash option... # 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 # Go to the pay with cash function
self.bot.send_message(self.chat.id, 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... # 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 # Go to the pay with credit card function
self.__add_credit_cc() self.__add_credit_cc()
# If the user has selected the Cancel option... # If the user has selected the Cancel option...
@ -651,36 +679,32 @@ class Worker(threading.Thread):
log.debug("Displaying __add_credit_cc") log.debug("Displaying __add_credit_cc")
# Create a keyboard to be sent later # Create a keyboard to be sent later
presets = list(map(lambda s: s.strip(" "), configloader.config["Credit Card"]["payment_presets"].split('|'))) presets = list(map(lambda s: s.strip(" "), configloader.config["Credit Card"]["payment_presets"].split('|')))
keyboard = [[telegram.KeyboardButton(str(utils.Price(preset)))] for preset in presets] keyboard = [[telegram.KeyboardButton(str(utils.Price(preset, self.loc)))] 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 # Boolean variable to check if the user has cancelled the action
cancelled = False cancelled = False
# Loop used to continue asking if there's an error during the input # Loop used to continue asking if there's an error during the input
while not cancelled: while not cancelled:
# Send the message and the keyboard # 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)) reply_markup=telegram.ReplyKeyboardMarkup(keyboard, one_time_keyboard=True))
# Wait until a valid amount is sent # 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 the user cancelled the action
if isinstance(selection, CancelSignal): if isinstance(selection, CancelSignal):
# Exit the loop # Exit the loop
cancelled = True cancelled = True
continue continue
# Convert the amount to an integer # Convert the amount to an integer
value = utils.Price(selection) value = utils.Price(selection, self.loc)
# Ensure the amount is within the range # 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.bot.send_message(self.chat.id,
strings.error_payment_amount_over_max.format( self.loc.get("error_payment_amount_over_max", max_amount=utils.Price(configloader.config["Credit Card"]["max_amount"], self.loc)))
max_amount=utils.Price(configloader.config["Credit Card"]["max_amount"]))
)
continue 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.bot.send_message(self.chat.id,
strings.error_payment_amount_under_min.format( self.loc.get("error_payment_amount_under_min", min_amount=utils.Price(configloader.config["Credit Card"]["min_amount"], self.loc)))
min_amount=utils.Price(configloader.config["Credit Card"]["min_amount"]))
)
continue continue
break break
# If the user cancelled the action... # If the user cancelled the action...
@ -694,20 +718,20 @@ class Worker(threading.Thread):
# Set the invoice active invoice payload # Set the invoice active invoice payload
self.invoice_payload = str(uuid.uuid4()) self.invoice_payload = str(uuid.uuid4())
# Create the price array # 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 # 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)) fee = int(self.__get_total_fee(amount))
if fee > 0: 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)) amount=fee))
# Create the invoice keyboard # Create the invoice keyboard
inline_keyboard = telegram.InlineKeyboardMarkup([[telegram.InlineKeyboardButton(strings.menu_pay, pay=True)], inline_keyboard = telegram.InlineKeyboardMarkup([[telegram.InlineKeyboardButton(self.loc.get("menu_pay"), pay=True)],
[telegram.InlineKeyboardButton(strings.menu_cancel, [telegram.InlineKeyboardButton(self.loc.get("menu_cancel"),
callback_data="cmd_cancel")]]) callback_data="cmd_cancel")]])
# The amount is valid, send the invoice # The amount is valid, send the invoice
self.bot.send_invoice(self.chat.id, self.bot.send_invoice(self.chat.id,
title=strings.payment_invoice_title, title=self.loc.get("payment_invoice_title"),
description=strings.payment_invoice_description.format(amount=str(amount)), description=self.loc.get("payment_invoice_description", amount=str(amount)),
payload=self.invoice_payload, payload=self.invoice_payload,
provider_token=configloader.config["Credit Card"]["credit_card_token"], provider_token=configloader.config["Credit Card"]["credit_card_token"],
start_parameter="tempdeeplink", start_parameter="tempdeeplink",
@ -757,7 +781,7 @@ class Worker(threading.Thread):
def __bot_info(self): def __bot_info(self):
"""Send information about the bot.""" """Send information about the bot."""
log.debug("Displaying __bot_info") 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): def __admin_menu(self):
"""Function called from the run method when the user is an administrator. """Function called from the run method when the user is an administrator.
@ -768,52 +792,52 @@ class Worker(threading.Thread):
# Create a keyboard with the admin main menu based on the admin permissions specified in the db # Create a keyboard with the admin main menu based on the admin permissions specified in the db
keyboard = [] keyboard = []
if self.admin.edit_products: if self.admin.edit_products:
keyboard.append([strings.menu_products]) keyboard.append([self.loc.get("menu_products")])
if self.admin.receive_orders: if self.admin.receive_orders:
keyboard.append([strings.menu_orders]) keyboard.append([self.loc.get("menu_orders")])
if self.admin.create_transactions: if self.admin.create_transactions:
keyboard.append([strings.menu_edit_credit]) keyboard.append([self.loc.get("menu_edit_credit")])
keyboard.append([strings.menu_transactions, strings.menu_csv]) keyboard.append([self.loc.get("menu_transactions"), self.loc.get("menu_csv")])
if self.admin.is_owner: if self.admin.is_owner:
keyboard.append([strings.menu_edit_admins]) keyboard.append([self.loc.get("menu_edit_admins")])
keyboard.append([strings.menu_user_mode]) keyboard.append([self.loc.get("menu_user_mode")])
# Send the previously created keyboard to the user (ensuring it can be clicked only 1 time) # 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), reply_markup=telegram.ReplyKeyboardMarkup(keyboard, one_time_keyboard=True),
) )
# Wait for a reply from the user # Wait for a reply from the user
selection = self.__wait_for_specific_message([strings.menu_products, strings.menu_orders, selection = self.__wait_for_specific_message([self.loc.get("menu_products"), self.loc.get("menu_orders"),
strings.menu_user_mode, strings.menu_edit_credit, self.loc.get("menu_user_mode"), self.loc.get("menu_edit_credit"),
strings.menu_transactions, strings.menu_csv, self.loc.get("menu_transactions"), self.loc.get("menu_csv"),
strings.menu_edit_admins]) self.loc.get("menu_edit_admins")])
# If the user has selected the Products option... # If the user has selected the Products option...
if selection == strings.menu_products: if selection == self.loc.get("menu_products"):
# Open the products menu # Open the products menu
self.__products_menu() self.__products_menu()
# If the user has selected the Orders option... # If the user has selected the Orders option...
elif selection == strings.menu_orders: elif selection == self.loc.get("menu_orders"):
# Open the orders menu # Open the orders menu
self.__orders_menu() self.__orders_menu()
# If the user has selected the Transactions option... # 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 # Open the edit credit menu
self.__create_transaction() self.__create_transaction()
# If the user has selected the User mode option... # 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 # 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 # Start the bot in user mode
self.__user_menu() self.__user_menu()
# If the user has selected the Add Admin option... # 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 # Open the edit admin menu
self.__add_admin() self.__add_admin()
# If the user has selected the Transactions option... # If the user has selected the Transactions option...
elif selection == strings.menu_transactions: elif selection == self.loc.get("menu_transactions"):
# Open the transaction pages # Open the transaction pages
self.__transaction_pages() self.__transaction_pages()
# If the user has selected the .csv option... # If the user has selected the .csv option...
elif selection == strings.menu_csv: elif selection == self.loc.get("menu_csv"):
# Generate the .csv file # Generate the .csv file
self.__transactions_file() self.__transactions_file()
@ -825,13 +849,13 @@ class Worker(threading.Thread):
# Create a list of product names # Create a list of product names
product_names = [product.name for product in products] 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 # 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(0, self.loc.get("menu_cancel"))
product_names.insert(1, strings.menu_add_product) product_names.insert(1, self.loc.get("menu_add_product"))
product_names.insert(2, strings.menu_delete_product) product_names.insert(2, self.loc.get("menu_delete_product"))
# Create a keyboard using the product names # Create a keyboard using the product names
keyboard = [[telegram.KeyboardButton(product_name)] for product_name in 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) # 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)) reply_markup=telegram.ReplyKeyboardMarkup(keyboard, one_time_keyboard=True))
# Wait for a reply from the user # Wait for a reply from the user
selection = self.__wait_for_specific_message(product_names, cancellable=True) selection = self.__wait_for_specific_message(product_names, cancellable=True)
@ -840,11 +864,11 @@ class Worker(threading.Thread):
# Exit the menu # Exit the menu
return return
# If the user has selected the Add Product option... # 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 # Open the add product menu
self.__edit_product_menu() self.__edit_product_menu()
# If the user has selected the Remove Product option... # 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 # Open the delete product menu
self.__delete_product_menu() self.__delete_product_menu()
# If the user has selected a product # If the user has selected a product
@ -858,15 +882,15 @@ class Worker(threading.Thread):
"""Add a product to the database or edit an existing one.""" """Add a product to the database or edit an existing one."""
log.debug("Displaying __edit_product_menu") log.debug("Displaying __edit_product_menu")
# Create an inline keyboard with a single skip button # Create an inline keyboard with a single skip button
cancel = telegram.InlineKeyboardMarkup([[telegram.InlineKeyboardButton(strings.menu_skip, cancel = telegram.InlineKeyboardMarkup([[telegram.InlineKeyboardButton(self.loc.get("menu_skip"),
callback_data="cmd_cancel")]]) callback_data="cmd_cancel")]])
# Ask for the product name until a valid product name is specified # Ask for the product name until a valid product name is specified
while True: while True:
# Ask the question to the user # 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 # Display the current name if you're editing an existing product
if 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) reply_markup=cancel)
# Wait for an answer # Wait for an answer
name = self.__wait_for_regex(r"(.*)", cancellable=bool(product)) name = self.__wait_for_regex(r"(.*)", cancellable=bool(product))
@ -875,24 +899,24 @@ class Worker(threading.Thread):
self.session.query(db.Product).filter_by(name=name, deleted=False).one_or_none() in [None, product]: self.session.query(db.Product).filter_by(name=name, deleted=False).one_or_none() in [None, product]:
# Exit the loop # Exit the loop
break 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 # 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 # Display the current description if you're editing an existing product
if product: if product:
self.bot.send_message(self.chat.id, 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) reply_markup=cancel)
# Wait for an answer # Wait for an answer
description = self.__wait_for_regex(r"(.*)", cancellable=bool(product)) description = self.__wait_for_regex(r"(.*)", cancellable=bool(product))
# Ask for the product price # Ask for the product price
self.bot.send_message(self.chat.id, 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 # Display the current name if you're editing an existing product
if product: if product:
self.bot.send_message(self.chat.id, self.bot.send_message(self.chat.id,
strings.edit_current_value.format( 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')), if product.price is not None else 'Non in vendita')),
reply_markup=cancel) reply_markup=cancel)
# Wait for an answer # Wait for an answer
@ -904,9 +928,9 @@ class Worker(threading.Thread):
elif price.lower() == "x": elif price.lower() == "x":
price = None price = None
else: else:
price = utils.Price(price) price = utils.Price(price, self.loc)
# Ask for the product image # 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 # Wait for an answer
photo_list = self.__wait_for_photo(cancellable=True) photo_list = self.__wait_for_photo(cancellable=True)
# If a new product is being added... # If a new product is being added...
@ -935,14 +959,14 @@ class Worker(threading.Thread):
# Get the file object associated with the photo # Get the file object associated with the photo
photo_file = self.bot.get_file(largest_photo.file_id) 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 # 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") self.bot.send_chat_action(self.chat.id, action="upload_photo")
# Set the image for that product # Set the image for that product
product.set_image(photo_file) product.set_image(photo_file)
# Commit the session changes # Commit the session changes
self.session.commit() self.session.commit()
# Notify the user # 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): def __delete_product_menu(self):
log.debug("Displaying __delete_product_menu") log.debug("Displaying __delete_product_menu")
@ -951,11 +975,11 @@ class Worker(threading.Thread):
# Create a list of product names # Create a list of product names
product_names = [product.name for product in products] product_names = [product.name for product in products]
# Insert at the start of the list the Cancel button # 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 # Create a keyboard using the product names
keyboard = [[telegram.KeyboardButton(product_name)] for product_name in 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) # 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)) reply_markup=telegram.ReplyKeyboardMarkup(keyboard, one_time_keyboard=True))
# Wait for a reply from the user # Wait for a reply from the user
selection = self.__wait_for_specific_message(product_names, cancellable=True) selection = self.__wait_for_specific_message(product_names, cancellable=True)
@ -969,22 +993,22 @@ class Worker(threading.Thread):
product.deleted = True product.deleted = True
self.session.commit() self.session.commit()
# Notify the user # 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): def __orders_menu(self):
"""Display a live flow of orders.""" """Display a live flow of orders."""
log.debug("Displaying __orders_menu") log.debug("Displaying __orders_menu")
# Create a cancel and a stop keyboard # Create a cancel and a stop keyboard
stop_keyboard = telegram.InlineKeyboardMarkup([[telegram.InlineKeyboardButton(strings.menu_stop, stop_keyboard = telegram.InlineKeyboardMarkup([[telegram.InlineKeyboardButton(self.loc.get("menu_stop"),
callback_data="cmd_cancel")]]) 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")]]) callback_data="cmd_cancel")]])
# Send a small intro message on the Live Orders mode # 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 # 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")], callback_data="order_complete")],
[telegram.InlineKeyboardButton(strings.menu_refund, [telegram.InlineKeyboardButton(self.loc.get("menu_refund"),
callback_data="order_refund")]]) callback_data="order_refund")]])
# Display the past pending orders # Display the past pending orders
orders = self.session.query(db.Order) \ orders = self.session.query(db.Order) \
@ -995,7 +1019,7 @@ class Worker(threading.Thread):
# Create a message for every one of them # Create a message for every one of them
for order in orders: for order in orders:
# Send the created message # 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) reply_markup=order_keyboard)
# Set the Live mode flag to True # Set the Live mode flag to True
self.admin.live_mode = True self.admin.live_mode = True
@ -1010,12 +1034,12 @@ class Worker(threading.Thread):
self.admin.live_mode = False self.admin.live_mode = False
break break
# Find the order # 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() order = self.session.query(db.Order).filter(db.Order.order_id == order_id).one()
# Check if the order hasn't been already cleared # Check if the order hasn't been already cleared
if order.delivery_date is not None or order.refund_date is not None: if order.delivery_date is not None or order.refund_date is not None:
# Notify the admin and skip that order # 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 break
# If the user pressed the complete order button, complete the order # If the user pressed the complete order button, complete the order
if update.data == "order_complete": if update.data == "order_complete":
@ -1024,16 +1048,15 @@ class Worker(threading.Thread):
# Commit the transaction # Commit the transaction
self.session.commit() self.session.commit()
# Update order message # 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) message_id=update.message.message_id)
# Notify the user of the completition # Notify the user of the completition
self.bot.send_message(order.user_id, self.bot.send_message(order.user_id,
strings.notification_order_completed.format(order=order.get_text(self.session, self.loc.get("notification_order_completed", order=order.text(loc=self.loc, session=self.session, user=True)))
user=True)))
# If the user pressed the refund order button, refund the order... # If the user pressed the refund order button, refund the order...
elif update.data == "order_refund": elif update.data == "order_refund":
# Ask for a refund reason # 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) reply_markup=cancel_keyboard)
# Wait for a reply # Wait for a reply
reply = self.__wait_for_regex("(.*)", cancellable=True) reply = self.__wait_for_regex("(.*)", cancellable=True)
@ -1053,15 +1076,16 @@ class Worker(threading.Thread):
# Commit the changes # Commit the changes
self.session.commit() self.session.commit()
# Update the order message # 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, chat_id=self.chat.id,
message_id=update.message.message_id) message_id=update.message.message_id)
# Notify the user of the refund # Notify the user of the refund
self.bot.send_message(order.user_id, 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.text(loc=self.loc,
session=self.session,
user=True))) user=True)))
# Notify the admin of the refund # 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): def __create_transaction(self):
"""Edit manually the credit of an user.""" """Edit manually the credit of an user."""
@ -1072,19 +1096,19 @@ class Worker(threading.Thread):
if isinstance(user, CancelSignal): if isinstance(user, CancelSignal):
return return
# Create an inline keyboard with a single cancel button # 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")]]) callback_data="cmd_cancel")]])
# Request from the user the amount of money to be credited manually # 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 # Wait for an answer
reply = self.__wait_for_regex(r"(-? ?[0-9]{1,3}(?:[.,][0-9]{1,2})?)", cancellable=True) reply = self.__wait_for_regex(r"(-? ?[0-9]{1,3}(?:[.,][0-9]{1,2})?)", cancellable=True)
# Allow the cancellation of the operation # Allow the cancellation of the operation
if isinstance(reply, CancelSignal): if isinstance(reply, CancelSignal):
return return
# Convert the reply to a price object # Convert the reply to a price object
price = utils.Price(reply) price = utils.Price(reply, self.loc)
# Ask the user for notes # 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 # Wait for an answer
reply = self.__wait_for_regex(r"(.*)", cancellable=True) reply = self.__wait_for_regex(r"(.*)", cancellable=True)
# Allow the cancellation of the operation # Allow the cancellation of the operation
@ -1102,36 +1126,36 @@ class Worker(threading.Thread):
self.session.commit() self.session.commit()
# Notify the user of the credit/debit # Notify the user of the credit/debit
self.bot.send_message(user.user_id, 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 # 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): def __help_menu(self):
"""Help menu. Allows the user to ask for assistance, get a guide or see some info about the bot.""" """Help menu. Allows the user to ask for assistance, get a guide or see some info about the bot."""
log.debug("Displaying __help_menu") log.debug("Displaying __help_menu")
# Create a keyboard with the user help menu # Create a keyboard with the user help menu
keyboard = [[telegram.KeyboardButton(strings.menu_guide)], keyboard = [[telegram.KeyboardButton(self.loc.get("menu_guide"))],
[telegram.KeyboardButton(strings.menu_contact_shopkeeper)], [telegram.KeyboardButton(self.loc.get("menu_contact_shopkeeper"))],
[telegram.KeyboardButton(strings.menu_cancel)]] [telegram.KeyboardButton(self.loc.get("menu_cancel"))]]
# Send the previously created keyboard to the user (ensuring it can be clicked only 1 time) # Send the previously created keyboard to the user (ensuring it can be clicked only 1 time)
self.bot.send_message(self.chat.id, 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)) reply_markup=telegram.ReplyKeyboardMarkup(keyboard, one_time_keyboard=True))
# Wait for a reply from the user # Wait for a reply from the user
selection = self.__wait_for_specific_message([strings.menu_guide, strings.menu_contact_shopkeeper, selection = self.__wait_for_specific_message([self.loc.get("menu_guide"), self.loc.get("menu_contact_shopkeeper"),
strings.menu_cancel]) self.loc.get("menu_cancel")])
# If the user has selected the Guide option... # 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 # 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... # 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 # Find the list of available shopkeepers
shopkeepers = self.session.query(db.Admin).filter_by(display_on_help=True).join(db.User).all() shopkeepers = self.session.query(db.Admin).filter_by(display_on_help=True).join(db.User).all()
# Create the string # Create the string
shopkeepers_string = "\n".join([admin.user.mention() for admin in shopkeepers]) shopkeepers_string = "\n".join([admin.user.mention() for admin in shopkeepers])
# Send the message to the user # 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 # If the user has selected the Cancel option the function will return immediately
def __transaction_pages(self): def __transaction_pages(self):
@ -1140,7 +1164,7 @@ class Worker(threading.Thread):
# Page number # Page number
page = 0 page = 0
# Create and send a placeholder message to be populated # Create and send a placeholder message to be populated
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 # Loop used to move between pages
while True: while True:
# Retrieve the 10 transactions in that page # Retrieve the 10 transactions in that page
@ -1155,22 +1179,21 @@ class Worker(threading.Thread):
if page != 0: if page != 0:
# Add a previous page button # Add a previous page button
inline_keyboard_list[0].append( 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 # Don't add a next page button if this is the last page
if len(transactions) == 10: if len(transactions) == 10:
# Add a next page button # Add a next page button
inline_keyboard_list[0].append( 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 # 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 # Create the inline keyboard markup
inline_keyboard = telegram.InlineKeyboardMarkup(inline_keyboard_list) inline_keyboard = telegram.InlineKeyboardMarkup(inline_keyboard_list)
# Create the message text # Create the message text
transactions_string = "\n".join([str(transaction) for transaction in transactions]) transactions_string = "\n".join([str(transaction) for transaction in transactions])
text = strings.transactions_page.format(page=page + 1, text = self.loc.get("transactions_page", page=page + 1, transactions=transactions_string)
transactions=transactions_string)
# Update the previously sent message # Update the previously sent message
self.bot.edit_message_text(chat_id=self.chat.id, message_id=message.message_id, text=text, self.bot.edit_message_text(chat_id=self.chat.id, message_id=message.message_id, text=text,
reply_markup=inline_keyboard) reply_markup=inline_keyboard)
@ -1224,7 +1247,7 @@ class Worker(threading.Thread):
f"{transaction.payment_email if transaction.payment_email is not None else ''};" f"{transaction.payment_email if transaction.payment_email is not None else ''};"
f"{transaction.refunded if transaction.refunded is not None else ''}\n") f"{transaction.refunded if transaction.refunded is not None else ''}\n")
# Describe the file to the user # 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 # Reopen the file for reading
with open(f"transactions_{self.chat.id}.csv") as file: with open(f"transactions_{self.chat.id}.csv") as file:
# Send the file via a manual request to Telegram # Send the file via a manual request to Telegram
@ -1247,13 +1270,13 @@ class Worker(threading.Thread):
admin = self.session.query(db.Admin).filter_by(user_id=user.user_id).one_or_none() admin = self.session.query(db.Admin).filter_by(user_id=user.user_id).one_or_none()
if admin is None: if admin is None:
# Create the keyboard to be sent # 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 # 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 # 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 # Proceed only if the answer is yes
if selection == strings.emoji_no: if selection == self.loc.get("emoji_no"):
return return
# Create a new admin # Create a new admin
admin = db.Admin(user=user, admin = db.Admin(user=user,
@ -1264,22 +1287,22 @@ class Worker(threading.Thread):
display_on_help=False) display_on_help=False)
self.session.add(admin) self.session.add(admin)
# Send the empty admin message and record the id # 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 # Start accepting edits
while True: while True:
# Create the inline keyboard with the admin status # Create the inline keyboard with the admin status
inline_keyboard = telegram.InlineKeyboardMarkup([ inline_keyboard = telegram.InlineKeyboardMarkup([
[telegram.InlineKeyboardButton(f"{utils.boolmoji(admin.edit_products)} {strings.prop_edit_products}", [telegram.InlineKeyboardButton(f"{self.loc.boolmoji(admin.edit_products)} {self.loc.get('prop_edit_products')}",
callback_data="toggle_edit_products")], callback_data="toggle_edit_products")],
[telegram.InlineKeyboardButton(f"{utils.boolmoji(admin.receive_orders)} {strings.prop_receive_orders}", [telegram.InlineKeyboardButton(f"{self.loc.boolmoji(admin.receive_orders)} {self.loc.get('prop_receive_orders')}",
callback_data="toggle_receive_orders")], callback_data="toggle_receive_orders")],
[telegram.InlineKeyboardButton( [telegram.InlineKeyboardButton(
f"{utils.boolmoji(admin.create_transactions)} {strings.prop_create_transactions}", f"{self.loc.boolmoji(admin.create_transactions)} {self.loc.get('prop_create_transactions')}",
callback_data="toggle_create_transactions")], callback_data="toggle_create_transactions")],
[telegram.InlineKeyboardButton( [telegram.InlineKeyboardButton(
f"{utils.boolmoji(admin.display_on_help)} {strings.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")], 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 # Update the inline keyboard
self.bot.edit_message_reply_markup(message_id=message.message_id, self.bot.edit_message_reply_markup(message_id=message.message_id,
@ -1300,13 +1323,67 @@ class Worker(threading.Thread):
break break
self.session.commit() 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): def __graceful_stop(self, stop_trigger: StopSignal):
"""Handle the graceful stop of the thread.""" """Handle the graceful stop of the thread."""
log.debug("Gracefully stopping the conversation") log.debug("Gracefully stopping the conversation")
# If the session has expired... # If the session has expired...
if stop_trigger.reason == "timeout": if stop_trigger.reason == "timeout":
# Notify the user that the session has expired and remove the keyboard # Notify the user that the session has expired and remove the keyboard
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()) reply_markup=telegram.ReplyKeyboardRemove())
# If a restart has been requested... # If a restart has been requested...
# Do nothing. # Do nothing.