2017-12-14 07:53:16 +00:00
|
|
|
import threading
|
2017-12-21 09:42:23 +00:00
|
|
|
import typing
|
2018-01-10 10:29:02 +00:00
|
|
|
import uuid
|
2018-02-19 12:47:43 +00:00
|
|
|
import datetime
|
2017-12-08 15:44:11 +00:00
|
|
|
import telegram
|
2017-12-12 18:56:38 +00:00
|
|
|
import strings
|
2017-12-13 10:20:53 +00:00
|
|
|
import configloader
|
|
|
|
import sys
|
2017-12-14 07:53:16 +00:00
|
|
|
import queue as queuem
|
2017-12-21 09:42:23 +00:00
|
|
|
import database as db
|
2018-01-03 13:52:05 +00:00
|
|
|
import re
|
2018-04-05 07:57:59 +00:00
|
|
|
import utils
|
2018-04-12 08:21:11 +00:00
|
|
|
import os
|
2018-02-01 09:06:40 +00:00
|
|
|
from html import escape
|
2017-12-13 10:20:53 +00:00
|
|
|
|
2018-04-05 08:34:14 +00:00
|
|
|
|
2017-12-12 18:56:38 +00:00
|
|
|
class StopSignal:
|
|
|
|
"""A data class that should be sent to the worker when the conversation has to be stopped abnormally."""
|
|
|
|
|
|
|
|
def __init__(self, reason: str=""):
|
|
|
|
self.reason = reason
|
|
|
|
|
2017-12-08 15:44:11 +00:00
|
|
|
|
2018-02-08 09:18:53 +00:00
|
|
|
class CancelSignal:
|
|
|
|
"""An empty class that is added to the queue whenever the user presses a cancel inline button."""
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
2017-12-14 08:40:03 +00:00
|
|
|
class ChatWorker(threading.Thread):
|
|
|
|
"""A worker for a single conversation. A new one is created every time the /start command is sent."""
|
2017-12-08 15:44:11 +00:00
|
|
|
|
2018-04-05 07:57:59 +00:00
|
|
|
def __init__(self, bot: utils.DuckBot, chat: telegram.Chat, *args, **kwargs):
|
2017-12-14 08:40:03 +00:00
|
|
|
# Initialize the thread
|
|
|
|
super().__init__(name=f"ChatThread {chat.first_name}", *args, **kwargs)
|
|
|
|
# Store the bot and chat info inside the class
|
2018-04-05 07:57:59 +00:00
|
|
|
self.bot: utils.DuckBot = bot
|
|
|
|
self.chat: telegram.Chat = chat
|
2017-12-21 09:42:23 +00:00
|
|
|
# Open a new database session
|
|
|
|
self.session = db.Session()
|
|
|
|
# Get the user db data from the users and admin tables
|
2018-04-05 07:57:59 +00:00
|
|
|
self.user: typing.Optional[db.User] = None
|
|
|
|
self.admin: typing.Optional[db.Admin] = None
|
2017-12-08 15:44:11 +00:00
|
|
|
# The sending pipe is stored in the ChatWorker class, allowing the forwarding of messages to the chat process
|
2017-12-14 07:53:16 +00:00
|
|
|
self.queue = queuem.Queue()
|
2018-01-04 19:11:48 +00:00
|
|
|
# The current active invoice payload; reject all invoices with a different payload
|
|
|
|
self.invoice_payload = None
|
2018-04-12 08:21:11 +00:00
|
|
|
# The Sentry client for reporting errors encountered by the user
|
|
|
|
if configloader.config["Error Reporting"]["sentry_token"] != \
|
2018-04-12 08:21:49 +00:00
|
|
|
"https://00000000000000000000000000000000:00000000000000000000000000000000@sentry.io/0000000":
|
2018-04-12 08:21:11 +00:00
|
|
|
import raven
|
|
|
|
self.sentry_client = raven.Client(configloader.config["Error Reporting"]["sentry_token"],
|
|
|
|
release=raven.fetch_git_sha(os.path.dirname(__file__)),
|
|
|
|
environment="Dev" if __debug__ else "Prod")
|
|
|
|
else:
|
|
|
|
self.sentry_client = None
|
2017-12-08 15:44:11 +00:00
|
|
|
|
2017-12-14 08:40:03 +00:00
|
|
|
def run(self):
|
|
|
|
"""The conversation code."""
|
|
|
|
# Welcome the user to the bot
|
|
|
|
self.bot.send_message(self.chat.id, strings.conversation_after_start)
|
2018-01-10 10:29:02 +00:00
|
|
|
# Get the user db data from the users and admin tables
|
|
|
|
self.user = self.session.query(db.User).filter(db.User.user_id == self.chat.id).one_or_none()
|
|
|
|
self.admin = self.session.query(db.Admin).filter(db.Admin.user_id == self.chat.id).one_or_none()
|
2017-12-21 09:42:23 +00:00
|
|
|
# If the user isn't registered, create a new record and add it to the db
|
|
|
|
if self.user is None:
|
|
|
|
# Create the new record
|
|
|
|
self.user = db.User(self.chat)
|
|
|
|
# Add the new record to the db
|
|
|
|
self.session.add(self.user)
|
|
|
|
# Commit the transaction
|
|
|
|
self.session.commit()
|
2018-04-12 08:21:11 +00:00
|
|
|
# Capture exceptions that occour during the conversation
|
|
|
|
try:
|
|
|
|
# If the user is not an admin, send him to the user menu
|
|
|
|
if self.admin is None:
|
|
|
|
self.__user_menu()
|
|
|
|
# If the user is an admin, send him to the admin menu
|
|
|
|
else:
|
|
|
|
# Clear the live orders flag
|
|
|
|
self.admin.live_mode = False
|
|
|
|
# Commit the change
|
|
|
|
self.session.commit()
|
|
|
|
# Open the admin menu
|
|
|
|
self.__admin_menu()
|
|
|
|
except Exception:
|
|
|
|
# Try to notify the user of the exception
|
|
|
|
try:
|
|
|
|
self.bot.send_message(self.chat.id, strings.fatal_conversation_exception)
|
|
|
|
except Exception:
|
|
|
|
pass
|
|
|
|
# If the Sentry integration is enabled, log the exception
|
|
|
|
if self.sentry_client is not None:
|
|
|
|
self.sentry_client.captureException()
|
2017-12-08 15:44:11 +00:00
|
|
|
|
2017-12-12 18:56:38 +00:00
|
|
|
def stop(self, reason: str=""):
|
|
|
|
"""Gracefully stop the worker process"""
|
2017-12-14 07:53:16 +00:00
|
|
|
# Send a stop message to the thread
|
|
|
|
self.queue.put(StopSignal(reason))
|
|
|
|
# Wait for the thread to stop
|
2017-12-14 08:40:03 +00:00
|
|
|
self.join()
|
2017-12-08 15:44:11 +00:00
|
|
|
|
2018-04-05 08:34:14 +00:00
|
|
|
# noinspection PyUnboundLocalVariable
|
2017-12-21 09:42:23 +00:00
|
|
|
def __receive_next_update(self) -> telegram.Update:
|
2017-12-14 08:40:03 +00:00
|
|
|
"""Get the next update from the queue.
|
|
|
|
If no update is found, block the process until one is received.
|
|
|
|
If a stop signal is sent, try to gracefully stop the thread."""
|
|
|
|
# Pop data from the queue
|
|
|
|
try:
|
2017-12-17 15:49:46 +00:00
|
|
|
data = self.queue.get(timeout=int(configloader.config["Telegram"]["conversation_timeout"]))
|
2017-12-14 08:40:03 +00:00
|
|
|
except queuem.Empty:
|
|
|
|
# If the conversation times out, gracefully stop the thread
|
2018-04-16 19:20:02 +00:00
|
|
|
self.__graceful_stop(StopSignal("timeout"))
|
2017-12-14 08:40:03 +00:00
|
|
|
# Check if the data is a stop signal instance
|
|
|
|
if isinstance(data, StopSignal):
|
|
|
|
# Gracefully stop the process
|
2018-04-16 19:20:02 +00:00
|
|
|
self.__graceful_stop(data)
|
2017-12-14 08:40:03 +00:00
|
|
|
# Return the received update
|
|
|
|
return data
|
2017-12-13 10:20:53 +00:00
|
|
|
|
2018-04-05 08:34:14 +00:00
|
|
|
def __wait_for_specific_message(self,
|
|
|
|
items: typing.List[str],
|
|
|
|
cancellable: bool=False) -> typing.Union[str, CancelSignal]:
|
2017-12-21 09:42:23 +00:00
|
|
|
"""Continue getting updates until until one of the strings contained in the list is received as a message."""
|
|
|
|
while True:
|
|
|
|
# Get the next update
|
|
|
|
update = self.__receive_next_update()
|
2018-02-08 09:18:53 +00:00
|
|
|
# Ensure the update isn't a CancelSignal
|
2018-04-13 07:40:31 +00:00
|
|
|
if isinstance(update, CancelSignal):
|
|
|
|
if cancellable:
|
|
|
|
# Return the CancelSignal
|
|
|
|
return update
|
|
|
|
else:
|
|
|
|
# Ignore the signal
|
|
|
|
continue
|
2017-12-21 09:42:23 +00:00
|
|
|
# Ensure the update contains a message
|
|
|
|
if update.message is None:
|
|
|
|
continue
|
|
|
|
# Ensure the message contains text
|
|
|
|
if update.message.text is None:
|
|
|
|
continue
|
|
|
|
# Check if the message is contained in the list
|
|
|
|
if update.message.text not in items:
|
|
|
|
continue
|
|
|
|
# Return the message text
|
|
|
|
return update.message.text
|
|
|
|
|
2018-04-05 08:34:14 +00:00
|
|
|
def __wait_for_regex(self, regex: str, cancellable: bool=False) -> typing.Union[str, CancelSignal]:
|
2018-01-03 13:52:05 +00:00
|
|
|
"""Continue getting updates until the regex finds a match in a message, then return the first capture group."""
|
|
|
|
while True:
|
|
|
|
# Get the next update
|
|
|
|
update = self.__receive_next_update()
|
2018-02-08 09:18:53 +00:00
|
|
|
# Ensure the update isn't a CancelSignal
|
|
|
|
if cancellable and isinstance(update, CancelSignal):
|
|
|
|
# Return the CancelSignal
|
|
|
|
return update
|
2018-01-03 13:52:05 +00:00
|
|
|
# Ensure the update contains a message
|
|
|
|
if update.message is None:
|
|
|
|
continue
|
|
|
|
# Ensure the message contains text
|
|
|
|
if update.message.text is None:
|
|
|
|
continue
|
|
|
|
# Try to match the regex with the received message
|
|
|
|
match = re.search(regex, update.message.text)
|
|
|
|
# Ensure there is a match
|
|
|
|
if match is None:
|
|
|
|
continue
|
|
|
|
# Return the first capture group
|
|
|
|
return match.group(1)
|
|
|
|
|
2018-04-05 08:34:14 +00:00
|
|
|
def __wait_for_precheckoutquery(self,
|
|
|
|
cancellable: bool=False) -> typing.Union[telegram.PreCheckoutQuery, CancelSignal]:
|
2018-01-10 10:29:02 +00:00
|
|
|
"""Continue getting updates until a precheckoutquery is received.
|
|
|
|
The payload is checked by the core before forwarding the message."""
|
2018-01-04 19:11:48 +00:00
|
|
|
while True:
|
|
|
|
# Get the next update
|
|
|
|
update = self.__receive_next_update()
|
2018-02-08 09:18:53 +00:00
|
|
|
# Ensure the update isn't a CancelSignal
|
|
|
|
if cancellable and isinstance(update, CancelSignal):
|
|
|
|
# Return the CancelSignal
|
|
|
|
return update
|
2018-01-04 19:11:48 +00:00
|
|
|
# Ensure the update contains a precheckoutquery
|
|
|
|
if update.pre_checkout_query is None:
|
|
|
|
continue
|
|
|
|
# Return the precheckoutquery
|
|
|
|
return update.pre_checkout_query
|
|
|
|
|
2018-01-10 10:29:02 +00:00
|
|
|
def __wait_for_successfulpayment(self) -> telegram.SuccessfulPayment:
|
|
|
|
"""Continue getting updates until a successfulpayment is received."""
|
|
|
|
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 is a successfulpayment
|
|
|
|
if update.message.successful_payment is None:
|
|
|
|
continue
|
|
|
|
# Return the successfulpayment
|
|
|
|
return update.message.successful_payment
|
|
|
|
|
2018-04-05 08:43:27 +00:00
|
|
|
def __wait_for_photo(self, cancellable: bool=False) -> typing.Union[typing.List[telegram.PhotoSize], CancelSignal]:
|
2018-02-15 09:37:51 +00:00
|
|
|
"""Continue getting updates until a photo is received, then return it."""
|
2018-02-01 09:06:40 +00:00
|
|
|
while True:
|
|
|
|
# Get the next update
|
|
|
|
update = self.__receive_next_update()
|
2018-02-08 09:18:53 +00:00
|
|
|
# Ensure the update isn't a CancelSignal
|
|
|
|
if cancellable and isinstance(update, CancelSignal):
|
|
|
|
# Return the CancelSignal
|
|
|
|
return update
|
2018-02-01 09:06:40 +00:00
|
|
|
# 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
|
|
|
|
|
2018-04-05 08:43:27 +00:00
|
|
|
def __wait_for_inlinekeyboard_callback(self, cancellable: bool=True) \
|
|
|
|
-> typing.Union[telegram.CallbackQuery, CancelSignal]:
|
2018-02-15 09:37:51 +00:00
|
|
|
"""Continue getting updates until an inline keyboard callback is received, then return it."""
|
|
|
|
while True:
|
|
|
|
# Get the next update
|
|
|
|
update = self.__receive_next_update()
|
2018-03-22 08:54:45 +00:00
|
|
|
# Ensure the update isn't a CancelSignal
|
|
|
|
if cancellable and isinstance(update, CancelSignal):
|
|
|
|
# Return the CancelSignal
|
|
|
|
return update
|
2018-02-15 09:37:51 +00:00
|
|
|
# Ensure the update is a CallbackQuery
|
|
|
|
if update.callback_query is None:
|
|
|
|
continue
|
2018-04-01 16:10:14 +00:00
|
|
|
# Answer the callbackquery
|
|
|
|
self.bot.answer_callback_query(update.callback_query.id)
|
2018-02-15 09:37:51 +00:00
|
|
|
# Return the callbackquery
|
|
|
|
return update.callback_query
|
|
|
|
|
2017-12-21 09:42:23 +00:00
|
|
|
def __user_menu(self):
|
|
|
|
"""Function called from the run method when the user is not an administrator.
|
|
|
|
Normal bot actions should be placed here."""
|
2017-12-22 08:56:03 +00:00
|
|
|
# Loop used to returning to the menu after executing a command
|
|
|
|
while True:
|
|
|
|
# Create a keyboard with the user main menu
|
|
|
|
keyboard = [[telegram.KeyboardButton(strings.menu_order)],
|
|
|
|
[telegram.KeyboardButton(strings.menu_order_status)],
|
|
|
|
[telegram.KeyboardButton(strings.menu_add_credit)],
|
2018-04-16 10:09:51 +00:00
|
|
|
[telegram.KeyboardButton(strings.menu_help), telegram.KeyboardButton(strings.menu_bot_info)]]
|
2017-12-22 08:56:03 +00:00
|
|
|
# Send the previously created keyboard to the user (ensuring it can be clicked only 1 time)
|
2018-04-05 07:30:32 +00:00
|
|
|
self.bot.send_message(self.chat.id,
|
2018-04-05 07:57:59 +00:00
|
|
|
strings.conversation_open_user_menu.format(credit=utils.Price(self.user.credit)),
|
2018-04-05 07:30:32 +00:00
|
|
|
reply_markup=telegram.ReplyKeyboardMarkup(keyboard, one_time_keyboard=True),
|
|
|
|
parse_mode="HTML")
|
2017-12-22 08:56:03 +00:00
|
|
|
# Wait for a reply from the user
|
|
|
|
selection = self.__wait_for_specific_message([strings.menu_order, strings.menu_order_status,
|
2018-04-16 10:09:51 +00:00
|
|
|
strings.menu_add_credit, strings.menu_bot_info,
|
|
|
|
strings.menu_help])
|
2017-12-22 08:56:03 +00:00
|
|
|
# If the user has selected the Order option...
|
|
|
|
if selection == strings.menu_order:
|
|
|
|
# Open the order menu
|
|
|
|
self.__order_menu()
|
|
|
|
# If the user has selected the Order Status option...
|
|
|
|
elif selection == strings.menu_order_status:
|
|
|
|
# Display the order(s) status
|
|
|
|
self.__order_status()
|
|
|
|
# If the user has selected the Add Credit option...
|
|
|
|
elif selection == strings.menu_add_credit:
|
|
|
|
# Display the add credit menu
|
|
|
|
self.__add_credit_menu()
|
|
|
|
# If the user has selected the Bot Info option...
|
|
|
|
elif selection == strings.menu_bot_info:
|
|
|
|
# Display information about the bot
|
|
|
|
self.__bot_info()
|
2018-04-16 10:09:51 +00:00
|
|
|
# If the user has selected the Help option...
|
|
|
|
elif selection == strings.menu_help:
|
|
|
|
# Go to the Help menu
|
|
|
|
self.__help_menu()
|
2017-12-22 08:56:03 +00:00
|
|
|
|
|
|
|
def __order_menu(self):
|
2018-02-05 11:49:21 +00:00
|
|
|
"""User menu to order products from the shop."""
|
2018-02-15 09:37:51 +00:00
|
|
|
# Get the products list from the db
|
2018-03-06 16:39:02 +00:00
|
|
|
products = self.session.query(db.Product).filter_by(deleted=False).all()
|
2018-02-15 09:37:51 +00:00
|
|
|
# Create a dict to be used as 'cart'
|
|
|
|
# The key is the message id of the product list
|
2018-02-28 19:07:40 +00:00
|
|
|
cart: typing.Dict[typing.List[db.Product, int]] = {}
|
2018-02-15 09:37:51 +00:00
|
|
|
# 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
|
2018-04-05 08:43:27 +00:00
|
|
|
inline_keyboard = telegram.InlineKeyboardMarkup([[telegram.InlineKeyboardButton(strings.menu_add_to_cart,
|
|
|
|
callback_data="cart_add")]])
|
2018-02-15 09:37:51 +00:00
|
|
|
# Edit the sent message and add the inline keyboard
|
|
|
|
if product.image is None:
|
2018-04-05 08:43:27 +00:00
|
|
|
self.bot.edit_message_text(chat_id=self.chat.id,
|
|
|
|
message_id=message['result']['message_id'],
|
|
|
|
text=product.text(),
|
|
|
|
parse_mode="HTML",
|
|
|
|
reply_markup=inline_keyboard)
|
2018-02-15 09:37:51 +00:00
|
|
|
else:
|
2018-04-05 08:43:27 +00:00
|
|
|
self.bot.edit_message_caption(chat_id=self.chat.id,
|
|
|
|
message_id=message['result']['message_id'],
|
2018-04-12 07:28:52 +00:00
|
|
|
caption=product.text(),
|
2018-04-05 08:43:27 +00:00
|
|
|
parse_mode="HTML",
|
|
|
|
reply_markup=inline_keyboard)
|
2018-02-15 09:37:51 +00:00
|
|
|
# Create the keyboard with the cancel button
|
2018-04-05 08:43:27 +00:00
|
|
|
inline_keyboard = telegram.InlineKeyboardMarkup([[telegram.InlineKeyboardButton(strings.menu_cancel,
|
|
|
|
callback_data="cart_cancel")]])
|
2018-02-15 09:37:51 +00:00
|
|
|
# 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":
|
2018-04-13 07:37:03 +00:00
|
|
|
# Get the selected product, ensuring it exists
|
|
|
|
p = cart.get(callback.message.message_id)
|
|
|
|
if p is None:
|
|
|
|
continue
|
|
|
|
product = p[0]
|
2018-02-15 09:37:51 +00:00
|
|
|
# Add 1 copy to the cart
|
|
|
|
cart[callback.message.message_id][1] += 1
|
|
|
|
# Create the product inline keyboard
|
2018-04-05 08:43:27 +00:00
|
|
|
product_inline_keyboard = telegram.InlineKeyboardMarkup(
|
|
|
|
[
|
2018-04-17 06:17:16 +00:00
|
|
|
[telegram.InlineKeyboardButton(strings.menu_add_to_cart, callback_data="cart_add"),
|
|
|
|
telegram.InlineKeyboardButton(strings.menu_remove_from_cart, callback_data="cart_remove")]
|
2018-04-05 08:43:27 +00:00
|
|
|
])
|
2018-02-15 09:37:51 +00:00
|
|
|
# Create the final inline keyboard
|
2018-04-05 08:43:27 +00:00
|
|
|
final_inline_keyboard = telegram.InlineKeyboardMarkup(
|
|
|
|
[
|
|
|
|
[telegram.InlineKeyboardButton(strings.menu_cancel, callback_data="cart_cancel")],
|
|
|
|
[telegram.InlineKeyboardButton(strings.menu_done, callback_data="cart_done")]
|
|
|
|
])
|
2018-02-15 09:37:51 +00:00
|
|
|
# Edit both the product and the final message
|
|
|
|
if product.image is None:
|
2018-04-05 08:43:27 +00:00
|
|
|
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)
|
2018-02-15 09:37:51 +00:00
|
|
|
else:
|
2018-04-05 08:43:27 +00:00
|
|
|
self.bot.edit_message_caption(chat_id=self.chat.id,
|
|
|
|
message_id=callback.message.message_id,
|
2018-04-12 07:28:52 +00:00
|
|
|
caption=product.text(cart_qty=cart[callback.message.message_id][1]),
|
2018-04-05 08:43:27 +00:00
|
|
|
parse_mode="HTML",
|
|
|
|
reply_markup=product_inline_keyboard)
|
2018-02-28 19:07:40 +00:00
|
|
|
# Create the cart summary
|
|
|
|
product_list = ""
|
2018-04-05 07:57:59 +00:00
|
|
|
total_cost = utils.Price(0)
|
2018-02-28 19:07:40 +00:00
|
|
|
for product_id in cart:
|
|
|
|
if cart[product_id][1] > 0:
|
|
|
|
product_list += cart[product_id][0].text(style="short", cart_qty=cart[product_id][1]) + "\n"
|
|
|
|
total_cost += cart[product_id][0].price * cart[product_id][1]
|
|
|
|
self.bot.edit_message_text(chat_id=self.chat.id, message_id=final.message_id,
|
2018-04-05 08:43:27 +00:00
|
|
|
text=strings.conversation_confirm_cart.format(product_list=product_list,
|
|
|
|
total_cost=str(total_cost)),
|
2018-04-17 07:51:05 +00:00
|
|
|
reply_markup=final_inline_keyboard, parse_mode="HTML")
|
2018-02-15 09:37:51 +00:00
|
|
|
# If the Remove from cart button has been pressed...
|
|
|
|
elif callback.data == "cart_remove":
|
2018-04-13 07:37:03 +00:00
|
|
|
# Get the selected product, ensuring it exists
|
|
|
|
p = cart.get(callback.message.message_id)
|
|
|
|
if p is None:
|
|
|
|
continue
|
|
|
|
product = p[0]
|
2018-02-15 09:37:51 +00:00
|
|
|
# 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
|
2018-04-05 08:43:27 +00:00
|
|
|
product_inline_list = [[telegram.InlineKeyboardButton(strings.menu_add_to_cart,
|
|
|
|
callback_data="cart_add")]]
|
2018-02-15 09:37:51 +00:00
|
|
|
if cart[callback.message.message_id][1] > 0:
|
2018-04-17 06:17:16 +00:00
|
|
|
product_inline_list[0].append(telegram.InlineKeyboardButton(strings.menu_remove_from_cart,
|
|
|
|
callback_data="cart_remove"))
|
2018-02-15 09:37:51 +00:00
|
|
|
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:
|
2018-04-05 08:43:27 +00:00
|
|
|
final_inline_list.append([telegram.InlineKeyboardButton(strings.menu_done,
|
|
|
|
callback_data="cart_done")])
|
2018-02-15 09:37:51 +00:00
|
|
|
break
|
|
|
|
final_inline_keyboard = telegram.InlineKeyboardMarkup(final_inline_list)
|
2018-02-19 12:47:43 +00:00
|
|
|
# Edit the product message
|
2018-02-15 09:37:51 +00:00
|
|
|
if product.image is None:
|
|
|
|
self.bot.edit_message_text(chat_id=self.chat.id, message_id=callback.message.message_id,
|
2018-04-05 08:43:27 +00:00
|
|
|
text=product.text(cart_qty=cart[callback.message.message_id][1]),
|
|
|
|
parse_mode="HTML", reply_markup=product_inline_keyboard)
|
2018-02-15 09:37:51 +00:00
|
|
|
else:
|
2018-04-05 08:43:27 +00:00
|
|
|
self.bot.edit_message_caption(chat_id=self.chat.id,
|
|
|
|
message_id=callback.message.message_id,
|
2018-04-12 07:28:52 +00:00
|
|
|
caption=product.text(cart_qty=cart[callback.message.message_id][1]),
|
2018-04-05 08:43:27 +00:00
|
|
|
parse_mode="HTML",
|
|
|
|
reply_markup=product_inline_keyboard)
|
2018-02-28 19:07:40 +00:00
|
|
|
# Create the cart summary
|
|
|
|
product_list = ""
|
2018-04-05 07:57:59 +00:00
|
|
|
total_cost = utils.Price(0)
|
2018-02-28 19:07:40 +00:00
|
|
|
for product_id in cart:
|
|
|
|
if cart[product_id][1] > 0:
|
|
|
|
product_list += cart[product_id][0].text(style="short", cart_qty=cart[product_id][1]) + "\n"
|
|
|
|
total_cost += cart[product_id][0].price * cart[product_id][1]
|
|
|
|
self.bot.edit_message_text(chat_id=self.chat.id, message_id=final.message_id,
|
|
|
|
text=strings.conversation_confirm_cart.format(product_list=product_list,
|
2018-03-05 12:12:38 +00:00
|
|
|
total_cost=str(total_cost)),
|
2018-04-17 07:51:05 +00:00
|
|
|
reply_markup=final_inline_keyboard, parse_mode="HTML")
|
2018-02-19 12:47:43 +00:00
|
|
|
# If the done button has been pressed...
|
|
|
|
elif callback.data == "cart_done":
|
|
|
|
# End the loop
|
|
|
|
break
|
|
|
|
# Create an inline keyboard with a single skip button
|
2018-04-05 08:43:27 +00:00
|
|
|
cancel = telegram.InlineKeyboardMarkup([[telegram.InlineKeyboardButton(strings.menu_skip,
|
|
|
|
callback_data="cmd_cancel")]])
|
2018-02-19 12:47:43 +00:00
|
|
|
# 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)
|
|
|
|
# Wait for user input
|
|
|
|
notes = self.__wait_for_regex(r"(.*)", cancellable=True)
|
|
|
|
# Create a new Order
|
|
|
|
order = db.Order(user=self.user,
|
|
|
|
creation_date=datetime.datetime.now(),
|
|
|
|
notes=notes if not isinstance(notes, CancelSignal) else "")
|
|
|
|
# Add the record to the session and get an ID
|
|
|
|
self.session.add(order)
|
|
|
|
self.session.flush()
|
|
|
|
# For each product added to the cart, create a new OrderItem and get the total value
|
|
|
|
value = 0
|
|
|
|
for product in cart:
|
|
|
|
# Add the price multiplied by the quantity to the total price
|
|
|
|
value -= cart[product][0].price * cart[product][1]
|
|
|
|
# Create {quantity} new OrderItems
|
|
|
|
for i in range(0, cart[product][1]):
|
|
|
|
orderitem = db.OrderItem(product=cart[product][0],
|
|
|
|
order_id=order.order_id)
|
|
|
|
self.session.add(orderitem)
|
|
|
|
# Ensure the user has enough credit to make the purchase
|
|
|
|
if self.user.credit + value < 0:
|
|
|
|
self.bot.send_message(self.chat.id, strings.error_not_enough_credit)
|
|
|
|
# Rollback all the changes
|
|
|
|
self.session.rollback()
|
|
|
|
return
|
|
|
|
# Create a new transaction and add it to the session
|
|
|
|
transaction = db.Transaction(user=self.user,
|
2018-03-12 09:19:01 +00:00
|
|
|
value=value,
|
|
|
|
order_id=order.order_id)
|
2018-02-19 12:47:43 +00:00
|
|
|
self.session.add(transaction)
|
|
|
|
# Subtract credit from the user
|
|
|
|
self.user.credit += value
|
|
|
|
# Commit all the changes
|
|
|
|
self.session.commit()
|
|
|
|
# Notify the user of the order result
|
|
|
|
self.bot.send_message(self.chat.id, strings.success_order_created)
|
2018-04-01 16:10:14 +00:00
|
|
|
# Notify the admins (in Live Orders mode) of the new order
|
|
|
|
admins = self.session.query(db.Admin).filter_by(live_mode=True).all()
|
|
|
|
# Create the order keyboard
|
|
|
|
order_keyboard = telegram.InlineKeyboardMarkup(
|
2018-04-05 08:43:27 +00:00
|
|
|
[
|
|
|
|
[telegram.InlineKeyboardButton(strings.menu_complete, callback_data="order_complete")],
|
|
|
|
[telegram.InlineKeyboardButton(strings.menu_refund, callback_data="order_refund")]
|
|
|
|
])
|
2018-03-07 09:48:30 +00:00
|
|
|
# Notify them of the new placed order
|
|
|
|
for admin in admins:
|
2018-04-01 16:10:14 +00:00
|
|
|
self.bot.send_message(admin.user_id,
|
|
|
|
f"{strings.notification_order_placed.format(order=order.get_text(self.session))}",
|
|
|
|
reply_markup=order_keyboard)
|
2018-02-19 12:47:43 +00:00
|
|
|
|
2017-12-22 08:56:03 +00:00
|
|
|
def __order_status(self):
|
2018-03-19 11:24:05 +00:00
|
|
|
"""Display the status of the sent orders."""
|
2018-04-05 07:30:32 +00:00
|
|
|
# Find the latest orders
|
2018-04-05 08:43:27 +00:00
|
|
|
orders = self.session.query(db.Order)\
|
|
|
|
.filter(db.Order.user == self.user)\
|
|
|
|
.order_by(db.Order.creation_date.desc())\
|
2018-04-12 07:28:52 +00:00
|
|
|
.limit(20)\
|
2018-04-05 08:43:27 +00:00
|
|
|
.all()
|
2018-04-05 07:30:32 +00:00
|
|
|
# Ensure there is at least one order to display
|
|
|
|
if len(orders) == 0:
|
|
|
|
self.bot.send_message(self.chat.id, strings.error_no_orders)
|
|
|
|
# Display the order status to the user
|
|
|
|
for order in orders:
|
|
|
|
self.bot.send_message(self.chat.id, order.get_text(self.session))
|
|
|
|
# TODO: maybe add a page displayer instead of showing the latest 5 orders
|
2017-12-22 08:56:03 +00:00
|
|
|
|
|
|
|
def __add_credit_menu(self):
|
2017-12-26 17:15:30 +00:00
|
|
|
"""Add more credit to the account."""
|
|
|
|
# Create a payment methods keyboard
|
|
|
|
keyboard = list()
|
|
|
|
# Add the supported payment methods to the keyboard
|
|
|
|
# Cash
|
|
|
|
keyboard.append([telegram.KeyboardButton(strings.menu_cash)])
|
|
|
|
# Telegram Payments
|
2018-01-10 10:29:02 +00:00
|
|
|
if configloader.config["Credit Card"]["credit_card_token"] != "":
|
2017-12-26 17:15:30 +00:00
|
|
|
keyboard.append([telegram.KeyboardButton(strings.menu_credit_card)])
|
|
|
|
# Keyboard: go back to the previous menu
|
|
|
|
keyboard.append([telegram.KeyboardButton(strings.menu_cancel)])
|
|
|
|
# Send the keyboard to the user
|
|
|
|
self.bot.send_message(self.chat.id, strings.conversation_payment_method,
|
|
|
|
reply_markup=telegram.ReplyKeyboardMarkup(keyboard, one_time_keyboard=True))
|
|
|
|
# Wait for a reply from the user
|
|
|
|
selection = self.__wait_for_specific_message([strings.menu_cash, strings.menu_credit_card, strings.menu_cancel])
|
|
|
|
# If the user has selected the Cash option...
|
|
|
|
if selection == strings.menu_cash:
|
|
|
|
# Go to the pay with cash function
|
2018-04-12 08:21:11 +00:00
|
|
|
self.bot.send_message(self.chat.id,
|
|
|
|
strings.payment_cash.format(user_cash_id=self.user.identifiable_str()),
|
|
|
|
parse_mode="HTML")
|
2017-12-26 17:15:30 +00:00
|
|
|
# If the user has selected the Credit Card option...
|
|
|
|
elif selection == strings.menu_credit_card:
|
|
|
|
# Go to the pay with credit card function
|
|
|
|
self.__add_credit_cc()
|
|
|
|
# If the user has selected the Cancel option...
|
2018-02-08 09:18:53 +00:00
|
|
|
elif selection == strings.menu_cancel:
|
2017-12-26 17:15:30 +00:00
|
|
|
# Send him back to the previous menu
|
|
|
|
return
|
|
|
|
|
|
|
|
def __add_credit_cc(self):
|
2018-01-29 12:26:49 +00:00
|
|
|
"""Add money to the wallet through a credit card payment."""
|
2018-01-04 19:11:48 +00:00
|
|
|
# Create a keyboard to be sent later
|
2018-04-05 07:57:59 +00:00
|
|
|
keyboard = [[telegram.KeyboardButton(str(utils.Price("10.00")))],
|
|
|
|
[telegram.KeyboardButton(str(utils.Price("25.00")))],
|
|
|
|
[telegram.KeyboardButton(str(utils.Price("50.00")))],
|
|
|
|
[telegram.KeyboardButton(str(utils.Price("100.00")))],
|
2018-02-08 09:18:53 +00:00
|
|
|
[telegram.KeyboardButton(strings.menu_cancel)]]
|
|
|
|
# Boolean variable to check if the user has cancelled the action
|
|
|
|
cancelled = False
|
2018-01-03 13:52:05 +00:00
|
|
|
# Loop used to continue asking if there's an error during the input
|
2018-02-08 09:18:53 +00:00
|
|
|
while not cancelled:
|
2018-01-03 13:52:05 +00:00
|
|
|
# Send the message and the keyboard
|
|
|
|
self.bot.send_message(self.chat.id, strings.payment_cc_amount,
|
|
|
|
reply_markup=telegram.ReplyKeyboardMarkup(keyboard, one_time_keyboard=True))
|
|
|
|
# Wait until a valid amount is sent
|
|
|
|
# TODO: check and debug the regex
|
2018-02-19 12:47:43 +00:00
|
|
|
selection = self.__wait_for_regex(r"([0-9]{1,3}(?:[.,][0-9]+)?|" + strings.menu_cancel + r")")
|
2018-02-08 09:18:53 +00:00
|
|
|
# If the user cancelled the action
|
|
|
|
if selection == strings.menu_cancel:
|
|
|
|
# Exit the loop
|
|
|
|
cancelled = True
|
|
|
|
continue
|
|
|
|
# Convert the amount to an integer
|
2018-04-05 07:57:59 +00:00
|
|
|
value = utils.Price(selection)
|
2018-01-03 13:52:05 +00:00
|
|
|
# Ensure the amount is within the range
|
2018-04-05 07:57:59 +00:00
|
|
|
if value > utils.Price(int(configloader.config["Credit Card"]["max_amount"])):
|
2018-04-05 08:43:27 +00:00
|
|
|
self.bot.send_message(self.chat.id,
|
|
|
|
strings.error_payment_amount_over_max.format(
|
|
|
|
max_amount=utils.Price(configloader.config["Payments"]["max_amount"])))
|
2018-01-03 13:52:05 +00:00
|
|
|
continue
|
2018-04-05 07:57:59 +00:00
|
|
|
elif value < utils.Price(int(configloader.config["Credit Card"]["min_amount"])):
|
2018-04-05 08:43:27 +00:00
|
|
|
self.bot.send_message(self.chat.id,
|
|
|
|
strings.error_payment_amount_under_min.format(
|
|
|
|
min_amount=utils.Price(configloader.config["Payments"]["min_amount"])))
|
2018-01-03 13:52:05 +00:00
|
|
|
continue
|
2018-01-03 14:23:08 +00:00
|
|
|
break
|
2018-02-08 09:18:53 +00:00
|
|
|
# If the user cancelled the action...
|
|
|
|
else:
|
|
|
|
# Exit the function
|
|
|
|
return
|
2018-01-10 10:29:02 +00:00
|
|
|
# Set the invoice active invoice payload
|
|
|
|
self.invoice_payload = str(uuid.uuid4())
|
2018-01-15 09:16:04 +00:00
|
|
|
# Create the price array
|
2018-02-28 19:07:40 +00:00
|
|
|
prices = [telegram.LabeledPrice(label=strings.payment_invoice_label, amount=int(value))]
|
2018-01-15 09:16:04 +00:00
|
|
|
# If the user has to pay a fee when using the credit card, add it to the prices list
|
2018-03-12 09:19:01 +00:00
|
|
|
fee_percentage = float(configloader.config["Credit Card"]["fee_percentage"]) / 100
|
2018-03-22 09:33:50 +00:00
|
|
|
fee_fixed = int(configloader.config["Credit Card"]["fee_fixed"])
|
2018-02-28 19:07:40 +00:00
|
|
|
total_fee = value * fee_percentage + fee_fixed
|
2018-01-29 10:46:01 +00:00
|
|
|
if total_fee > 0:
|
|
|
|
prices.append(telegram.LabeledPrice(label=strings.payment_invoice_fee_label, amount=int(total_fee)))
|
|
|
|
else:
|
|
|
|
# Otherwise, set the fee to 0 to ensure no accidental discounts are applied
|
|
|
|
total_fee = 0
|
2018-02-08 09:18:53 +00:00
|
|
|
# Create the invoice keyboard
|
2018-02-15 07:49:04 +00:00
|
|
|
inline_keyboard = telegram.InlineKeyboardMarkup([[telegram.InlineKeyboardButton(strings.menu_pay, pay=True)],
|
2018-04-05 08:43:27 +00:00
|
|
|
[telegram.InlineKeyboardButton(strings.menu_cancel,
|
|
|
|
callback_data="cmd_cancel")]])
|
2018-01-04 19:11:48 +00:00
|
|
|
# The amount is valid, send the invoice
|
|
|
|
self.bot.send_invoice(self.chat.id,
|
|
|
|
title=strings.payment_invoice_title,
|
2018-02-28 19:07:40 +00:00
|
|
|
description=strings.payment_invoice_description.format(amount=str(value)),
|
2018-01-10 10:29:02 +00:00
|
|
|
payload=self.invoice_payload,
|
|
|
|
provider_token=configloader.config["Credit Card"]["credit_card_token"],
|
2018-01-04 19:11:48 +00:00
|
|
|
start_parameter="tempdeeplink", # TODO: no idea on how deeplinks should work
|
|
|
|
currency=configloader.config["Payments"]["currency"],
|
2018-01-15 09:16:04 +00:00
|
|
|
prices=prices,
|
2018-01-10 10:29:02 +00:00
|
|
|
need_name=configloader.config["Credit Card"]["name_required"] == "yes",
|
|
|
|
need_email=configloader.config["Credit Card"]["email_required"] == "yes",
|
2018-02-08 09:18:53 +00:00
|
|
|
need_phone_number=configloader.config["Credit Card"]["phone_required"] == "yes",
|
|
|
|
reply_markup=inline_keyboard)
|
2018-01-04 19:11:48 +00:00
|
|
|
# Wait for the invoice
|
2018-02-08 09:18:53 +00:00
|
|
|
precheckoutquery = self.__wait_for_precheckoutquery(cancellable=True)
|
|
|
|
# Check if the user has cancelled the invoice
|
|
|
|
if isinstance(precheckoutquery, CancelSignal):
|
|
|
|
# Exit the function
|
|
|
|
return
|
2018-01-10 10:29:02 +00:00
|
|
|
# Accept the checkout
|
|
|
|
self.bot.answer_pre_checkout_query(precheckoutquery.id, ok=True)
|
|
|
|
# Wait for the payment
|
|
|
|
successfulpayment = self.__wait_for_successfulpayment()
|
|
|
|
# Create a new database transaction
|
|
|
|
transaction = db.Transaction(user=self.user,
|
2018-01-29 10:46:01 +00:00
|
|
|
value=successfulpayment.total_amount - int(total_fee),
|
2018-01-10 10:29:02 +00:00
|
|
|
provider="Credit Card",
|
|
|
|
telegram_charge_id=successfulpayment.telegram_payment_charge_id,
|
|
|
|
provider_charge_id=successfulpayment.provider_payment_charge_id)
|
|
|
|
if successfulpayment.order_info is not None:
|
|
|
|
transaction.payment_name = successfulpayment.order_info.name
|
|
|
|
transaction.payment_email = successfulpayment.order_info.email
|
|
|
|
transaction.payment_phone = successfulpayment.order_info.phone_number
|
|
|
|
# Add the credit to the user account
|
2018-02-28 19:07:40 +00:00
|
|
|
self.user.credit += successfulpayment.total_amount - int(total_fee)
|
2018-01-10 10:29:02 +00:00
|
|
|
# Add and commit the transaction
|
|
|
|
self.session.add(transaction)
|
|
|
|
self.session.commit()
|
2017-12-21 09:42:23 +00:00
|
|
|
|
2017-12-22 08:56:03 +00:00
|
|
|
def __bot_info(self):
|
|
|
|
"""Send information about the bot."""
|
|
|
|
self.bot.send_message(self.chat.id, strings.bot_info, parse_mode="HTML")
|
2017-12-21 09:42:23 +00:00
|
|
|
|
|
|
|
def __admin_menu(self):
|
|
|
|
"""Function called from the run method when the user is an administrator.
|
|
|
|
Administrative bot actions should be placed here."""
|
2018-01-29 12:26:49 +00:00
|
|
|
# Loop used to return to the menu after executing a command
|
|
|
|
while True:
|
2018-03-22 09:33:50 +00:00
|
|
|
# Create a keyboard with the admin main menu based on the admin permissions specified in the db
|
|
|
|
keyboard = [[strings.menu_user_mode]]
|
|
|
|
if self.admin.edit_products:
|
|
|
|
keyboard.append([strings.menu_products])
|
|
|
|
if self.admin.receive_orders:
|
|
|
|
keyboard.append([strings.menu_orders])
|
2018-04-12 07:28:52 +00:00
|
|
|
if self.admin.create_transactions:
|
|
|
|
keyboard.append([strings.menu_edit_credit])
|
2018-01-29 12:26:49 +00:00
|
|
|
# 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,
|
2018-04-17 07:51:05 +00:00
|
|
|
reply_markup=telegram.ReplyKeyboardMarkup(keyboard, one_time_keyboard=True),
|
|
|
|
parse_mode="HTML")
|
2018-01-29 12:26:49 +00:00
|
|
|
# Wait for a reply from the user
|
|
|
|
selection = self.__wait_for_specific_message([strings.menu_products, strings.menu_orders,
|
2018-04-12 07:28:52 +00:00
|
|
|
strings.menu_user_mode, strings.menu_edit_credit])
|
2018-01-29 12:26:49 +00:00
|
|
|
# If the user has selected the Products option...
|
|
|
|
if selection == strings.menu_products:
|
|
|
|
# Open the products menu
|
|
|
|
self.__products_menu()
|
|
|
|
# If the user has selected the Orders option...
|
|
|
|
elif selection == strings.menu_orders:
|
|
|
|
# Open the orders menu
|
|
|
|
self.__orders_menu()
|
2018-04-12 07:28:52 +00:00
|
|
|
# If the user has selected the Transactions option...
|
|
|
|
elif selection == strings.menu_edit_credit:
|
|
|
|
# Open the edit credit menu
|
|
|
|
self.__create_transaction()
|
2018-01-29 12:26:49 +00:00
|
|
|
# If the user has selected the User mode option...
|
|
|
|
elif selection == strings.menu_user_mode:
|
|
|
|
# Start the bot in user mode
|
|
|
|
self.__user_menu()
|
|
|
|
|
|
|
|
def __products_menu(self):
|
|
|
|
"""Display the admin menu to select a product to edit."""
|
|
|
|
# Get the products list from the db
|
2018-03-06 16:39:02 +00:00
|
|
|
products = self.session.query(db.Product).filter_by(deleted=False).all()
|
2018-01-29 12:26:49 +00:00
|
|
|
# Create a list of product names
|
|
|
|
product_names = [product.name for product in products]
|
2018-03-06 16:39:02 +00:00
|
|
|
# Insert at the start of the list the add product option, the remove product option and the Cancel option
|
2018-02-08 09:18:53 +00:00
|
|
|
product_names.insert(0, strings.menu_cancel)
|
|
|
|
product_names.insert(1, strings.menu_add_product)
|
2018-03-06 16:39:02 +00:00
|
|
|
product_names.insert(2, strings.menu_delete_product)
|
2018-01-29 12:26:49 +00:00
|
|
|
# Create a keyboard using the product names
|
|
|
|
keyboard = [[telegram.KeyboardButton(product_name)] for product_name in product_names]
|
|
|
|
# Send the previously created keyboard to the user (ensuring it can be clicked only 1 time)
|
|
|
|
self.bot.send_message(self.chat.id, strings.conversation_admin_select_product,
|
|
|
|
reply_markup=telegram.ReplyKeyboardMarkup(keyboard, one_time_keyboard=True))
|
|
|
|
# Wait for a reply from the user
|
|
|
|
selection = self.__wait_for_specific_message(product_names)
|
2018-02-08 09:18:53 +00:00
|
|
|
# If the user has selected the Cancel option...
|
|
|
|
if selection == strings.menu_cancel:
|
|
|
|
# Exit the menu
|
|
|
|
return
|
2018-01-29 12:26:49 +00:00
|
|
|
# If the user has selected the Add Product option...
|
2018-02-08 09:18:53 +00:00
|
|
|
elif selection == strings.menu_add_product:
|
2018-01-29 12:26:49 +00:00
|
|
|
# Open the add product menu
|
2018-02-01 09:06:40 +00:00
|
|
|
self.__edit_product_menu()
|
2018-03-06 16:39:02 +00:00
|
|
|
# If the user has selected the Remove Product option...
|
|
|
|
elif selection == strings.menu_delete_product:
|
|
|
|
# Open the delete product menu
|
|
|
|
self.__delete_product_menu()
|
2018-01-29 12:26:49 +00:00
|
|
|
# If the user has selected a product
|
|
|
|
else:
|
2018-02-01 09:06:40 +00:00
|
|
|
# Find the selected product
|
2018-03-06 16:39:02 +00:00
|
|
|
product = self.session.query(db.Product).filter_by(name=selection, deleted=False).one()
|
2018-01-29 12:26:49 +00:00
|
|
|
# Open the edit menu for that specific product
|
2018-02-01 09:06:40 +00:00
|
|
|
self.__edit_product_menu(product=product)
|
2018-01-29 12:26:49 +00:00
|
|
|
|
2018-02-01 09:06:40 +00:00
|
|
|
def __edit_product_menu(self, product: typing.Optional[db.Product]=None):
|
|
|
|
"""Add a product to the database or edit an existing one."""
|
2018-02-08 09:18:53 +00:00
|
|
|
# Create an inline keyboard with a single skip button
|
2018-04-05 08:43:27 +00:00
|
|
|
cancel = telegram.InlineKeyboardMarkup([[telegram.InlineKeyboardButton(strings.menu_skip,
|
|
|
|
callback_data="cmd_cancel")]])
|
2018-01-29 12:26:49 +00:00
|
|
|
# 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)
|
2018-02-01 09:06:40 +00:00
|
|
|
# Display the current name if you're editing an existing product
|
|
|
|
if product:
|
2018-04-05 08:43:27 +00:00
|
|
|
self.bot.send_message(self.chat.id, strings.edit_current_value.format(value=escape(product.name)),
|
|
|
|
parse_mode="HTML",
|
|
|
|
reply_markup=cancel)
|
2018-01-29 12:26:49 +00:00
|
|
|
# Wait for an answer
|
2018-02-08 09:18:53 +00:00
|
|
|
name = self.__wait_for_regex(r"(.*)", cancellable=bool(product))
|
2018-01-29 12:26:49 +00:00
|
|
|
# Ensure a product with that name doesn't already exist
|
2018-04-05 08:43:27 +00:00
|
|
|
if (product and isinstance(name, CancelSignal)) or\
|
|
|
|
self.session.query(db.Product).filter_by(name=name, deleted=False).one_or_none() in [None, product]:
|
2018-01-29 12:26:49 +00:00
|
|
|
# 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)
|
2018-02-01 09:06:40 +00:00
|
|
|
# Display the current description if you're editing an existing product
|
|
|
|
if product:
|
2018-04-09 10:24:55 +00:00
|
|
|
self.bot.send_message(self.chat.id,
|
|
|
|
strings.edit_current_value.format(value=escape(product.description)),
|
2018-04-05 08:43:27 +00:00
|
|
|
parse_mode="HTML",
|
|
|
|
reply_markup=cancel)
|
2018-01-29 12:26:49 +00:00
|
|
|
# Wait for an answer
|
2018-02-08 09:18:53 +00:00
|
|
|
description = self.__wait_for_regex(r"(.*)", cancellable=bool(product))
|
2018-01-29 12:26:49 +00:00
|
|
|
# Ask for the product price
|
2018-04-09 10:24:55 +00:00
|
|
|
self.bot.send_message(self.chat.id,
|
|
|
|
strings.ask_product_price,
|
2018-04-05 08:43:27 +00:00
|
|
|
parse_mode="HTML")
|
2018-02-01 09:06:40 +00:00
|
|
|
# Display the current name if you're editing an existing product
|
|
|
|
if product:
|
2018-04-05 08:43:27 +00:00
|
|
|
self.bot.send_message(self.chat.id,
|
|
|
|
strings.edit_current_value.format(
|
|
|
|
value=(str(utils.Price(product.price))
|
|
|
|
if product.price is not None else 'Non in vendita')),
|
|
|
|
parse_mode="HTML",
|
|
|
|
reply_markup=cancel)
|
2018-02-01 09:06:40 +00:00
|
|
|
# Wait for an answer
|
2018-04-05 08:43:27 +00:00
|
|
|
price = self.__wait_for_regex(r"([0-9]{1,3}(?:[.,][0-9]{1,2})?|[Xx])",
|
|
|
|
cancellable=True)
|
2018-02-01 09:06:40 +00:00
|
|
|
# If the price is skipped
|
2018-02-08 09:18:53 +00:00
|
|
|
if isinstance(price, CancelSignal):
|
|
|
|
pass
|
|
|
|
elif price.lower() == "x":
|
2018-02-01 09:06:40 +00:00
|
|
|
price = None
|
|
|
|
else:
|
2018-04-05 07:57:59 +00:00
|
|
|
price = utils.Price(price)
|
2018-02-01 09:06:40 +00:00
|
|
|
# Ask for the product image
|
2018-02-08 09:18:53 +00:00
|
|
|
self.bot.send_message(self.chat.id, strings.ask_product_image, reply_markup=cancel)
|
2018-01-29 12:26:49 +00:00
|
|
|
# Wait for an answer
|
2018-02-08 09:18:53 +00:00
|
|
|
photo_list = self.__wait_for_photo(cancellable=True)
|
2018-02-01 09:06:40 +00:00
|
|
|
# If a new product is being added...
|
|
|
|
if not product:
|
|
|
|
# Create the db record for the product
|
2018-04-09 10:24:55 +00:00
|
|
|
# noinspection PyTypeChecker
|
2018-02-01 09:06:40 +00:00
|
|
|
product = db.Product(name=name,
|
|
|
|
description=description,
|
2018-02-28 19:07:40 +00:00
|
|
|
price=int(price),
|
2018-03-06 16:39:02 +00:00
|
|
|
deleted=False)
|
2018-02-01 09:06:40 +00:00
|
|
|
# Add the record to the database
|
|
|
|
self.session.add(product)
|
|
|
|
# If a product is being edited...
|
|
|
|
else:
|
|
|
|
# Edit the record with the new values
|
2018-02-08 09:18:53 +00:00
|
|
|
product.name = name if not isinstance(name, CancelSignal) else product.name
|
|
|
|
product.description = description if not isinstance(description, CancelSignal) else product.description
|
|
|
|
product.price = price if not isinstance(price, CancelSignal) else product.price
|
|
|
|
# If a photo has been sent...
|
|
|
|
if not isinstance(photo_list, CancelSignal):
|
|
|
|
# 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)
|
|
|
|
# Commit the session changes
|
2018-01-29 12:26:49 +00:00
|
|
|
self.session.commit()
|
|
|
|
# Notify the user
|
2018-02-15 07:49:04 +00:00
|
|
|
self.bot.send_message(self.chat.id, strings.success_product_edited)
|
2018-01-29 12:26:49 +00:00
|
|
|
|
2018-03-06 16:39:02 +00:00
|
|
|
def __delete_product_menu(self):
|
|
|
|
# Get the products list from the db
|
|
|
|
products = self.session.query(db.Product).filter_by(deleted=False).all()
|
|
|
|
# Create a list of product names
|
|
|
|
product_names = [product.name for product in products]
|
|
|
|
# Insert at the start of the list the Cancel button
|
|
|
|
product_names.insert(0, strings.menu_cancel)
|
|
|
|
# Create a keyboard using the product names
|
|
|
|
keyboard = [[telegram.KeyboardButton(product_name)] for product_name in product_names]
|
|
|
|
# Send the previously created keyboard to the user (ensuring it can be clicked only 1 time)
|
|
|
|
self.bot.send_message(self.chat.id, strings.conversation_admin_select_product_to_delete,
|
|
|
|
reply_markup=telegram.ReplyKeyboardMarkup(keyboard, one_time_keyboard=True))
|
|
|
|
# Wait for a reply from the user
|
|
|
|
selection = self.__wait_for_specific_message(product_names)
|
|
|
|
if selection == strings.menu_cancel:
|
|
|
|
# Exit the menu
|
|
|
|
return
|
|
|
|
else:
|
|
|
|
# Find the selected product
|
|
|
|
product = self.session.query(db.Product).filter_by(name=selection, deleted=False).one()
|
|
|
|
# "Delete" the product by setting the deleted flag to true
|
|
|
|
product.deleted = True
|
|
|
|
self.session.commit()
|
|
|
|
# Notify the user
|
|
|
|
self.bot.send_message(self.chat.id, strings.success_product_deleted)
|
|
|
|
|
2018-01-29 12:26:49 +00:00
|
|
|
def __orders_menu(self):
|
2018-03-19 11:24:05 +00:00
|
|
|
"""Display a live flow of orders."""
|
2018-03-22 08:54:45 +00:00
|
|
|
# Create a cancel and a stop keyboard
|
2018-04-05 08:43:27 +00:00
|
|
|
stop_keyboard = telegram.InlineKeyboardMarkup([[telegram.InlineKeyboardButton(strings.menu_stop,
|
|
|
|
callback_data="cmd_cancel")]])
|
|
|
|
cancel_keyboard = telegram.InlineKeyboardMarkup([[telegram.InlineKeyboardButton(strings.menu_cancel,
|
|
|
|
callback_data="cmd_cancel")]])
|
2018-03-19 11:24:05 +00:00
|
|
|
# Send a small intro message on the Live Orders mode
|
2018-04-17 07:51:05 +00:00
|
|
|
self.bot.send_message(self.chat.id, strings.conversation_live_orders_start, reply_markup=stop_keyboard,
|
|
|
|
parse_mode="HTML")
|
2018-03-19 11:24:05 +00:00
|
|
|
# Create the order keyboard
|
2018-04-05 08:43:27 +00:00
|
|
|
order_keyboard = telegram.InlineKeyboardMarkup([[telegram.InlineKeyboardButton(strings.menu_complete,
|
|
|
|
callback_data="order_complete")],
|
|
|
|
[telegram.InlineKeyboardButton(strings.menu_refund,
|
|
|
|
callback_data="order_refund")]])
|
2018-03-19 11:24:05 +00:00
|
|
|
# Display the past pending orders
|
2018-04-05 08:43:27 +00:00
|
|
|
orders = self.session.query(db.Order)\
|
|
|
|
.filter_by(delivery_date=None, refund_date=None)\
|
|
|
|
.join(db.Transaction)\
|
|
|
|
.join(db.User)\
|
|
|
|
.all()
|
2018-03-19 11:24:05 +00:00
|
|
|
# Create a message for every one of them
|
|
|
|
for order in orders:
|
|
|
|
# Send the created message
|
2018-04-05 08:43:27 +00:00
|
|
|
self.bot.send_message(self.chat.id, order.get_text(session=self.session),
|
|
|
|
reply_markup=order_keyboard)
|
2018-04-01 16:10:14 +00:00
|
|
|
# Set the Live mode flag to True
|
|
|
|
self.admin.live_mode = True
|
|
|
|
# Commit the change to the database
|
|
|
|
self.session.commit()
|
2018-03-22 08:54:45 +00:00
|
|
|
while True:
|
|
|
|
# Wait for any message to stop the listening mode
|
|
|
|
update = self.__wait_for_inlinekeyboard_callback(cancellable=True)
|
|
|
|
# If the user pressed the stop button, exit listening mode
|
|
|
|
if isinstance(update, CancelSignal):
|
|
|
|
# Stop the listening mode
|
2018-04-01 16:10:14 +00:00
|
|
|
self.admin.live_mode = False
|
|
|
|
break
|
|
|
|
# Find the order
|
|
|
|
# TODO: debug the regex?
|
|
|
|
order_id = re.search(strings.order_number.replace("{id}", "([0-9]+)"), update.message.text).group(1)
|
|
|
|
order = self.session.query(db.Order).filter(db.Order.order_id == order_id).one()
|
|
|
|
# Check if the order hasn't been already cleared
|
|
|
|
if order.delivery_date is not None or order.refund_date is not None:
|
|
|
|
# Notify the admin and skip that order
|
|
|
|
self.bot.edit_message_text(self.chat.id, strings.error_order_already_cleared)
|
2018-03-22 09:33:50 +00:00
|
|
|
break
|
2018-03-22 08:54:45 +00:00
|
|
|
# If the user pressed the complete order button, complete the order
|
2018-04-01 16:10:14 +00:00
|
|
|
if update.data == "order_complete":
|
2018-03-22 08:54:45 +00:00
|
|
|
# Mark the order as complete
|
2018-04-01 16:10:14 +00:00
|
|
|
order.delivery_date = datetime.datetime.now()
|
2018-03-22 08:54:45 +00:00
|
|
|
# Commit the transaction
|
|
|
|
self.session.commit()
|
2018-04-01 16:10:14 +00:00
|
|
|
# Update order message
|
|
|
|
self.bot.edit_message_text(order.get_text(session=self.session), chat_id=self.chat.id,
|
|
|
|
message_id=update.message.message_id)
|
2018-03-22 08:54:45 +00:00
|
|
|
# Notify the user of the completition
|
2018-04-05 08:43:27 +00:00
|
|
|
self.bot.send_message(order.user_id,
|
|
|
|
strings.notification_order_completed.format(order=order.get_text(self.session)))
|
2018-03-22 08:54:45 +00:00
|
|
|
# If the user pressed the refund order button, refund the order...
|
|
|
|
elif update.data == "order_refund":
|
|
|
|
# Ask for a refund reason
|
2018-04-16 10:28:00 +00:00
|
|
|
reason_msg = self.bot.send_message(self.chat.id, strings.ask_refund_reason, reply_markup=cancel_keyboard)
|
2018-03-22 08:54:45 +00:00
|
|
|
# Wait for a reply
|
|
|
|
reply = self.__wait_for_regex("(.*)", cancellable=True)
|
|
|
|
# If the user pressed the cancel button, cancel the refund
|
|
|
|
if isinstance(reply, CancelSignal):
|
2018-04-16 10:28:00 +00:00
|
|
|
# Delete the message asking for the refund reason
|
|
|
|
self.bot.delete_message(self.chat.id, reason_msg.message_id)
|
2018-03-22 08:54:45 +00:00
|
|
|
continue
|
|
|
|
# Mark the order as refunded
|
|
|
|
order.refund_date = datetime.datetime.now()
|
|
|
|
# Save the refund reason
|
|
|
|
order.refund_reason = reply
|
|
|
|
# Refund the credit, reverting the old transaction
|
|
|
|
order.transaction.refunded = True
|
|
|
|
# Restore the credit to the user
|
|
|
|
order.user.credit -= order.transaction.value
|
|
|
|
# Commit the changes
|
|
|
|
self.session.commit()
|
2018-04-01 16:10:14 +00:00
|
|
|
# Update the order message
|
2018-04-05 08:43:27 +00:00
|
|
|
self.bot.edit_message_text(order.get_text(session=self.session),
|
|
|
|
chat_id=self.chat.id,
|
|
|
|
message_id=update.message.message_id)
|
2018-03-22 08:54:45 +00:00
|
|
|
# Notify the user of the refund
|
2018-04-05 08:43:27 +00:00
|
|
|
self.bot.send_message(order.user_id,
|
|
|
|
strings.notification_order_refunded.format(order=order.get_text(self.session)))
|
2018-04-16 10:28:00 +00:00
|
|
|
# Notify the admin of the refund
|
|
|
|
self.bot.send_message(self.chat.id, strings.success_order_refunded.format(order_id=order.order_id))
|
2017-12-21 09:42:23 +00:00
|
|
|
|
2018-04-12 07:28:52 +00:00
|
|
|
def __create_transaction(self):
|
|
|
|
"""Edit manually the credit of an user."""
|
|
|
|
# Find all the users in the database
|
2018-04-12 08:21:11 +00:00
|
|
|
users = self.session.query(db.User).order_by(db.User.user_id).all()
|
2018-04-12 07:28:52 +00:00
|
|
|
# Create a list containing all the keyboard button strings
|
|
|
|
keyboard_buttons = [[strings.menu_cancel]]
|
|
|
|
# Add to the list all the users
|
|
|
|
for user in users:
|
|
|
|
keyboard_buttons.append([user.identifiable_str()])
|
|
|
|
# TODO: handle more than 99 users
|
|
|
|
# Create the keyboard
|
|
|
|
keyboard = telegram.ReplyKeyboardMarkup(keyboard_buttons, one_time_keyboard=True)
|
|
|
|
# Send the keyboard
|
|
|
|
self.bot.send_message(self.chat.id, strings.conversation_admin_select_user, reply_markup=keyboard)
|
|
|
|
# Wait for a reply
|
|
|
|
reply = self.__wait_for_regex("user_([0-9]+)", cancellable=True)
|
|
|
|
# Allow the cancellation of the operation
|
|
|
|
if reply == strings.menu_cancel:
|
|
|
|
return
|
|
|
|
# Find the user in the database
|
|
|
|
user = self.session.query(db.User).filter_by(user_id=int(reply)).one_or_none()
|
|
|
|
# Ensure the user exists
|
|
|
|
if not user:
|
|
|
|
self.bot.send_message(self.chat.id, strings.error_user_does_not_exist)
|
|
|
|
# Create an inline keyboard with a single cancel button
|
|
|
|
cancel = telegram.InlineKeyboardMarkup([[telegram.InlineKeyboardButton(strings.menu_cancel,
|
|
|
|
callback_data="cmd_cancel")]])
|
|
|
|
# Request from the user the amount of money to be credited manually
|
|
|
|
self.bot.send_message(self.chat.id, strings.ask_credit, reply_markup=cancel)
|
|
|
|
# Wait for an answer
|
|
|
|
reply = self.__wait_for_regex(r"(-? ?[0-9]{1,3}(?:[.,][0-9]{1,2})?)", cancellable=True)
|
|
|
|
# Allow the cancellation of the operation
|
|
|
|
if isinstance(reply, CancelSignal):
|
|
|
|
return
|
|
|
|
# Convert the reply to a price object
|
|
|
|
price = utils.Price(reply)
|
|
|
|
# Ask the user for notes
|
|
|
|
self.bot.send_message(self.chat.id, strings.ask_transaction_notes, reply_markup=cancel)
|
|
|
|
# Wait for an answer
|
|
|
|
reply = self.__wait_for_regex(r"(.*)", cancellable=True)
|
|
|
|
# Allow the cancellation of the operation
|
|
|
|
if isinstance(reply, CancelSignal):
|
|
|
|
return
|
|
|
|
# Create a new transaction
|
|
|
|
transaction = db.Transaction(user=user,
|
|
|
|
value=int(price),
|
|
|
|
provider="Manual",
|
|
|
|
notes=reply)
|
|
|
|
self.session.add(transaction)
|
|
|
|
# Change the user credit
|
|
|
|
user.credit += int(price)
|
|
|
|
# Commit the changes
|
|
|
|
self.session.commit()
|
|
|
|
# Notify the user of the credit/debit
|
|
|
|
self.bot.send_message(user.user_id,
|
|
|
|
strings.notification_transaction_created.format(transaction=str(transaction)),
|
|
|
|
parse_mode="HTML")
|
|
|
|
# Notify the admin of the success
|
|
|
|
self.bot.send_message(self.chat.id, strings.success_transaction_created)
|
|
|
|
|
2018-04-16 10:09:51 +00:00
|
|
|
def __help_menu(self):
|
|
|
|
"""Help menu. Allows the user to ask for assistance, get a guide or see some info about the bot."""
|
|
|
|
# Create a keyboard with the user help menu
|
|
|
|
keyboard = [[telegram.KeyboardButton(strings.menu_guide)],
|
|
|
|
[telegram.KeyboardButton(strings.menu_contact_shopkeeper)],
|
|
|
|
[telegram.KeyboardButton(strings.menu_bot_info)],
|
|
|
|
[telegram.KeyboardButton(strings.menu_cancel)]]
|
|
|
|
# Send the previously created keyboard to the user (ensuring it can be clicked only 1 time)
|
|
|
|
self.bot.send_message(self.chat.id,
|
|
|
|
strings.conversation_open_help_menu,
|
|
|
|
reply_markup=telegram.ReplyKeyboardMarkup(keyboard, one_time_keyboard=True),
|
|
|
|
parse_mode="HTML")
|
|
|
|
# Wait for a reply from the user
|
|
|
|
selection = self.__wait_for_specific_message([strings.menu_guide, strings.menu_contact_shopkeeper,
|
|
|
|
strings.menu_cancel])
|
|
|
|
# If the user has selected the Guide option...
|
|
|
|
if selection == strings.menu_guide:
|
|
|
|
# Send them the bot guide
|
|
|
|
self.bot.send_message(self.chat.id, strings.help_msg)
|
|
|
|
# If the user has selected the Order Status option...
|
|
|
|
elif selection == strings.menu_contact_shopkeeper:
|
|
|
|
# Find the list of available shopkeepers
|
|
|
|
shopkeepers = self.session.query(db.Admin).filter_by(display_on_help=True).join(db.User).all()
|
|
|
|
# Create the string
|
|
|
|
shopkeepers_string = "\n".join([admin.user.mention() for admin in shopkeepers])
|
|
|
|
# Send the message to the user
|
|
|
|
self.bot.send_message(self.chat.id, strings.contact_shopkeeper.format(shopkeepers=shopkeepers_string))
|
2018-04-16 10:18:42 +00:00
|
|
|
# If the user has selected the Cancel option the function will return immediately
|
2018-04-16 10:09:51 +00:00
|
|
|
|
2018-04-16 19:20:02 +00:00
|
|
|
def __graceful_stop(self, stop_trigger: StopSignal):
|
2017-12-14 08:40:03 +00:00
|
|
|
"""Handle the graceful stop of the thread."""
|
2018-04-16 19:20:02 +00:00
|
|
|
# If the session has expired...
|
|
|
|
if stop_trigger.reason == "timeout":
|
|
|
|
# Notify the user that the session has expired and remove the keyboard
|
|
|
|
self.bot.send_message(self.chat.id, strings.conversation_expired, reply_markup=telegram.ReplyKeyboardRemove())
|
|
|
|
# If a restart has been requested...
|
|
|
|
# Do nothing.
|
2017-12-21 09:42:23 +00:00
|
|
|
# Close the database session
|
2017-12-14 08:40:03 +00:00
|
|
|
# End the process
|
|
|
|
sys.exit(0)
|