diff --git a/database.py b/database.py
index 2b2893f..cf72499 100644
--- a/database.py
+++ b/database.py
@@ -1,10 +1,11 @@
from sqlalchemy import create_engine, Column, ForeignKey, UniqueConstraint, CheckConstraint
-from sqlalchemy import Integer, BigInteger, String, Numeric, Text
+from sqlalchemy import Integer, BigInteger, String, Text, LargeBinary
from sqlalchemy.orm import sessionmaker, relationship
from sqlalchemy.ext.declarative import declarative_base
import configloader
import telegram
import strings
+import requests
from html import escape
# Create a (lazy) database engine
@@ -67,8 +68,8 @@ class Product(TableDeclarativeBase):
description = Column(Text)
# Product price, if null product is not for sale
price = Column(Integer)
- # Image filename
- image = Column(String)
+ # Image data
+ image = Column(LargeBinary)
# Stock quantity, if null product has infinite stock
stock = Column(Integer)
@@ -79,15 +80,29 @@ class Product(TableDeclarativeBase):
def __str__(self):
"""Return the product details formatted with Telegram HTML. The image is omitted."""
- 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" \
+ 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)}"
def __repr__(self):
return f""
- # TODO: add get image (and set image?) method(s)
+ 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
+
+ def set_image(self, file: telegram.File):
+ """Download an image from Telegram and store it in the image column.
+ This is a slow blocking function. Try to avoid calling it directly, use a thread if possible."""
+ # Download the photo through a get request
+ r = requests.get(file.file_path)
+ # Store the photo in the database record
+ self.image = r.content
class Transaction(TableDeclarativeBase):
diff --git a/strings.py b/strings.py
index b6e8100..9daec4b 100644
--- a/strings.py
+++ b/strings.py
@@ -75,7 +75,20 @@ ask_product_name = "Come si deve chiamare il prodotto?"
ask_product_description = "Quale deve essere la descrizione del prodotto?"
# Add product: price?
-ask_product_price = "Quanto deve costare il prodotto?"
+ask_product_price = "Quanto deve costare il prodotto?\n" \
+ "Scrivi skip
se vuoi che il prodotto non sia ancora in vendita."
+
+# Add product: image?
+ask_product_image = "Che immagine vuoi che abbia il prodotto?"
+
+# Thread has started downloading an image and might be unresponsive
+downloading_image = "Sto scaricando la tua foto!\n" \
+ "Potrei metterci un po'... Abbi pazienza!\n" \
+ "Non sarò in grado di risponderti durante il download."
+
+# Edit product: current value
+edit_current_value = "Il valore attuale è:\n" \
+ "{value}
"
# Payment: cash payment info
payment_cash = "Puoi pagare in contanti alla sede fisica del negozio.\n" \
@@ -105,6 +118,9 @@ bot_info = 'Questo bot utilizza gree
# Success: product has been added to the database
success_product_added = "✅ Il prodotto è stato aggiunto con successo!"
+# Success: product has been added to the database
+success_product_edited = "✅ Il prodotto è stato modificato con successo!"
+
# Error: message received not in a private chat
error_nonprivate_chat = "⚠️ Questo bot funziona solo in chat private."
diff --git a/worker.py b/worker.py
index a565742..126db57 100644
--- a/worker.py
+++ b/worker.py
@@ -8,6 +8,8 @@ import sys
import queue as queuem
import database as db
import re
+import requests
+from html import escape
class StopSignal:
"""A data class that should be sent to the worker when the conversation has to be stopped abnormally."""
@@ -144,6 +146,20 @@ class ChatWorker(threading.Thread):
# Return the successfulpayment
return update.message.successful_payment
+ def __wait_for_photo(self) -> typing.List[telegram.PhotoSize]:
+ """Continue getting updates until a photo is received, then download and return it."""
+ while True:
+ # Get the next update
+ update = self.__receive_next_update()
+ # Ensure the update contains a message
+ if update.message is None:
+ continue
+ # Ensure the message contains a photo
+ if update.message.photo is None:
+ continue
+ # Return the photo array
+ return update.message.photo
+
def __user_menu(self):
"""Function called from the run method when the user is not an administrator.
Normal bot actions should be placed here."""
@@ -339,47 +355,85 @@ class ChatWorker(threading.Thread):
# If the user has selected the Add Product option...
if selection == strings.menu_add_product:
# Open the add product menu
- self.__add_product_menu()
+ self.__edit_product_menu()
# If the user has selected a product
else:
+ # Find the selected product
+ product = self.session.query(db.Product).filter_by(name=selection).one()
# Open the edit menu for that specific product
- self.__edit_product_menu(selection)
+ self.__edit_product_menu(product=product)
- def __add_product_menu(self):
- """Add a product to the database."""
+ def __edit_product_menu(self, product: typing.Optional[db.Product]=None):
+ """Add a product to the database or edit an existing one."""
# Ask for the product name until a valid product name is specified
while True:
# Ask the question to the user
self.bot.send_message(self.chat.id, strings.ask_product_name)
+ # Display the current name if you're editing an existing product
+ if product:
+ self.bot.send_message(self.chat.id, strings.edit_current_value.format(value=escape(product.name)), parse_mode="HTML")
# Wait for an answer
name = self.__wait_for_regex(r"(.*)")
# Ensure a product with that name doesn't already exist
- if self.session.query(db.Product).filter_by(name=name).one_or_none() is None:
+ if self.session.query(db.Product).filter_by(name=name).one_or_none() in [None, product]:
# Exit the loop
break
self.bot.send_message(self.chat.id, strings.error_duplicate_name)
# Ask for the product description
self.bot.send_message(self.chat.id, strings.ask_product_description)
+ # Display the current description if you're editing an existing product
+ if product:
+ self.bot.send_message(self.chat.id, strings.edit_current_value.format(value=escape(product.description)), parse_mode="HTML")
# Wait for an answer
description = self.__wait_for_regex(r"(.*)")
# Ask for the product price
self.bot.send_message(self.chat.id, strings.ask_product_price)
+ # Display the current name if you're editing an existing product
+ if product:
+ self.bot.send_message(self.chat.id, strings.edit_current_value.format(value=(strings.currency_format_string.format(symbol=strings.currency_symbol, value=(product.price / (10 ** int(configloader.config["Payments"]["currency_exp"]))))) if product.price is not None else 'Non in vendita'), parse_mode="HTML")
# Wait for an answer
- price = int(self.__wait_for_regex(r"([0-9]{1,3}(?:[.,][0-9]{1,2})?)").replace(".", "").replace(",", "")) * (
- 10 ** int(configloader.config["Payments"]["currency_exp"]))
- # TODO: ask for product image
- # Create the db record for the product
- product = db.Product(name=name,
- description=description,
- price=price)
- # Add the record to the session, then commit
- self.session.add(product)
+ price = self.__wait_for_regex(r"([0-9]{1,3}(?:[.,][0-9]{1,2})?|[Ss][Kk][Ii][Pp])")
+ # If the price is skipped
+ if price.lower() == "skip":
+ price = None
+ else:
+ price = int(price.replace(".", "").replace(",", "")) * (10 ** int(configloader.config["Payments"]["currency_exp"]))
+ # Ask for the product image
+ self.bot.send_message(self.chat.id, strings.ask_product_image)
+ # Wait for an answer
+ photo_list = self.__wait_for_photo()
+ # If a new product is being added...
+ if not product:
+ # Create the db record for the product
+ product = db.Product(name=name,
+ description=description,
+ price=price)
+ # Add the record to the database
+ self.session.add(product)
+ # If a product is being edited...
+ else:
+ # Edit the record with the new values
+ product.name = name
+ product.description = description
+ product.price = price
+ # Find the largest photo id
+ largest_photo = photo_list[0]
+ for photo in photo_list[1:]:
+ if photo.width > largest_photo.width:
+ largest_photo = photo
+ # Get the file object associated with the photo
+ photo_file = self.bot.get_file(largest_photo.file_id)
+ # Notify the user that the bot is downloading the image and might be inactive for a while
+ self.bot.send_message(self.chat.id, strings.downloading_image)
+ self.bot.send_chat_action(self.chat.id, action="upload_photo")
+ # Set the image for that product
+ product.set_image(photo_file)
self.session.commit()
# Notify the user
- self.bot.send_message(self.chat.id, strings.success_product_added)
-
- def __edit_product_menu(self, name: str):
- raise NotImplementedError()
+ if product:
+ self.bot.send_message(self.chat.id, strings.success_product_edited)
+ else:
+ self.bot.send_message(self.chat.id, strings.success_product_added)
def __orders_menu(self):
raise NotImplementedError()