diff --git a/database.py b/database.py
index d04e1a4..3bebc4d 100644
--- a/database.py
+++ b/database.py
@@ -80,25 +80,36 @@ class Product(TableDeclarativeBase):
# No __init__ is needed, the default one is sufficient
- def __str__(self, one_row=False):
+ def __str__(self):
+ return self.text()
+
+ def text(self, one_row:bool=False, cart_qty:int=None):
"""Return the product details formatted with Telegram HTML. The image is omitted."""
if one_row:
return f"{escape(self.name)} - {strings.currency_format_string.format(symbol=strings.currency_symbol, value=self.price)}"
- return f"{escape(self.name)}\n" \
- f"{escape(self.description)}\n\n" \
- f"{strings.in_stock_format_string.format(quantity=self.stock) if self.stock is not None else ''}\n" \
- f"{strings.currency_format_string.format(symbol=strings.currency_symbol, value=self.price)}"
+ return f"{escape(self.name)}\n" \
+ f"{escape(self.description)}\n" \
+ f"{strings.in_stock_format_string.format(quantity=self.stock) if self.stock is not None else ''}\n" \
+ f"{strings.currency_format_string.format(symbol=strings.currency_symbol, value=self.price)}\n" \
+ f"{strings.in_cart_format_string.format(quantity=cart_qty) if cart_qty is not None else ''}"
def __repr__(self):
return f""
- def send_as_message(self, chat_id: int) -> requests.Response:
+ def send_as_message(self, chat_id: int) -> dict:
"""Send a message containing the product data."""
- r = requests.post(f"https://api.telegram.org/bot{configloader.config['Telegram']['token']}/sendPhoto",
- files={"photo": self.image},
- params={"chat_id": chat_id,
- "caption": str(self)})
- return r
+ if self.image is None:
+ r = requests.get(f"https://api.telegram.org/bot{configloader.config['Telegram']['token']}/sendMessage",
+ params={"chat_id": chat_id,
+ "text": str(self),
+ "parse_mode": "HTML"})
+ else:
+ r = requests.post(f"https://api.telegram.org/bot{configloader.config['Telegram']['token']}/sendPhoto",
+ files={"photo": self.image},
+ params={"chat_id": chat_id,
+ "caption": str(self),
+ "parse_mode": "HTML"})
+ return r.json()
def set_image(self, file: telegram.File):
"""Download an image from Telegram and store it in the image column.
diff --git a/strings.py b/strings.py
index 989129f..19b7bbc 100644
--- a/strings.py
+++ b/strings.py
@@ -11,6 +11,9 @@ currency_format_string = "{symbol} {value}"
# Quantity of a product in stock
in_stock_format_string = "{quantity} disponibili"
+# Copies of a product in cart
+in_cart_format_string = "{quantity} nel carrello"
+
# Conversation: the start command was sent and the bot should welcome the user
conversation_after_start = "Ciao!\n" \
"Benvenuto su greed!"
@@ -34,6 +37,9 @@ conversation_admin_select_product = "Che prodotto vuoi modificare?"
# Conversation: add extra notes to the order
conversation_extra_notes = "Che messaggio vuoi lasciare insieme al tuo ordine?"
+# Conversation: click below to pay for the purchase
+conversation_cart_actions = "Quando hai finito di aggiungere prodotti al carrello, clicca uno dei pulsanti qui sotto!"
+
# Conversation: confirm the cart contents
conversation_confirm_cart = "Il tuo carrello contiene questi prodotti:\n" \
"{product_list}\n" \
@@ -87,6 +93,12 @@ menu_done = "✅️ Fatto"
# Menu: pay invoice
menu_pay = "💳 Paga"
+# Menu: add to cart
+menu_add_to_cart = "➕ Aggiungi"
+
+# Menu: remove from cart
+menu_remove_from_cart = "➖ Rimuovi"
+
# Add product: name?
ask_product_name = "Come si deve chiamare il prodotto?"
diff --git a/worker.py b/worker.py
index 3a2009e..61b53e2 100644
--- a/worker.py
+++ b/worker.py
@@ -163,7 +163,7 @@ class ChatWorker(threading.Thread):
return update.message.successful_payment
def __wait_for_photo(self, cancellable:bool=False) -> typing.Union[typing.List[telegram.PhotoSize], CancelSignal]:
- """Continue getting updates until a photo is received, then download and return it."""
+ """Continue getting updates until a photo is received, then return it."""
while True:
# Get the next update
update = self.__receive_next_update()
@@ -180,6 +180,17 @@ class ChatWorker(threading.Thread):
# Return the photo array
return update.message.photo
+ def __wait_for_inlinekeyboard_callback(self) -> telegram.CallbackQuery:
+ """Continue getting updates until an inline keyboard callback is received, then return it."""
+ while True:
+ # Get the next update
+ update = self.__receive_next_update()
+ # Ensure the update is a CallbackQuery
+ if update.callback_query is None:
+ continue
+ # Return the callbackquery
+ return update.callback_query
+
def __user_menu(self):
"""Function called from the run method when the user is not an administrator.
Normal bot actions should be placed here."""
@@ -215,75 +226,98 @@ class ChatWorker(threading.Thread):
def __order_menu(self):
"""User menu to order products from the shop."""
- raise NotImplementedError()
- # # Create a list with the requested items
- # order_items = []
- # # Get the products list from the db
- # products = self.session.query(db.Product).all()
- # # TODO: this should be changed
- # # Loop exit reason
- # exit_reason = None
- # # Ask for a list of products to order
- # while True:
- # # Create a list of product names
- # product_names = [product.name for product in products]
- # # Add a Cancel button at the end of the keyboard
- # product_names.append(strings.menu_cancel)
- # # If at least 1 product has been ordered, add a Done button at the start of the keyboard
- # if len(order_items) > 0:
- # product_names.insert(0, strings.menu_done)
- # # Create a keyboard using the product names
- # keyboard = [[telegram.KeyboardButton(product_name)] for product_name in product_names]
- # # Wait for an answer
- # selection = self.__wait_for_specific_message(product_names)
- # # If the user selected the Cancel option...
- # if selection == strings.menu_cancel:
- # exit_reason = "Cancel"
- # break
- # # If the user selected the Done option...
- # elif selection == strings.menu_done:
- # exit_reason = "Done"
- # break
- # # If the user selected a product...
- # else:
- # # Find the selected product
- # product = self.session.query(db.Product).filter_by(name=selection).one()
- # # Add the product to the order_items list
- # order_items.append(product)
- # # Ask for extra notes
- # self.bot.send_message(self.chat.id, strings.conversation_extra_notes)
- # # Wait for an answer
- # notes = self.__wait_for_regex("(.+)")
- # # Create the confirmation message and find the total cost
- # total_cost = 0
- # product_list_string = ""
- # for item in order_items:
- # # Add to the string and the cost
- # product_list_string += f"{str(item)}\n"
- # total_cost += item.price
- # # Send the confirmation message
- # self.bot.send_message(self.chat.id, strings.conversation_confirm_cart.format(product_list=product_list_string, total_cost=strings.currency_format_string.format(symbol=strings.currency_symbol, value=(total_cost / (10 ** int(configloader.config["Payments"]["currency_exp"]))))))
- # # TODO: wait for an answer
- # # TODO: create a new transaction
- # # TODO: test the code
- # # TODO: everything
- # # Create the order record and add it to the session
- # order = db.Order(user=self.user,
- # creation_date=datetime.datetime.now(),
- # notes=notes)
- # self.session.add(order)
- # # Commit the session so the order record gets an id
- # self.session.commit()
- # # Create the orderitems for the selected products
- # for item in order_items:
- # item_record = db.OrderItem(product=item,
- # order_id=order.order_id)
- # # Add the created item to the session
- # self.session.add(item_record)
- # # Commit the session
- # self.session.commit()
- # # Send a confirmation to the user
- # self.bot.send_message(self.chat.id, strings.success_order_created)
+ # Get the products list from the db
+ products = self.session.query(db.Product).all()
+ # Create a dict to be used as 'cart'
+ # The key is the message id of the product list
+ cart = {} # type: typing.Dict[typing.List[db.Product, int]]
+ # Initialize the products list
+ for product in products:
+ # If the product is not for sale, don't display it
+ if product.price is None:
+ continue
+ # Send the message without the keyboard to get the message id
+ message = product.send_as_message(self.chat.id)
+ # Add the product to the cart
+ cart[message['result']['message_id']] = [product, 0]
+ # Create the inline keyboard to add the product to the cart
+ inline_keyboard = telegram.InlineKeyboardMarkup([[telegram.InlineKeyboardButton(strings.menu_add_to_cart, callback_data="cart_add")]])
+ # Edit the sent message and add the inline keyboard
+ if product.image is None:
+ self.bot.edit_message_text(chat_id=self.chat.id, message_id=message['result']['message_id'], text=str(product), parse_mode="HTML", reply_markup=inline_keyboard)
+ else:
+ self.bot.edit_message_caption(chat_id=self.chat.id, message_id=message['result']['message_id'], caption=str(product), parse_mode="HTML", reply_markup=inline_keyboard)
+ # Create the keyboard with the cancel button
+ inline_keyboard = telegram.InlineKeyboardMarkup([[telegram.InlineKeyboardButton(strings.menu_cancel, callback_data="cart_cancel")]])
+ # Send a message containing the button to cancel or pay
+ final = self.bot.send_message(self.chat.id, strings.conversation_cart_actions, reply_markup=inline_keyboard)
+ # Wait for user input
+ while True:
+ callback = self.__wait_for_inlinekeyboard_callback()
+ # React to the user input
+ # If the cancel button has been pressed...
+ if callback.data == "cart_cancel":
+ # Stop waiting for user input and go back to the previous menu
+ return
+ # If a Add to Cart button has been pressed...
+ elif callback.data == "cart_add":
+ # Get the selected product
+ product = cart[callback.message.message_id][0]
+ # Add 1 copy to the cart
+ cart[callback.message.message_id][1] += 1
+ # Create the product inline keyboard
+ product_inline_keyboard = telegram.InlineKeyboardMarkup([[telegram.InlineKeyboardButton(strings.menu_add_to_cart, callback_data="cart_add")],
+ [telegram.InlineKeyboardButton(strings.menu_remove_from_cart, callback_data="cart_remove")]])
+ # Create the final inline keyboard
+ final_inline_keyboard = telegram.InlineKeyboardMarkup([[telegram.InlineKeyboardButton(strings.menu_cancel, callback_data="cart_cancel")],
+ [telegram.InlineKeyboardButton(strings.menu_done, callback_data="cart_done")]])
+ # Edit both the product and the final message
+ if product.image is None:
+ self.bot.edit_message_text(chat_id=self.chat.id, message_id=callback.message.message_id,
+ text=product.text(cart_qty=cart[callback.message.message_id][1]), parse_mode="HTML", reply_markup=product_inline_keyboard)
+ else:
+ self.bot.edit_message_caption(chat_id=self.chat.id, message_id=callback.message.message_id,
+ caption=product.text(cart_qty=cart[callback.message.message_id][1]), parse_mode="HTML", reply_markup=product_inline_keyboard)
+ try:
+ self.bot.edit_message_text(chat_id=self.chat.id, message_id=final.message_id,
+ text=strings.conversation_cart_actions, reply_markup=final_inline_keyboard)
+ except telegram.error.BadRequest:
+ # Telegram returns an error if the message is not edited
+ pass
+ # If the Remove from cart button has been pressed...
+ elif callback.data == "cart_remove":
+ # Get the selected product
+ product = cart[callback.message.message_id][0]
+ # Remove 1 copy from the cart
+ if cart[callback.message.message_id][1] > 0:
+ cart[callback.message.message_id][1] -= 1
+ else:
+ continue
+ # Create the product inline keyboard
+ product_inline_list = [[telegram.InlineKeyboardButton(strings.menu_add_to_cart, callback_data="cart_add")]]
+ if cart[callback.message.message_id][1] > 0:
+ product_inline_list.append([telegram.InlineKeyboardButton(strings.menu_remove_from_cart, callback_data="cart_remove")])
+ product_inline_keyboard = telegram.InlineKeyboardMarkup(product_inline_list)
+ # Create the final inline keyboard
+ final_inline_list = [[telegram.InlineKeyboardButton(strings.menu_cancel, callback_data="cart_cancel")]]
+ for product_id in cart:
+ if cart[product_id][1] > 0:
+ final_inline_list.append([telegram.InlineKeyboardButton(strings.menu_done, callback_data="cart_done")])
+ break
+ final_inline_keyboard = telegram.InlineKeyboardMarkup(final_inline_list)
+ # Edit both the product and the final message
+ if product.image is None:
+ self.bot.edit_message_text(chat_id=self.chat.id, message_id=callback.message.message_id,
+ text=product.text(cart_qty=cart[callback.message.message_id][1]), parse_mode="HTML", reply_markup=product_inline_keyboard)
+ else:
+ self.bot.edit_message_caption(chat_id=self.chat.id, message_id=callback.message.message_id,
+ caption=product.text(cart_qty=cart[callback.message.message_id][1]), parse_mode="HTML", reply_markup=product_inline_keyboard)
+ try:
+ self.bot.edit_message_text(chat_id=self.chat.id, message_id=final.message_id,
+ text=strings.conversation_cart_actions, reply_markup=final_inline_keyboard)
+ except telegram.error.BadRequest:
+ # Telegram returns an error if the message is not edited
+ pass
def __order_status(self):
raise NotImplementedError()